Skip to content

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

生成时间:2026-05-21 23:55


【面试题】1. 请详细解释Raft算法中Leader选举的完整流程,包括什么情况下会触发选举、选举超时如何设计、以及如何避免split brain? ​

【标准答案】Raft的Leader选举核心流程:每个Follower启动时都有一个随机的选举超时时间(election timeout),通常150ms~300ms之间随机取值。如果在这个时间内没有收到Leader的心跳,Follower就认为Leader挂了,把自己转为Candidate,term加1,先给自己投一票,然后向其他节点发RequestVote RPC。

选举成功的条件是拿到多数派(majority)节点的投票。一个节点在同一term内只能投一票,先到先得,这就保证了每个term最多只有一个Leader被选出,避免split brain。

如果选举超时内没有拿到多数票,就自增term重新发起选举。随机选举超时的设计很关键——它让各节点错开发起选举的时间,避免多个节点同时竞选导致选票分散、一直选不出Leader的情况。

实际工程中,选举超时一般设为心跳间隔的10~20倍。比如心跳50ms一次,选举超时就500ms~1000ms。太短会频繁触发不必要的选举,太长则故障检测慢,影响可用性。


【面试题】2. TCC模式中,如果Confirm阶段失败了,应该怎么处理?和Try阶段失败的处理有什么本质区别? ​

【标准答案】TCC中Try阶段失败和Confirm阶段失败的处理逻辑完全不同,这是面试的高频踩坑点。

Try阶段失败:没问题,直接执行Cancel就行。因为Try只是资源预留,还没实际执行业务,Cancel就是释放预留资源,逻辑干净。

Confirm阶段失败:这才是TCC最棘手的问题。因为Confirm是真正执行业务逻辑的阶段,资源已经扣了、钱已经转了,不能简单回滚。核心原则是——Confirm必须重试直到成功,不允许回滚。具体做法:

  1. 事务协调器记录Confirm执行状态,失败后持续重试
  2. Confirm操作必须保证幂等性,重试不会产生副作用
  3. 如果重试多次仍失败,需要人工介入或告警处理,但不能自动Cancel

为什么不能Cancel?因为Confirm可能部分执行成功了,比如A账户已扣款但B账户还没加款,这时候Cancel会把A的扣款恢复,但B那边没执行你也不知道该恢复什么,数据就不一致了。

所以TCC的设计哲学是:Try阶段允许回滚,Confirm/Cancel阶段必须向前重试到成功。这也是为什么TCC对业务侵入性大——每个操作都要写Try/Confirm/Cancel三个方法,而且Confirm和Cancel都要保证幂等。


【面试题】3. Redis Cluster模式下,如果某个主节点宕机,它的从节点晋升为新主节点的完整流程是什么?期间集群是否可用? ​

【标准答案】Redis Cluster主节点宕机后的故障转移流程:

  1. 故障检测:集群中其他节点通过Gossip协议互相发送PING/PONG。如果某节点在cluster-node-timeout(默认15秒)内没有收到目标主节点的PONG响应,就把它标记为PFAIL(疑似下线)
  2. 确认下线:当集群中多数主节点都把该节点标记为PFAIL,就升级为FAIL(真正下线),这个判定由最先发现的节点广播给全集群
  3. 从节点选举:该主节点的所有从节点检测到主节点FAIL后,开始竞选。每个从节点有一个偏移量(replication offset),offset最大(即数据最新)的从节点优先成为候选人
  4. 投票:候选人向其他主节点请求投票,获得多数主节点同意后晋升为新主节点
  5. 广播:新主节点通过PONG消息通知全集群自己已晋升

期间集群是否可用? 要分情况:

  • 宕机主节点负责的16384个slot对应的请求会报错CLUSTERDOWN,这部分数据不可读写
  • 其他主节点负责的slot不受影响,正常服务
  • 整个过程大概需要cluster-node-timeout + 选举时间,通常15~30秒

如果该主节点没有从节点,那对应的slot就彻底不可用了,直到手动修复或集群管理员处理。


【面试题】4. Snowflake雪花算法的时钟回拨问题如何解决?市面上有哪些改进方案? ​

