Skip to content

Java分布式架构面试题库 ​

生成时间:2026-05-22 00:15 | 题目数量:10


【面试题】1. CAP定理在真实分布式系统中到底怎么选?能结合实际场景说说吗? ​

【标准答案】CAP定理说的是分布式系统在一致性(C)、可用性(A)、分区容错性(P)三者中只能同时满足两个。但实际中P是必选的——网络分区是客观存在的,没法避免,所以本质上就是CP和AP二选一。选CP就是分区时牺牲可用性保一致性,典型像Zookeeper,Leader选举期间整个集群不可用;选AP就是分区时牺牲一致性保可用性,典型像Eureka,节点间数据可能不一致但服务注册发现照常工作。面试中要强调一点:CAP不是非黑即白的,大部分系统在不同操作上可以有不同的选择。比如同一系统,订单创建走CP,商品浏览走AP。还有一点,CAP说的是网络分区发生时的选择,没分区的时候CA是可以同时满足的,别搞混了。

【面试题】2. Redis分布式锁用SET NX EX和用Redisson有什么本质区别?生产环境选哪个? ​

【标准答案】SET NX EX是最基础的实现,能加锁能过期,但问题很明显:第一,锁续期做不到——业务没执行完锁就过期了,别的线程就拿到了;第二,释放锁可能误删别人的锁——线程A的锁过期了,线程B拿到了,线程A执行完一删把B的锁删了;第三,不可重入,同一线程再次获取同一把锁会失败。Redisson把这些全解决了:看门狗机制自动续期(默认30秒,每10秒续一次),加锁时带唯一标识释放时校验,支持可重入锁用hash结构记录重入次数。生产环境必须用Redisson,SET NX EX只适合对可靠性要求不高的简单场景。如果对一致性要求极高,还要考虑Redlock方案,在多个独立Redis节点上同时加锁,超过半数成功才算获取锁,防止单节点故障导致锁失效。

【面试题】3. TCC模式和Saga模式在分布式事务中怎么选?各自适合什么业务场景? ​

【标准答案】TCC是强一致性的二阶段业务补偿模式,Try阶段冻结资源不提交,Confirm阶段确认提交,Cancel阶段回滚释放。优点是一致性强、锁粒度可控;缺点是业务侵入性大,每个操作都要写三个方法,而且要自己保证幂等和空回滚处理。Saga是长事务编排模式,把大事务拆成多个本地事务,每个本地事务完成后发消息触发下一个,失败时按反序执行补偿操作。优点是业务侵入小,适合长流程;缺点是缺乏隔离性,中间状态对外可见,补偿操作也可能失败。选型原则:短事务、对一致性要求高、能接受业务改造——选TCC;长流程、业务步骤多、对实时一致性要求不高——选Saga。实际项目中Seata框架同时支持两种,TCC模式适合金融转账、库存扣减这种关键操作,Saga模式适合订单履约、跨系统业务编排这种长链路。

【面试题】4. Snowflake雪花算法的ID是怎么组成的?时钟回拨问题怎么解决? ​

【标准答案】Snowflake生成64位Long型ID,从高位到低位:1位符号位(永远0) + 41位时间戳(毫秒级,约69年) + 10位工作机器ID(可分5位数据中心+5位机器) + 12位序列号(同一毫秒内最多4096个ID)。核心思想是用时间戳保趋势递增,用机器ID保分布式唯一,用序列号保单机并发。时钟回拨是最致命的问题——NTP同步导致系统时间往回跳,可能生成重复ID。解决方案有几层:第一,轻度回拨(比如几毫秒),等待回拨时间过去再生成;第二,中度回拨,用历史最大时间戳替代系统时间,差值存下来补偿;第三,严重回拨,直接拒绝服务或报警切换到备用ID生成器。百度UidGenerator用了RingBuffer预生成+双Buffer机制,美团的Leaf-snowflake用了Zookeeper来协调workerId并在检测到回拨时抛异常。生产中建议回拨超过阈值直接报错,别硬扛。

【面试题】5. 缓存穿透、缓存击穿、缓存雪崩分别是什么?线上怎么防? ​

【标准答案】三者都是缓存失效导致请求打到数据库,但原因不同。穿透是查询根本不存在的数据,缓存没命中DB也没命中,每次都穿透;击穿是热点key突然过期,大量并发同时打到DB;雪崩是大面积key同时过期,DB瞬间承压。防穿透:布隆过滤器前置拦截(有误判率但够用)+ 空值缓存设短过期时间(防频繁查同一个不存在的key)。防击穿:热点key永不过期(逻辑过期,后台线程异步更新)+ 互斥锁(只放一个请求去查DB回填缓存,其他等)。防雪崩:过期时间加随机偏移量(打散过期时间)+ 多级缓存(本地缓存兜一层)+ 熔断降级(DB扛不住时快速失败)。线上最常见的是雪崩,一般是批量导入数据时用了统一过期时间,这点要特别注意。

【面试题】6. JWT无状态方案和Session集中存储方案怎么选?各有什么坑? ​

