Skip to content

Java分布式架构面试题库 ​

生成时间:2026-05-21 22:57 | 题目数:10


【面试题】1. CAP定理中,为什么分布式系统只能三选二?在实际架构中如何做取舍? ​

【标准答案】CAP定理的核心原因是网络分区(P)是客观存在的,不是你能选的——分布式系统跨网络通信,网络迟早会断,所以P必须选。剩下的就是在C和A之间做权衡。分区发生时,你要么牺牲一致性保证可用性(AP),要么牺牲可用性保证一致性(CP)。不存在三者兼得的系统。

实际取舍看业务场景:

  • CP系统:金融转账、库存扣减,数据不能错,宁可不可用。典型如Zookeeper、Etcd、HBase。
  • AP系统:社交动态、商品评论,短暂不一致可以忍,但服务不能挂。典型如Cassandra、Eureka、DynamoDB。
  • 非分区时段,CAP三者可以同时满足,别被误导说"永远只能二选三"。

面试加分点:提到大多数互联网系统在分区恢复后通过补偿机制实现最终一致性,实际上是"AP + 最终C"的混合策略。


【面试题】2. Redis分布式锁用SET NX EX实现有什么问题?Redisson是如何解决的? ​

【标准答案】SET NX EX的三大经典问题:

  1. 锁超时误释放:业务没执行完,锁过期了,别的线程拿到了锁,前一个线程执行完把别人的锁删了。解决:value存唯一标识(如UUID),释放时Lua脚本判断是不是自己的锁再删。
  2. 不可重入:同一线程拿锁后再拿锁会被自己阻塞。解决:Redisson用Hash结构,field存线程ID,value存重入次数,加锁计数+1,解锁-1,归零才删。
  3. 续期问题:业务执行时间不确定,锁过期怎么办?解决:Redisson的看门狗(Watchdog)机制,默认30秒,每10秒(1/3超时时间)自动续期,业务完成或客户端宕机才停止。

Redisson还提供了:

  • 公平锁:按请求顺序获取,避免饥饿
  • 读写锁:读读共享,读写/写写互斥
  • 联锁(MultiLock):多把锁同时获取
  • RedLock算法:向N个独立Redis实例加锁,超过半数成功才算获取锁,解决单点故障问题

面试关键:RedLock在业界有争议(Martin Kleppmann写过文章质疑),实际生产中单Redis实例+Redisson看门狗是主流方案,RedLock用得相对少。


【面试题】3. TCC模式和Saga模式分别适用于什么场景?它们的补偿机制有什么本质区别? ​

【标准答案】

TCC模式:

  • 适用于执行时间短、一致性要求高的业务,如资金转账、库存扣减。
  • Try阶段做资源预留(冻结),Confirm阶段确认提交,Cancel阶段释放预留资源。
  • 补偿是反向操作——Cancel是对Try的精确撤销,比如冻结100元→解冻100元,操作是对称的。
  • 空回滚、幂等、悬挂是TCC三大难题,必须处理。

Saga模式:

  • 适用于长事务、涉及多个服务调用的场景,如订单创建流程(下单→扣库存→扣余额→发积分)。
  • 正向事务按顺序执行,某步失败则逆向补偿已完成的步骤。
  • 补偿是正向操作的业务补偿,不是技术回滚——比如已扣库存就补偿为"加回库存",而不是数据库rollback。
  • Saga有两种编排方式:** Choreography(事件驱动)** 和 Orchestration(集中编排)。

本质区别:

  • TCC的Try是预留资源,业务层面数据还是隔离的,Confirm/Cancel是二选一;Saga每步都是真实提交,没有隔离性,补偿是额外写一条"冲正记录"。
  • TCC一致性更强但侵入性大(每个服务写Try/Confirm/Cancel三个接口);Saga侵入性小但只有最终一致性,中间态可见。

面试加分:Seata框架中AT模式本质是自动化的TCC(框架帮你生成回滚SQL),降低了开发成本。


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

【标准答案】

ID构成(64位Long):

  • 1位符号位(永远0)
  • 41位时间戳(毫秒级,约69年)
  • 10位工作机器ID(可拆分为5位数据中心ID + 5位机器ID,共1024个节点)
  • 12位序列号(同一毫秒内可生成4096个ID)

单机QPS约400万/秒,完全够用。

时钟回拨问题: NTP时钟同步可能导致系统时间往回跳,此时生成的ID会重复。解决方案:

  1. 回拨时间小(<几毫秒):等待回拨时间过去再生成。百度的UidGenerator、美团的Leaf都采用这种策略。
  2. 回拨时间大:采用备用workerId,回拨时切换到新的机器ID。
  3. Leaf的方案:用Zookeeper维护workerId,每次启动时记录时间戳,回拨超过阈值(默认5ms)直接拒绝服务,等待时钟追上。
  4. 严苛方案:引入单调时钟(monotonic clock),不依赖系统时钟,自行维护时间递增。

