Skip to content

Java分布式架构面试题库 ​

生成时间:2026-05-24 21:09


【面试题】1. CAP定理中,你的系统该选CP还是AP?举个实际场景说明。 ​

【标准答案】这得看业务场景。CAP说分布式系统只能三选二,P(分区容错)是必须的,因为网络分区不可避免,所以实际是CP和AP的选择。

选CP的场景:金融账户、库存扣减。比如银行转账,一致性优先,宁可服务不可用也不能账户数据错乱。我做过一个支付系统,数据库分库分表后,我们用分布式事务保证强一致,万一网络抖动,支付接口直接返回失败,用户重试就行,但绝不允许扣了A的钱B没收到。

选AP的场景:社交动态、商品推荐。比如朋友圈点赞数,稍微不一致没关系,用户刷新一下就一致了。我经手过一个电商推荐系统,用的是最终一致性,用户看的是缓存数据,可能落后主库几秒,但用户体验好,系统可用性高。

核心判断标准:数据不一致会不会造成资金损失、法律风险?会就选CP,不会就选AP。不要一刀切,一个系统里不同模块可以有不同的选择。


【面试题】2. Redis分布式锁用SET NX EX,什么情况下会出问题?Redlock能解决吗? ​

【标准答案】SET NX EX确实有坑,我踩过。

问题一:锁续期问题。业务执行时间超过锁过期时间,锁自动释放,其他线程拿到锁,两个线程同时操作临界资源。解决方案是开个守护线程定时续期,或者直接用Redisson的看门狗机制。

问题二:主从切换丢锁。客户端A在Master上加锁成功,还没同步到Slave,Master挂了,Slave升级为新Master,客户端B又加锁成功,两个客户端同时持有锁。这是SET NX EX解决不了的。

Redlock方案:同时在多个独立Redis节点上加锁,超过半数成功才算加锁成功。但Redlock争议很大,Martin Kleppmann写过文章质疑,说依赖时间假设不安全。实际上,我项目里用的是Redisson,它实现了Redlock,但我也只在非关键场景用。真正高要求的场景,建议用Zookeeper。

总结:一般业务用Redisson就够了;涉及资金、库存的强一致场景,上Zookeeper。


【面试题】3. TCC分布式事务中,什么是"空回滚"和"悬挂"?怎么解决? ​

【标准答案】这两个都是TCC的典型问题,必须处理。

空回滚:Try阶段因网络超时失败,事务协调器触发Cancel,但业务上Try根本没执行,Cancel却执行了,这就是空回滚。比如扣库存,Try没执行,Cancel却把库存加回去了。

解决方案:在Try阶段执行前先写入一条事务日志记录,Cancel阶段检查是否有对应的Try记录,没有就直接返回,不执行回滚逻辑。

悬挂:Try请求网络拥堵,Cancel先执行完了,Try请求才到达,此时业务已经结束,Try又执行了一次,资源被占用没人释放,这就是悬挂。

解决方案:Cancel执行完后标记事务为"已回滚",Try执行前先检查状态,已回滚就直接返回。可以用数据库状态字段或Redis标记。

代码层面:我习惯用一张事务日志表,字段包括事务ID、状态(TRYING/CONFIRMED/CANCELED),每个阶段都检查状态,防止空回滚和悬挂。这是TCC落地的基本功。


【面试题】4. Snowflake算法时钟回拨怎么办?生产环境怎么设计? ​

【标准答案】时钟回拨是Snowflake最大的坑,处理不好会生成重复ID。

轻度回拨(几十毫秒):等时钟追上,期间阻塞生成。我的做法是,检测到回拨后,计算回拨时长,Sleep等待,最多等5毫秒,超过就报错。

中度回拨(几秒):使用备用workerId。比如配置两个workerId,一个主用,回拨时切换到备用,备用ID的时间戳比当前时间超前一点,可以平滑过渡。

严重回拨(分钟级):直接报错,走降级方案。我线上会兜底调用数据库号段模式生成ID,保证可用性。

