Skip to content

本轮 Java分布式架构面试题库(带答案) ​

生成时间:2026-06-19 23:17


【面试题】1. CAP定理说分布式系统C、A、P只能三选二,但实际项目中我们好像三个都有?怎么理解这个矛盾? ​

【标准答案】CAP定理里的"三选二"指的是发生网络分区(P)时,你必须在C和A之间做取舍,不是说平时只能选两个。正常运行没有分区时,C和A可以同时满足。一旦出现网络分区——比如两个机房之间断网了——你要么选C:拒绝写入、保证数据一致;要么选A:允许写入、但两边数据可能不一致、等网络恢复后再同步。实际项目里P是必须接受的(网络不可靠),所以本质是CP还是AP的选择。比如ZooKeeper选CP:Leader挂了整个集群暂时不可用;Eureka选AP:节点挂了其他节点照样注册,数据可能短暂不一致。没有"三个都要"的方案,只是分区没发生的时候看起来三个都有。

【面试题】2. Redis分布式锁用SET NX EX就能实现,为什么还要用Redisson?它解决了什么问题? ​

【标准答案】原生命令SET lockKey value NX EX 30只能解决"加锁"这一个动作,但生产环境有四个坑它填不了:第一,锁续期——业务执行超过30秒,锁自动释放了,其他线程进来造成并发问题。Redisson的WatchDog机制会每隔10秒自动续期到30秒,业务完成再主动释放。第二,可重入——同一个线程多次获取同一把锁,原生命令会死锁,Redisson用hash结构+计数器支持重入。第三,解锁误删——线程A的锁过期了,线程B加锁成功,然后线程A去解锁把B的锁删了。Redisson解引用value校验(UUID+线程ID),不是自己的锁不解。第四,主从切换丢锁——主节点挂了,从节点还没同步锁数据就升主,两个客户端同时拿到锁。这个Redisson的RedLock通过向多个独立Redis实例依次加锁来解决,但性能开销大,一般业务用单实例+Redisson就够。

【面试题】3. TCC和Saga都是分布式事务方案,各有什么优缺点?实际怎么选? ​

【标准答案】TCC是两阶段、强一致性方案。Try阶段预留资源(比如冻结库存),Confirm阶段确认提交(扣减库存),Cancel阶段回滚释放(解冻库存)。优点是实时一致性高、适合金融扣款场景;缺点是实现复杂,每个接口都要写三套逻辑,业务侵入性强,且Cancel失败需要人工介入。Saga是长事务、最终一致性方案,有两种实现:编排式(每个服务调下一个,类似责任链)和控制式(一个协调器统一调度)。每个步骤有对应的补偿操作,正向失败就逆序补偿。优点是异步解耦、适合长流程(如订单超过30分钟取消);缺点是数据中间态可见(用户看到订单状态反复变)、补偿也可能失败需要重试或告警。选型经验:资金类用TCC,流程类用Saga。下单扣库存这种关键资源用TCC;创建订单→发优惠券→发短信这种非关键链路用Saga。

【面试题】4. Snowflake雪花算法生成的ID全局唯一,但如果服务器时钟回拨了怎么办? ​

【标准答案】时钟回拨是Snowflake的致命问题——如果机器时间往回走了,可能生成重复ID。常见解决方案有五种:第一,抛异常拒绝——检测到回拨直接报错,等时钟追上再服务,简单粗暴但影响可用性。第二,等待追上——回拨时间较短(比如2秒内),当前线程睡一会儿再取时间,美团的Leaf做了这个。第三,借用未来时间——记录上次生成ID的时间戳,回拨后沿用该时间戳,但调整序列号,等于"透支"未来时间。第四,扩展位补偿——预留2-bit给回拨计数,每回拨一次计数器+1,这样即使时间倒退,ID也不重复,百度UidGenerator就用这个思路。第五,换workId——发现回拨后自动更换workerId,避免冲突。生产上最实用的组合:先等(短回拨),等不了就抛异常+告警切到备用ID方案(如号段模式),最终依赖运维保证NTP不突然跳变。

【面试题】5. 缓存穿透、缓存击穿、缓存雪崩,三个词只差一个字,实际场景怎么区分和解决? ​

【标准答案】三个问题的本质区别在于"打穿的是什么"。缓存穿透:查一个数据库也不存在的数据(比如id=-1),缓存里没有、库里也没有,每次请求都穿透到数据库。解决:布隆过滤器提前拦截不存在的key,或者把空结果也缓存起来(设短过期时间)。缓存击穿:一个热点key刚好过期了,瞬间大量并发请求同时打到数据库。关键词是"热点+过期"。解决:互斥锁——第一个请求去加载数据加锁,其他请求等待;或者"永不过期"——逻辑过期由异步线程刷新。缓存雪崩:大量key在同一时刻集体过期,或者Redis集群挂了,请求全部压到数据库。解决:过期时间加随机值(避免同时过期)、多级缓存(本地Caffeine+Redis)、限流降级熔断保护数据库。记忆口诀:穿透查不到、击穿一个点、雪崩一大片。

【面试题】6. Raft算法怎么选出Leader?如果老Leader还活着新Leader被选出来,会发生什么? ​