面试提醒:别只说"等待回拨",面试官追问的就是回拨时间很长怎么办——备用workerId或拒绝服务是更完整的答案。


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

【标准答案】

缓存穿透:请求查询一个根本不存在的数据,缓存没有,数据库也没有,每次请求都打到DB。

  • 方案1:布隆过滤器,在缓存前加一层,不存在的key直接拦截。有误判率但可接受。
  • 方案2:缓存空值,查不到也缓存null,设短TTL(如60秒)。注意防止恶意攻击构造大量不同key。
  • 方案3:参数校验,在入口层拦截非法请求。

缓存击穿:某个热点key过期的瞬间,大量并发请求同时打到DB。

  • 方案1:互斥锁,只让一个线程查DB并回写缓存,其他线程等。用Redis SETNX实现。
  • 方案2:逻辑过期,缓存不设TTL,value里存逻辑过期时间,过期后异步更新,其他线程返回旧数据。
  • 方案3:热点key永不过期,配合手动刷新。

缓存雪崩:大量key同时过期,或者缓存集群宕机,请求全部涌向DB。

  • 方案1:过期时间加随机值,避免同时失效,如TTL = base + random(0~300s)。
  • 方案2:多级缓存,本地缓存(Caffeine)+ 分布式缓存(Redis),兜底。
  • 方案3:缓存高可用,Redis哨兵/Cluster,避免整个缓存层挂掉。
  • 方案4:熔断降级,DB扛不住时直接降级返回默认值或排队。

面试速记口诀:穿透查不到→布隆/空值;击穿热点过→互斥锁;雪崩大批过→随机TTL+多级缓存。


【面试题】6. JWT无状态方案和Session集中存储方案各有什么优缺点?如何选择? ​

【标准答案】

Session集中存储(Redis/Memcached):

  • 优点:服务端完全可控,能强制踢人、实时查看在线状态、token即时失效。
  • 缺点:有状态,依赖Redis可用性,每次请求都要访问Redis,水平扩展时Session同步是个问题。
  • 适用:后台管理系统、内部平台,用户量可控,安全性要求高。

JWT无状态方案:

  • 优点:无状态,不依赖存储,天然支持水平扩展,移动端友好,跨域方便。
  • 缺点:无法主动失效是最大问题——签发后只要没过期就一直有效;token体积大(Payload明文Base64),每次请求都携带浪费带宽;Payload不能存敏感信息。
  • 适用:开放API、移动端、微服务间调用。

怎么选:

  • 外部面向C端用户→JWT + 短有效期 + Refresh Token双Token方案。
  • 内部管理系统→Session集中存储,安全可控。
  • 混合方案:JWT做身份认证,敏感操作仍校验Redis中的状态(如是否被封禁),相当于JWT + Redis黑名单。

面试加分:提到Refresh Token的刷新机制——Access Token有效期短(如30分钟),Refresh Token有效期长(如7天),用Refresh Token换新的Access Token。Refresh Token存Redis,可以主动失效。


【面试题】7. 令牌桶和漏桶算法的区别是什么?分布式限流中Redis+Lua怎么实现滑动窗口? ​

【标准答案】

漏桶算法:请求进入漏桶,桶以固定速率漏水。不管突发流量多大,输出速率恒定。适合平滑流量,比如调用第三方API需要严格控制速率。缺点是面对突发流量时无法利用空闲期积累的处理能力。

令牌桶算法:桶以固定速率放令牌,请求消耗令牌,桶满则丢弃新令牌。允许突发流量——桶里攒够了令牌,瞬间可以处理一批请求。适合自身服务限流,合理利用处理能力。

核心区别:漏桶是恒速输出,令牌桶是匀速放行但允许突发。绝大多数场景用令牌桶更合适。

Redis+Lua实现滑动窗口限流:

lua
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local now = tonumber(ARGV[3])

-- 移除窗口外的记录
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)

-- 当前窗口计数
local count = redis.call('ZCARD', key)

if count < limit then
    redis.call('ZADD', key, now, now .. '-' .. math.random(1000000))
    redis.call('EXPIRE', key, window / 1000)
    return 1  -- 允许
else
    return 0  -- 限流
end

核心思路:用Sorted Set,score存时间戳,每次请求先删除窗口外的旧记录,再统计当前窗口请求数,未超限则加入新记录。Lua脚本保证原子性。