【标准答案】JWT是无状态的,Token自包含用户信息,服务端不存储状态,天然支持水平扩展。但坑也多:第一,Token一旦签发无法主动失效,只能等过期,所以要做Token黑名单或短Token+RefreshToken双Token方案;第二,Payload不能放敏感信息,因为Base64不是加密;第三,Token体积比SessionID大很多,每次请求都带上,带宽开销大;第四,续期麻烦,要么每次请求都刷新Token,要么用RefreshToken单独换。Session集中存储是把Session放到Redis里,应用无状态化,SessionID通过Cookie传递。优点是服务端完全可控,踢人、续期、失效都简单;缺点是Redis是单点依赖,Redis挂了整个认证体系瘫痪。选型建议:微服务内部、对安全性要求高的系统用Session+Redis;对外开放API、跨域场景、移动端用JWT。很多团队现在用混合方案:JWT做服务间认证,Redis做用户在线状态管理,两者结合。

【面试题】7. 令牌桶和漏桶限流算法有什么区别?分布式场景下怎么实现? ​

【标准答案】漏桶是恒定速率流出,不管请求多汹涌,处理速率是固定的,适合对下游保护严格、要求平滑流量的场景。令牌桶是恒定速率往桶里放令牌,请求拿到令牌才能通过,允许一定程度的突发流量——桶里有积累的令牌时可以瞬间处理一批请求。核心区别:漏桶不允许突发,令牌桶允许受控突发。大部分互联网场景选令牌桶更合适,因为真实流量就是突发的。分布式实现关键在于令牌的发放和计数要原子性,主流方案是Redis+Lua脚本:Lua脚本里原子性地读取当前令牌数、计算补充量、判断是否足够、扣减令牌,整个过程在Redis单线程里执行不会有并发问题。Sentinel的分布式限流也是类似思路,集群限流模式下有一个Token Server统一分配令牌,各节点向Server请求令牌,保证了全局限流的准确性。

【面试题】8. Raft算法的Leader选举过程是怎样的?如果出现网络分区会怎样? ​

【标准答案】Raft的Leader选举分几步:所有节点初始都是Follower状态,有一个随机化的选举超时时间(150ms~300ms)。Follower在超时前没收到Leader的心跳,就转为Candidate,自增当前任期,先给自己投一票,然后向其他节点发RequestVote RPC。其他节点的投票条件:候选人的term比自己大、自己还没在当前term投过票、候选人的日志至少和自己一样新。Candidate收到多数票就成为Leader,开始发心跳维持权威。网络分区时:如果Leader在少数派分区,它不知道自己已被隔离,还在发心跳但到不了多数派;多数派分区会选出新Leader。旧Leader收到更高term的心跳后自动降级为Follower。关键保证:任何term最多一个Leader,获得多数票意味着和之前Leader的日志有交集,不会丢失已提交的日志。Raft比Paxos好理解就在这——把共识问题拆成了Leader选举、日志复制、安全性三个子问题。

【面试题】9. Zookeeper的Watch机制有什么特性?为什么说它是轻量级通知? ​

【标准答案】Zookeeper的Watch是一次性触发器——数据变更触发通知后Watch就被消费掉了,客户端必须重新注册才能继续监听。这是设计上的刻意选择:如果Watch是持久性的,频繁变更的节点会产生大量通知事件,服务端和网络的负担会很大。一次性Watch保证了通知量可控。Watch的另一个特性是轻量级——通知事件只告诉客户端"数据变了",不携带变更后的数据,客户端需要主动再去读。这样设计是因为如果携带数据,数据量不可控,可能一个Watch通知就几MB。使用上要注意几个坑:第一,Watch注册和数据读取不是原子操作,可能注册Watch后、还没读到最新数据,中间就有变更,要用exists+getData组合或者Curator的Cache机制;第二,一次性Watch在重注册间隙可能丢事件,Curator的PathChildrenCache/TreeCache用后台线程持续监听+本地缓存解决了这个问题;第三,Watch是顺序的,保证事件不会乱序,但重注册时要处理版本号。

【面试题】10. 分布式链路追踪中TraceId是怎么跨服务传递的?异步调用场景有什么特殊处理? ​

【标准答案】TraceId跨服务传递的核心是在RPC调用的协议层做透传。HTTP调用一般放在请求头里,常用的key是trace-id或X-B3-TraceId(Spring Cloud Sleuth用B3协议),下游服务从请求头取出TraceId放到当前线程的上下文(MDC或ThreadLocal)里。gRPC调用放在Metadata里,Dubbo放在RpcContext的Attachment里。关键是全链路所有组件都要遵循同一套传播协议。异步调用是链路追踪的难点:主线程提交任务到线程池时,ThreadLocal里的TraceId不会自动传递过去,所以异步线程拿不到TraceId,链路就断了。解决方案:手动传递——提交任务时把TraceId作为参数传进去,异步线程里再set到上下文;框架支持——Spring Cloud Sleuth对@Async做了自动包装,RocketMQ的消息头自动携带TraceId,Dubbo的RpcContext在异步回调时也会传递。线程池场景建议用装饰器模式包装Runnable/Callable,自动注入TraceId。另外CompletableFuture的链式调用也要注意,thenApply等回调可能在不同线程执行,需要用Micrometer Tracing的context-propagation库自动传播上下文。