【标准答案】Snowflake的核心依赖是单调递增的时间戳,时钟回拨会导致ID重复或乱序,这是它最大的隐患。

常见解决方案:

  1. 回拨拒绝:检测到时钟回拨直接抛异常,拒绝生成ID。简单粗暴但影响可用性。美团Leaf的号段模式就用了这种策略——回拨超过阈值(如5ms)就抛异常,短时间回拨就等待追上。

  2. 回拨等待:短时间回拨(如几毫秒),线程sleep等到时钟追回来再生成。适合NTP微小校准的场景。

  3. 备用workerId:给每个实例分配多个workerId,回拨时切换到备用的workerId。因为时间戳+workerId的组合变了,即使时间戳重复也不会ID冲突。百度的UidGenerator就用了这种思路。

  4. 历史时间戳缓存:缓存最近一段时间生成的最大时间戳,每次生成前对比,如果当前时间小于缓存时间就用缓存时间。Leaf-segment的改进版用了这个。

  5. 完全换方案:如果时钟回拨是高频问题,可以直接用号段模式(如Leaf-segment),完全不依赖时钟,用数据库号段发号,彻底规避时钟问题。

我的建议:生产环境优先选Leaf的号段模式+Snowflake双方案。号段模式做主,Snowflake做降级。号段模式不依赖时钟,高可用又不会重复;Snowflake作为降级方案在数据库不可用时启用,两者互补。


【面试题】5. 请对比分析Paxos、Raft和ZAB三种一致性算法的核心区别,以及各自适用场景? ​

【标准答案】三者对比,面试要抓住核心差异:

Paxos:理论鼻祖,证明了一致性是可行的,但工程实现极其困难。Basic Paxos需要多轮通信,效率低;Multi-Paxos没有规定具体实现细节,各家公司自己摸索(Google Chubby是经典实现)。特点是能容忍少于半数的节点故障,但理解和实现门槛极高。适用场景:理论研究和少数大厂自研系统。

Raft:Paxos的工程化简化版。核心改进是强Leader模型——所有写请求必须经过Leader,日志复制由Leader单向推给Follower。这比Paxos的多Proposer竞争简单太多。Leader选举、日志复制、安全性三个模块拆开,每个都可独立理解和验证。etcd、Consul用的就是Raft。适用场景:需要强一致性的中小规模集群(3~7节点),如配置中心、服务发现。

ZAB:专门为Zookeeper设计的协议,和Raft很像但有两个关键不同:①支持两种模式——崩溃恢复模式(选Leader)和消息广播模式(正常写操作),模式切换比Raft更明确;②新Leader选举后必须先与过半Follower同步数据,确保已提交的提案不丢失,然后才真正成为Leader对外服务。这个"先同步再服务"的设计比Raft更保守但更安全。适用场景:Zookeeper生态,分布式协调场景。

一句话总结:Paxos是理论证明,Raft是工程简化,ZAB是为ZK定制的Raft变种。新项目选Raft,ZK生态用ZAB,Paxos留给论文。


【面试题】6. 在高并发场景下,如何设计一个基于Redis+Lua的分布式限流方案?请说明令牌桶算法的完整实现思路? ​

【标准答案】基于Redis+Lua的令牌桶限流,核心是利用Lua脚本保证原子性,避免多客户端竞争导致超限。

令牌桶核心参数:桶容量(burst)、填充速率(rate,每秒加几个令牌)、当前令牌数(tokens)、上次填充时间(last_time)。

Lua脚本逻辑:

  1. 获取当前时间,计算与last_time的时间差
  2. 根据时间差 × rate计算应补充的令牌数,加到tokens上,不超过burst
  3. 更新last_time为当前时间
  4. 判断tokens是否>=1,如果够就扣1个令牌返回允许;不够就拒绝
  5. 整个脚本用Redis的EVAL执行,保证原子性

关键代码思路:

-- KEYS[1] = 限流key
-- ARGV[1] = rate(每秒填充速率)
-- ARGV[2] = burst(桶容量)
-- ARGV[3] = 当前时间戳(毫秒)