生产设计建议:

  1. 关闭NTP同步或使用只向后调整的同步策略
  2. ID生成服务做高可用,多节点部署,时钟回拨只影响单节点
  3. 监控时钟偏移,超过阈值告警
  4. 准备降级方案,如Leaf号段模式、数据库自增

我们生产用的是改造版Snowflake + Leaf号段双保险,Snowflake正常走性能高,异常时降级到号段模式,稳得很。


【面试题】5. 缓存穿透、击穿、雪崩分别是什么?你的解决方案? ​

【标准答案】这三个都是缓存失效导致的数据库压力问题,但原因不同。

缓存穿透:查询不存在的数据,缓存没数据,数据库也没有,每次请求都打到数据库。比如恶意请求ID=-1的数据。

解决方案:

  • 布隆过滤器:把所有可能存在的key预加载到布隆过滤器,请求先过布隆过滤器,不存在直接返回。我用的Redis布隆过滤器,几个亿数据也就几MB。
  • 空值缓存:查不到也缓存,设短过期时间,比如60秒。

缓存击穿:某个热点key过期瞬间,大量并发请求直接打到数据库。

解决方案:

  • 热点数据永不过期,逻辑过期:物理不过期,后台线程定时刷新。
  • 互斥锁:缓存失效时,用分布式锁让一个线程去查库,其他线程等待。

缓存雪崩:大量key同时过期或Redis宕机,所有请求打到数据库。

解决方案:

  • 过期时间加随机值,避免同时过期。
  • Redis高可用:主从+哨兵或Cluster。
  • 多级缓存:本地缓存+分布式缓存,Redis挂了,本地缓存顶一阵。
  • 熔断降级:数据库压力太大时熔断,返回降级数据。

我线上架构是:布隆过滤器防穿透 + 热点数据逻辑过期防击穿 + 随机过期时间+多级缓存防雪崩。


【面试题】6. JWT无状态方案怎么实现Token刷新?单点登录时怎么踢出已登录用户? ​

【标准答案】JWT无状态是优点也是缺点,刷新和踢人都得特殊处理。

Token刷新方案:

  1. 双Token方案:Access Token短(15分钟)+ Refresh Token长(7天)。Access过期后用Refresh换新的Access,Refresh过期才需要重新登录。
  2. 滑动刷新:每次请求检查Token剩余有效期,低于阈值(比如剩5分钟)就生成新Token返回,前端拦截响应更新Token。

我们用的是双Token方案,Refresh Token存Redis,可以主动失效,兼顾无状态和可控性。

踢出已登录用户: 纯JWT做不到,因为JWT签发后无法主动失效。必须引入状态存储:

  • 方案一:黑名单机制。被踢用户的Token加入Redis黑名单,每次请求检查黑名单,命中就拒绝。黑名单TTL设为Token剩余有效期。
  • 方案二:版本号机制。用户表加token_version字段,签发JWT时把version写入payload,踢人时version+1,校验时比对version,不匹配就拒绝。

我用的版本号方案,踢人、强制重登都很方便,代价是每次请求都要查库,可以加缓存优化。


【面试题】7. 令牌桶和漏桶算法有什么区别?什么场景用哪个? ​

【标准答案】两个都是限流经典算法,但思路不同。

漏桶算法:请求像水流入桶,桶底以固定速率漏水。请求先入桶,桶满了就丢弃。特点是以固定速率流出,流量削峰填谷,输出平滑。

适用场景:需要严格控制处理速率的场景,比如调用第三方API,对方有频率限制,用漏桶保证不超过限速。

令牌桶算法:系统以固定速率往桶里放令牌,请求来了先拿令牌,拿到才能执行,拿不到就拒绝或等待。桶有容量上限,令牌满了就丢弃。特点是可以突发,桶里有N个令牌就能瞬间处理N个请求。

适用场景:允许突发流量但总体限流的场景,比如电商秒杀,瞬间流量大,但系统有处理上限,用令牌桶既能应对突发,又能保护系统。

实际选型:

  • 我API网关用令牌桶(如Sentinel、Guava RateLimiter),允许正常突发,压垮系统。
  • 调用外部支付接口用漏桶,严格遵守对方QPS限制,避免被封。