【标准答案】Raft的Leader选举依赖两个核心机制:随机选举超时和Term任期号。过程是:所有节点初始是Follower,每个节点维护一个随机超时时间(150-300ms),超时后变成Candidate,Term+1,先给自己投一票,然后向其他节点发RequestVote。其他节点在同一Term内"先到先得"投票,Candidate拿到超过半数票就升级为Leader,立即发心跳阻止其他节点超时。如果两个Candidate同时发起选举、都拿不到半数,各自随机等待后重新选举。

老Leader还活着的情况:新Leader当选后Term更高,老Leader发心跳时Term落后,Follower直接拒绝并告知最新Term。老Leader发现自己的Term过时了,自动降级为Follower。这是Raft安全性保证的核心——Term单调递增,高Term永远优先。如果老Leader在不知道被取代的情况下继续处理写请求,它的日志条目会因为Term落后而无法提交,最终被新Leader的日志覆盖,不会造成数据不一致。

【面试题】7. 一个接口QPS暴涨到1万,怎么用Redis+Lua实现分布式限流?令牌桶和漏桶哪个更合适? ​

【标准答案】Redis+Lua限流的核心思想是原子化判断——限流逻辑放在一个Lua脚本里执行,避免"读-判断-写"的竞态条件。以令牌桶为例:Lua脚本先获取当前时间和上次填充时间,计算应生成的令牌数并更新桶中令牌(不超过容量),然后判断当前请求能否拿到令牌,能则令牌数-1返回true,否则返回false。整个过程在Redis单线程模型下天然原子。

令牌桶vs漏桶:令牌桶允许短时突发流量(桶里有攒的令牌就能瞬间消费),适合秒杀、抢购这种有波峰的场景;漏桶流速恒定,强行平滑请求,适合保护下游第三方API(他们有固定调用频率限制)。突发流量用令牌桶,均匀保护用漏桶。生产上常用Guava RateLimiter做单机限流+Redis Lua做全局限流两级联动:本地先挡掉99%,剩下的走Redis。

【面试题】8. Zookeeper实现分布式锁,为什么用临时顺序节点而不是直接用临时节点? ​

【标准答案】如果只用一个临时节点,所有竞争者watch同一个节点,锁释放时所有等待者同时被唤醒——这就是惊群效应。100个线程等锁,锁一释放100个都去抢,99个抢失败又继续等,大量无效竞争和ZK压力。临时顺序节点(EPHEMERAL_SEQUENTIAL)解决这个问题:每个竞争者创建一个带序号的后缀节点,比如/lock/request-0001、/lock/request-0002,序号最小的持有锁。每个节点只watch自己前一个节点,锁释放(节点删除)时只有后一个节点被唤醒。这样把"竞态通知"变成了链式唤醒,锁的获取变成公平排队——先申请的先获取,天然是公平锁。同时利用临时节点的特性:客户端断开连接节点自动删除,不会死锁。

【面试题】9. 分布式ID生成方案里,美团的Leaf号段模式和Snowflake各自适合什么场景?Leaf怎么保证高可用? ​

【标准答案】Leaf号段模式的核心是批量取号、本地发放:从DB一次取一段号(如1-1000),缓存在本地内存,用完再取下一段。优点是发号极快(纯内存操作)、ID严格递增、对DB压力小。适合对ID趋势递增有强需求的场景,比如MySQL主键(有序ID对B+树友好)。Snowflake是本地计算生成,不依赖DB,但ID趋势递增(依赖时间)、且有时钟回拨风险。适合分布式无状态服务、不需要严格递增的场景。

Leaf高可用三招:第一,双Buffer——当前号段用到10%时异步预加载下一段,避免"号用完了才去DB取"的卡顿。第二,多DB实例——两个号段服务连接不同DB,号段错开分配(如A取1/3/5段,B取2/4/6段),一个DB挂了另一个继续服务。第三,号段监控——接近上限自动告警扩容,不会"号用光了才发现"。整体思路:不在发号的临界路径上依赖DB。

【面试题】10. 分布式链路追踪里,TraceId在跨线程、跨MQ消息的场景下怎么保证不丢失? ​

【标准答案】链路追踪的核心是上下文传递。同步调用天然简单——TraceId放HTTP Header(如X-B3-TraceId)或RPC元数据,框架自动透传。难点在异步场景:跨线程——线程池里开了个CompletableFuture,切线程后ThreadLocal丢了。解决:装饰线程池,把当前Span上下文在提交任务时捕获,任务执行时恢复。比如Spring Cloud Sleuth用LazyTraceExecutor包装线程池,内部把TraceContext从当前线程拷贝到工作线程。跨MQ——消息发送时把TraceId塞进消息头,消费端从消息头取出TraceId并创建新的Span,关联parentSpanId。RocketMQ、Kafka都支持消息头,配合@KafkaListener或MessageListener做拦截器提取即可。关键原则:产生Span(生产者)时注入,消费Span(消费者)时提取。不要依赖隐式传递,显式编码最可控。


本轮覆盖考点:CAP理论、Redis锁/Redisson、TCC vs Saga、Snowflake时钟回拨、缓存三兄弟、Raft选举、Redis+Lua限流、ZK锁实现、Leaf号段、链路追踪Context传递