local rate = tonumber(ARGV[1])
local burst = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local info = redis.call('HMGET', KEYS[1], 'tokens', 'last_time')
local tokens = tonumber(info[1]) or burst
local last_time = tonumber(info[2]) or now

-- 计算补充的令牌
local delta = math.max(0, now - last_time) / 1000
tokens = math.min(burst, tokens + delta * rate)

-- 判断是否放行
if tokens >= 1 then
    tokens = tokens - 1
    redis.call('HMSET', KEYS[1], 'tokens', tokens, 'last_time', now)
    redis.call('EXPIRE', KEYS[1], math.ceil(burst / rate) + 10)
    return 1  -- 允许
else
    redis.call('HMSET', KEYS[1], 'tokens', tokens, 'last_time', now)
    return 0  -- 拒绝
end

工程要点:

  • 令牌桶允许突发流量(burst个请求瞬间打进来都能通过),这是它相比漏桶的核心优势
  • 加EXPIRE防止冷门key永远占内存
  • 时间戳由客户端传入而非用Redis TIME,避免集群时间不一致问题
  • 如果是集群限流,需要额外考虑key路由到同一slot(用hash tag {限流key})

【面试题】7. Sentinel和Hystrix在熔断降级机制上的核心区别是什么?生产环境如何选型? ​

【标准答案】核心区别从三个维度看:

1. 熔断策略

  • Hystrix:基于异常比例和请求量,达到阈值就熔断,只有简单的半开状态恢复
  • Sentinel:除了异常比例,还支持慢调用比例熔断,这在实际生产中更实用——接口没报错但响应从50ms飙到2秒,Hystrix不会熔断,Sentinel能精准捕获

2. 限流能力

  • Hystrix:只有隔离舱(线程池/信号量隔离),不是真正的限流
  • Sentinel:内置多种限流算法——QPS限流、线程数限流、热点参数限流、流控效果(直接拒绝/排队等待/慢启动),这是Sentinel的核心优势

3. 实时监控与规则配置

  • Hystrix:规则写死在代码里或配置文件,修改需重启
  • Sentinel:支持动态规则配置,接入了Nacos/Apollo等配置中心后,规则秒级生效;还有实时监控Dashboard,能看每个资源的QPS、RT、异常数

生产选型建议:

  • Hystrix已进入维护模式(不再更新),新项目不建议使用
  • Sentinel是阿里开源、社区活跃、功能更全,是当前Java微服务限流熔断的首选
  • 如果项目已经用了Spring Cloud Netflix全家桶,迁移成本不大时也建议逐步切换到Sentinel
  • 如果是Service Mesh架构,熔断限流建议下沉到Istio的Envoy sidecar,应用层不再关心

【面试题】8. Seata的AT模式的工作原理是什么?它的一阶段和二阶段分别做了什么?与TCC模式相比有什么优劣? ​

【标准答案】Seata AT模式是对业务零侵入的分布式事务方案,核心靠SQL解析和自动补偿。

一阶段(业务SQL+回滚日志):

  1. 拦截业务SQL,解析SQL语义找到要修改的记录
  2. 查询修改前的数据(before image),保存为回滚日志(undo log)
  3. 执行业务SQL,更新数据
  4. 提交本地事务(业务数据+undo log一起提交,保证原子性)
  5. 向TC(事务协调器)注册分支事务,上报执行结果

二阶段-提交:因为一阶段已经提交了本地事务,提交阶段只需要异步删除undo log,非常轻量

二阶段-回滚:

  1. 收到TC的回滚指令
  2. 读取undo log中的before image
  3. 校验当前数据是否被脏写(对比after image和当前数据)
  4. 如果没被脏写,根据before image生成反向SQL回滚
  5. 删除undo log

AT vs TCC对比:

维度AT模式TCC模式
业务侵入零侵入,只需加@GlobalTransactional重度侵入,每个操作写Try/Confirm/Cancel
性能一阶段直接提交本地事务,锁持有时间短Try阶段持有资源锁到Confirm,锁时间长
隔离性全局锁保证读已提交,存在脏读窗口Try阶段即隔离,一致性更强
适用场景常规业务,对一致性要求不是特别极致资金类核心业务,要求强隔离
局限性依赖SQL解析,不支持NoSQL;脏写检测失败需人工开发成本高,Confirm/Cancel幂等设计复杂

