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,保证可用性。
生产设计建议:
- 关闭NTP同步或使用只向后调整的同步策略
- ID生成服务做高可用,多节点部署,时钟回拨只影响单节点
- 监控时钟偏移,超过阈值告警
- 准备降级方案,如Leaf号段模式、数据库自增
我们生产用的是改造版Snowflake + Leaf号段双保险,Snowflake正常走性能高,异常时降级到号段模式,稳得很。
【面试题】5. 缓存穿透、击穿、雪崩分别是什么?你的解决方案?
【标准答案】这三个都是缓存失效导致的数据库压力问题,但原因不同。
缓存穿透:查询不存在的数据,缓存没数据,数据库也没有,每次请求都打到数据库。比如恶意请求ID=-1的数据。
解决方案:
- 布隆过滤器:把所有可能存在的key预加载到布隆过滤器,请求先过布隆过滤器,不存在直接返回。我用的Redis布隆过滤器,几个亿数据也就几MB。
- 空值缓存:查不到也缓存,设短过期时间,比如60秒。
缓存击穿:某个热点key过期瞬间,大量并发请求直接打到数据库。
解决方案:
- 热点数据永不过期,逻辑过期:物理不过期,后台线程定时刷新。
- 互斥锁:缓存失效时,用分布式锁让一个线程去查库,其他线程等待。
缓存雪崩:大量key同时过期或Redis宕机,所有请求打到数据库。
解决方案:
- 过期时间加随机值,避免同时过期。
- Redis高可用:主从+哨兵或Cluster。
- 多级缓存:本地缓存+分布式缓存,Redis挂了,本地缓存顶一阵。
- 熔断降级:数据库压力太大时熔断,返回降级数据。
我线上架构是:布隆过滤器防穿透 + 热点数据逻辑过期防击穿 + 随机过期时间+多级缓存防雪崩。
【面试题】6. JWT无状态方案怎么实现Token刷新?单点登录时怎么踢出已登录用户?
【标准答案】JWT无状态是优点也是缺点,刷新和踢人都得特殊处理。
Token刷新方案:
- 双Token方案:Access Token短(15分钟)+ Refresh Token长(7天)。Access过期后用Refresh换新的Access,Refresh过期才需要重新登录。
- 滑动刷新:每次请求检查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选举是典型的多数派决策,很经典。
正常选举流程:
- 所有节点初始都是Follower状态,有个选举超时计时器(150-300ms随机)。
- Follower在超时前没收到Leader心跳,转为Candidate,投自己一票,向其他节点发起RequestVote。
- 其他节点收到投票请求,检查:term是否更新、日志是否至少和自己一样新,都满足就投票。
- Candidate收到多数派投票后,成为Leader,立即发送心跳给所有Follower确认地位。
- Leader之后定期发送心跳,Follower收到心跳重置选举超时。
重新选举触发条件:
- Leader故障:Leader挂了或网络分区,Follower收不到心跳,超时后触发。
- Candidate选举超时:Candidate发起投票后,没收到多数派响应(可能票被瓜分),超时后自增term重新选举。
- 发现更高term:节点收到更高term的消息,知道自己落后,主动转为Follower参与新选举。
- 日志落后被拒:Candidate日志不够新,拿不到多数票,等下次超时重试。
我看过Raft论文的Figure 2那张状态机图,建议面试前背下来,画出来面试官眼前一亮。
【面试题】9. Zookeeper临时节点有什么用?分布式锁实现为什么要用临时节点而不是持久节点?
【标准答案】临时节点是Zookeeper的核心特性,生命周期和客户端会话绑定。
临时节点特点:
- 会话结束,节点自动删除。无论客户端主动断开还是故障断开,节点都会消失。
- 不能有子节点。只能作为叶子节点。
典型应用场景:
- 分布式锁:锁节点用临时节点,客户端挂了锁自动释放,避免死锁。
- 服务注册:服务实例注册为临时节点,实例下线节点自动删除,注册中心自动感知。
- Leader选举:多个候选者创建临时节点,创建成功的为Leader,其他监听节点删除事件。
为什么锁要用临时节点: 如果用持久节点,客户端加锁后挂了,锁节点不会删除,其他客户端永远拿不到锁,这就是死锁。用临时节点,客户端断开会话,锁节点自动删除,锁释放,其他客户端可以继续抢占。
实际开发:我直接用Curator的InterProcessMutex,它内部就是临时顺序节点+监听前一节点,可重入、公平锁都支持,不用自己造轮子。
【面试题】10. 分布式链路追踪中,异步调用怎么保证TraceId传递?线程池场景怎么处理?
【标准答案】异步调用链路断裂是链路追踪的常见坑,必须处理。
问题根源:TraceId存在ThreadLocal里,线程切换就丢了。异步线程、线程池都是新线程,拿不到主线程的TraceId。
解决方案一:手动传递 在创建异步任务时,主线程先获取当前TraceId,传给异步线程,异步线程在执行前手动set到自己的ThreadLocal。
String traceId = MDC.get("traceId");
executor.execute(() -> {
MDC.put("traceId", traceId);
try {
// 业务逻辑
} finally {
MDC.remove("traceId");
}
});解决方案二:线程池包装 使用装饰过的线程池,自动传递上下文。比如阿里的TransmittableThreadLocal(TTL),或者Spring Cloud Sleuth的TraceRunnable包装。
Executor wrappedExecutor = TtlExecutors.getTtlExecutorService(executorService);
wrappedExecutor.execute(() -> {
// TraceId自动传递,不用手动处理
});解决方案三:消息队列传递 如果是MQ异步,把TraceId塞到消息Header里,消费端取出Header,set到ThreadLocal继续传递。
生产建议:用TTL或Sleuth的线程池包装,一劳永逸,别每个地方手动传,容易漏。我们线上全部线程池都用TTL包装,TraceId、SpanId、UserId这些上下文全链路透传,排查问题效率提升10倍。
本轮题库生成完毕,共10题。