核心区别一句话:漏桶强行平滑,令牌桶允许突发。


【面试题】8. Raft算法Leader选举过程?什么情况下会触发重新选举? ​

【标准答案】Raft选举是典型的多数派决策,很经典。

正常选举流程:

  1. 所有节点初始都是Follower状态,有个选举超时计时器(150-300ms随机)。
  2. Follower在超时前没收到Leader心跳,转为Candidate,投自己一票,向其他节点发起RequestVote。
  3. 其他节点收到投票请求,检查:term是否更新、日志是否至少和自己一样新,都满足就投票。
  4. Candidate收到多数派投票后,成为Leader,立即发送心跳给所有Follower确认地位。
  5. Leader之后定期发送心跳,Follower收到心跳重置选举超时。

重新选举触发条件:

  1. Leader故障:Leader挂了或网络分区,Follower收不到心跳,超时后触发。
  2. Candidate选举超时:Candidate发起投票后,没收到多数派响应(可能票被瓜分),超时后自增term重新选举。
  3. 发现更高term:节点收到更高term的消息,知道自己落后,主动转为Follower参与新选举。
  4. 日志落后被拒:Candidate日志不够新,拿不到多数票,等下次超时重试。

我看过Raft论文的Figure 2那张状态机图,建议面试前背下来,画出来面试官眼前一亮。


【面试题】9. Zookeeper临时节点有什么用?分布式锁实现为什么要用临时节点而不是持久节点? ​

【标准答案】临时节点是Zookeeper的核心特性,生命周期和客户端会话绑定。

临时节点特点:

  1. 会话结束,节点自动删除。无论客户端主动断开还是故障断开,节点都会消失。
  2. 不能有子节点。只能作为叶子节点。

典型应用场景:

  • 分布式锁:锁节点用临时节点,客户端挂了锁自动释放,避免死锁。
  • 服务注册:服务实例注册为临时节点,实例下线节点自动删除,注册中心自动感知。
  • Leader选举:多个候选者创建临时节点,创建成功的为Leader,其他监听节点删除事件。

为什么锁要用临时节点: 如果用持久节点,客户端加锁后挂了,锁节点不会删除,其他客户端永远拿不到锁,这就是死锁。用临时节点,客户端断开会话,锁节点自动删除,锁释放,其他客户端可以继续抢占。

实际开发:我直接用Curator的InterProcessMutex,它内部就是临时顺序节点+监听前一节点,可重入、公平锁都支持,不用自己造轮子。


【面试题】10. 分布式链路追踪中,异步调用怎么保证TraceId传递?线程池场景怎么处理? ​

【标准答案】异步调用链路断裂是链路追踪的常见坑,必须处理。

问题根源:TraceId存在ThreadLocal里,线程切换就丢了。异步线程、线程池都是新线程,拿不到主线程的TraceId。

解决方案一:手动传递 在创建异步任务时,主线程先获取当前TraceId,传给异步线程,异步线程在执行前手动set到自己的ThreadLocal。

java
String traceId = MDC.get("traceId");
executor.execute(() -> {
    MDC.put("traceId", traceId);
    try {
        // 业务逻辑
    } finally {
        MDC.remove("traceId");
    }
});

解决方案二:线程池包装 使用装饰过的线程池,自动传递上下文。比如阿里的TransmittableThreadLocal(TTL),或者Spring Cloud Sleuth的TraceRunnable包装。

java
Executor wrappedExecutor = TtlExecutors.getTtlExecutorService(executorService);
wrappedExecutor.execute(() -> {
    // TraceId自动传递,不用手动处理
});

解决方案三:消息队列传递 如果是MQ异步,把TraceId塞到消息Header里,消费端取出Header,set到ThreadLocal继续传递。

生产建议:用TTL或Sleuth的线程池包装,一劳永逸,别每个地方手动传,容易漏。我们线上全部线程池都用TTL包装,TraceId、SpanId、UserId这些上下文全链路透传,排查问题效率提升10倍。


本轮题库生成完毕,共10题。