面试加分:提到Sentinel的滑动窗口实现更高效——用环形数组+时间窗口,时间复杂度O(1),比ZREMRANGEBYSCORE轻量。


【面试题】8. Raft算法的Leader选举过程是怎样的?选举超时时间如何设计? ​

【标准答案】

Leader选举过程:

  1. 所有节点初始都是Follower状态,启动一个选举超时定时器。
  2. 如果Follower在超时时间内没收到Leader的心跳,转为Candidate,自增当前任期(Term),投自己一票,向其他节点发送RequestVote RPC。
  3. 其他节点收到RequestVote后,如果本轮Term没投过票且Candidate的日志至少和自己一样新,就投票给它。
  4. Candidate收到多数派(N/2+1)投票,成为Leader,立即发送心跳通知所有节点。
  5. 如果出现多个Candidate平票(split vote),超时后重新选举。

选举超时时间设计:

  • 一般设为150~300ms的随机值。随机化是为了避免多个节点同时超时同时发起选举导致平票。
  • 太短:容易误判Leader故障,频繁选举,集群不稳定。
  • 太长:Leader真正故障后恢复慢,可用性下降。
  • 实践经验:心跳间隔远小于选举超时(如心跳50ms,选举超时150~300ms),确保正常情况下Follower不会误触发选举。

面试加分:提到Raft的关键约束——日志完整性原则,Leader选举不仅要多数票,还要求Candidate的日志至少和投票者一样新(比较lastLogTerm和lastLogIndex),保证选出来的Leader一定包含所有已提交的日志。


【面试题】9. Zookeeper的Watch机制有什么局限性?如何实现配置变更的实时通知? ​

【标准答案】

Watch机制的三大局限性:

  1. 一次性触发:Watch被触发一次后自动失效,需要重新注册。如果在重新注册的间隙发生了数据变更,会丢失事件。
  2. 轻量通知:Watch事件只告诉客户端"数据变了",不携带变更后的新值,客户端需要主动再去读取。
  3. 不保证有序:客户端看到Watch事件之前,可能已经读到了更新的数据(通过其他途径),存在时序问题。

配置变更实时通知的实现:

  1. 客户端在配置节点(如/config/app/db-url)上注册Watch。
  2. 配置中心修改节点数据时,触发Watch事件通知所有客户端。
  3. 客户端收到通知后重新读取最新值,并重新注册Watch。
  4. 对于一次性触发的问题,使用Curator的PathChildrenCache或TreeCache,它内部自动处理Watch的重新注册和事件监听,对开发者透明。

更优的替代方案:

  • Nacos配置中心用长轮询(Long Polling)+ UDP推送,既实时又可靠。
  • Apollo用长轮询 + 客户端定时拉取,双保险。

面试加分:Zookeeper的Watch不适合高频变更场景(如秒级配置更新),因为每次变更都要通知所有客户端重新Watch,产生"羊群效应"。Nacos/Apollo在这类场景下更合适。


【面试题】10. XXL-Job的架构是怎样的?任务分片和故障转移分别怎么实现? ​

【标准答案】

XXL-Job架构:

  • 调度中心(Admin):负责触发调度、管理任务、监控日志。可集群部署,通过数据库悲观锁保证同一触发时间只有一个节点触发调度。
  • 执行器(Executor):嵌入到业务应用中的客户端,负责接收调度并执行任务。通过Netty与调度中心通信。
  • 数据库:存储任务配置、调度日志、执行器注册信息。

任务分片实现:

  • 调度中心按分片参数(分片总数+分片序号)下发任务,每个执行器收到自己的分片序号。
  • 例如3个执行器处理10万条数据,分片总数=3,执行器A拿到分片0,处理第0、3、6…条数据;执行器B拿到分片1,处理第1、4、7…条。
  • 代码中通过XxlJobHelper.getShardIndex()和XxlJobHelper.getShardTotal()获取分片参数,自行按取模方式分配数据。
  • 新增执行器后,下次调度自动重新分片,实现弹性扩容。

故障转移实现:

  • 调度中心发出调度请求后,如果执行器在超时时间内未响应,调度中心标记该次调度失败,按策略处理:
    • 故障转移策略:依次尝试其他空闲执行器,直到有一个成功接受。
    • 失败重试策略:对同一执行器重试指定次数。
  • 执行器宕机后,调度中心通过心跳检测感知,将该执行器从注册列表移除,后续调度不再分配给它。

面试加分:XXL-Job调度中心集群用数据库行锁实现调度互斥,高并发场景下数据库是瓶颈。对比Elastic-Job用Zookeeper做分布式协调,无需数据库但运维复杂度更高。