一句话:AT图省事,TCC图安全。大多数业务用AT就够了,涉及钱的核心链路上TCC。


【面试题】9. 在微服务架构中,API网关的核心职责有哪些?Spring Cloud Gateway和Kong分别适合什么场景? ​

【标准答案】API网关的核心职责,面试要分层说:

核心职责:

  1. 路由转发:根据URL/Host/Header将请求路由到后端微服务
  2. 认证鉴权:统一JWT校验、OAuth2.0认证、RBAC权限控制,下游服务不再重复鉴权
  3. 限流熔断:入口级限流,防止流量洪峰打垮整个系统
  4. 协议转换:外部HTTP转内部gRPC,或反向
  5. 灰度发布:按用户ID/Header/权重路由到不同版本的服务实例
  6. 日志监控:统一请求日志、链路追踪TraceId注入
  7. 响应聚合/裁剪:多个后端接口聚合为一个响应,或敏感字段脱敏

Spring Cloud Gateway vs Kong:

Spring Cloud Gateway:

  • 基于WebFlux(Netty+Reactor),异步非阻塞,性能不错
  • 与Spring Cloud生态无缝集成,Nacos注册发现、Sentinel限流天然支持
  • 用Java代码/YAML配置路由规则,开发灵活
  • 适合:Java技术栈团队,Spring Cloud微服务体系,需要深度定制网关逻辑

Kong:

  • 基于Nginx+OpenResty(Lua),C层面性能极致
  • 插件体系丰富,开箱即用(认证、限流、日志等数百插件)
  • 多语言微服务通吃,不绑定Java
  • 支持DB-less声明式配置(Kubernetes生态友好)
  • 适合:多语言微服务、高并发入口、K8s环境、团队不想写网关代码只想配插件

选型建议:纯Java体系选Spring Cloud Gateway,多语言或K8s原生选Kong,超大规模两者都用(Kong做外层流量网关,SCG做内层业务网关)。


【面试题】10. 请解释链路追踪中TraceId和SpanId的传递机制,在异步调用场景下如何保证链路不断? ​

【标准答案】链路追踪的核心数据模型是Trace和Span,传递机制是关键。

TraceId/SpanId传递机制:

  • 每个请求入口生成全局唯一的TraceId,贯穿整个调用链
  • 每经过一个服务/组件生成一个SpanId,格式通常为父SpanId.当前序号(如0.1.2表示根Span的第1个子Span的第2个子Span)
  • 传递方式:HTTP调用通过请求Header(如X-B3-TraceId、X-B3-SpanId),RPC调用通过附件(Attachment),消息队列通过消息属性(Message Properties)
  • 每个服务从Header/Attachment中提取上下文,继续往下传递

异步场景的链路断裂问题: 线程切换时ThreadLocal失效,TraceId丢失。这是最常见的坑。

解决方案:

  1. 线程池场景:用TraceRunnable/TraceCallable包装任务,在提交任务时捕获当前线程的Trace上下文,在执行任务时恢复到新线程。Spring Cloud Sleuth的LazyTraceExecutor就是这么干的。

  2. @Async场景:Sleuth自动包装异步任务,但需要确保用的是Spring的@Async而不是自己new线程。

  3. CompletableFuture场景:手动用ContextExecutorService包装线程池,或者用Tracing.newBuilder().currentTraceContext(ThreadLocalCurrentTraceContext.create())配置上下文传播。

  4. 消息队列场景:生产者发送消息时把TraceId写入消息Header,消费者消费时从Header取出恢复上下文。Spring Cloud Sleuth + Spring Cloud Stream已经自动处理了RabbitMQ/Kafka的链路传递。

  5. 跨服务异步回调:最复杂。比如A调B,B异步回调A,需要在回调请求中带上原始TraceId,可以用自定义Header或约定回调URL中带上TraceId参数。

面试重点:能说清"线程切换导致ThreadLocal失效"这个根因,再给出具体方案,面试官会觉得你真正踩过坑。