Java分布式架构面试题库
生成时间:2026-05-24 20:16
【面试题】1. CAP定理中为什么说分区容错性P是必须保证的?在什么场景下可以选择CP或AP?
【标准答案】这是一个很好的切入点问题。CAP定理的核心是分布式系统在网络分区发生时,必须在一致性和可用性之间做选择。
分区容错性P必须保证,是因为分布式系统天生就存在网络不可靠的问题——网络延迟、节点故障、消息丢失都是常态,分区是不可避免的现实约束。如果放弃P,就意味着不允许网络分区,这在真实分布式环境中是不现实的。
选择CP的场景:金融账户系统、库存扣减、订单状态管理。比如银行转账,宁可系统暂时不可用,也不能出现账户余额不一致,否则会产生严重后果。Zookeeper就是典型的CP系统,Leader选举期间集群不可用。
选择AP的场景:社交媒体、电商商品展示、评论系统。用户刷新商品列表,短暂看到旧数据可以接受,但页面打不开会严重影响用户体验。Eureka就是AP设计,节点间数据同步失败时仍能提供服务注册查询。
实际项目中,我们往往在细分场景做选择——核心交易链路走CP,边缘业务走AP,做到整体平衡。
【面试题】2. Redisson的看门狗机制解决了什么问题?如果看门狗失效会发生什么?
【标准答案】Redisson的看门狗机制解决的是分布式锁自动续期问题。
传统Redis分布式锁用SET key value NX EX 30设置30秒过期,但如果业务执行超过30秒,锁就会自动释放,其他线程拿到锁后,原线程还在执行,导致并发安全问题。如果设置过期时间太长,又会出现持有者宕机后锁迟迟不释放的问题。
Redisson的看门狗机制是:默认每10秒检查一次锁是否还被当前线程持有,如果持有就续期到30秒。这样只要业务线程还在运行,锁就不会过期;业务结束主动释放,看门狗自动停止。
如果看门狗失效,比如Redisson客户端宕机、Redis连接断开、或者业务线程被kill,锁会在最后一次续期后30秒自动过期释放。这个设计保证了即使发生异常,锁也不会永久持有。
实际使用中,我们会根据业务平均执行时间调整lockWatchdogTimeout参数,确保看门狗续期间隔合理。极端情况还要考虑业务超时主动降级,防止死循环导致锁一直被持有。
【面试题】3. TCC模式和Saga模式在分布式事务中如何选择?各有什么适用场景?
【标准答案】这两个都是最终一致性的分布式事务方案,但设计思路完全不同。
TCC是Try-Confirm-Cancel,核心是预留资源、确认执行、取消回滚三个阶段。Try阶段冻结库存,Confirm扣减库存,Cancel解冻库存。优点是资源锁定粒度细,失败时能精确补偿。缺点是业务侵入性强,每个接口都要写三套逻辑,还要处理幂等、空回滚、悬挂等边界问题。
Saga是长事务拆分成多个本地事务,每个本地事务有对应的补偿动作。执行顺序是T1→T2→T3,失败时反向补偿C3→C2→C1。优点是对业务侵入小,适合跨系统的长流程事务。缺点是没有资源预留,可能出现脏读,补偿逻辑可能失败。
选择建议:
- 金融核心交易、库存扣减等资源竞争激烈的场景,选TCC。因为能锁定资源,防止超卖。
- 跨多个系统的长流程业务,如订单创建→库存扣减→积分增加→消息通知,选Saga。因为协调成本低,接入简单。
- 如果系统已经有成熟的补偿机制,用Saga增量改造更快。如果从零建设且业务简单,TCC更可控。
实际项目中,我们结合使用:核心交易链路用Seata的TCC模式,边缘业务用Saga模式,配置中心统一管理事务状态。
【面试题】4. Snowflake雪花算法的时间回拨问题如何解决?WorkerId冲突怎么办?
【标准答案】Snowflake依赖机器时钟,时间回拨是最大的痛点,可能生成重复ID。
时间回拨解决方案:
- 轻微回拨(<5ms):等待时钟追平,直接sleep后继续生成。这是最常见的处理方式。
- 中等回拨(5ms~1s):使用备用workerId,开启新的ID序列空间。不同workerId生成的ID不会冲突。
- 严重回拨(>1s):直接报错或降级到数据库生成,让运维介入处理时钟问题。
百度UidGenerator的方案是:维护最近一秒内每毫秒的最大序列号,回拨时复用历史序列号空间,前提是单机QPS不超上限。
WorkerId冲突解决方案:
- Zookeeper注册:每个实例启动时向ZK注册临时节点,路径包含workerId,节点存在则换下一个。Leaf-segment就是这么干的。
- 数据库号段分配:在数据库维护workerId分配表,实例启动时申请租约,定期续约。
- 本地配置文件:小规模集群可以手工配置,适合机器数量固定的场景。
生产环境推荐用Zookeeper或数据库集中分配,配合心跳续约检测僵尸实例。同时做好监控告警,一旦workerId池耗尽或时钟回拨超阈值立即通知。
【面试题】5. 缓存穿透、缓存击穿、缓存雪崩的区别是什么?各用什么方案解决?
【标准答案】这三个是缓存三大经典问题,本质都是缓存失效导致请求直接打到数据库。
缓存穿透:查询不存在的数据,缓存无数据可存,每次请求都穿透到数据库。恶意攻击常用这种手段。 解决方案:
- 布隆过滤器:启动时加载所有有效key,查询前先判断key是否可能存在,不存在直接返回。
- 缓存空值:查不到数据时缓存null或特定标记,设置较短过期时间(如5分钟)。
- 接口层校验:对参数做合法性校验,过滤明显非法请求。
缓存击穿:热点key在某一时刻过期,瞬间大量请求同时击穿缓存直达数据库。 解决方案:
- 热点数据永不过期:不设置过期时间,后台异步更新。
- 互斥锁:第一个请求去查数据库,其他请求等待结果。可以用Redis的SETNX实现。
- 逻辑过期:数据本身不过期,但记录逻辑过期时间,查询时异步刷新。
缓存雪崩:大量key在同一时间集中过期,或Redis宕机导致全面缓存失效。 解决方案:
- 过期时间加随机值:避免同时过期,如基础过期时间+随机1~5分钟。
- 多级缓存:本地缓存+Redis缓存,Redis挂了本地缓存还能扛一阵。
- 熔断降级:数据库压力过大时熔断,返回降级数据。
- Redis高可用:主从+哨兵+Cluster,避免单点故障。
实际项目中,这三者往往结合使用。我们系统就是:布隆过滤器防穿透 + 热点key互斥锁防击穿 + 过期时间随机化+多级缓存防雪崩。
【面试题】6. Raft算法的Leader选举过程是怎样的?为什么能保证同一时刻只有一个Leader?
【标准答案】Raft是强一致性的共识算法,Leader选举是它的核心机制。
选举过程:
- 初始所有节点都是Follower状态,每个节点有随机的选举超时时间(150~300ms)。
- 如果Follower在超时时间内没有收到Leader心跳,转为Candidate状态,任期term+1,投自己一票。
- Candidate向其他节点发送RequestVote请求,请求参数包含term、candidateId、lastLogIndex、lastLogTerm。
- 其他节点收到请求后,检查:term是否>=自己的term、candidate的日志是否比自己新。都满足才投票。
- Candidate收到多数派投票后成为新Leader,立即发送心跳确认地位。
为什么保证单一Leader:
- 多数派原则:必须获得超过半数投票才能成为Leader,不可能有两个节点同时获得多数派。
- 随机超时:每个节点的选举超时随机,大概率只有一个节点先触发选举。
- Term递增:每个term最多一个Leader,新term的Leader会让旧term的Leader自动降级。
- 日志完整性校验:投票时检查候选者日志是否比自己新,保证选出的Leader拥有最新数据。
实际使用中,比如Etcd和Consul都是基于Raft。我们项目中用Etcd做配置中心,观察到Leader故障时,新Leader选举通常在1秒内完成,客户端几乎无感知。
【面试题】7. XXL-Job的任务分片机制如何实现海量数据处理?分片失败如何处理?
【标准答案】XXL-Job的分片广播模式能把一个大任务拆分成多个子任务并行处理,是处理海量数据的利器。
分片实现原理:
- 配置任务的路由策略为分片广播(ShardingUtil.ShardingVO)。
- 调度中心根据执行器数量动态生成分片参数:分片序号(0,1,2...)和总分片数。
- 每个执行器收到任务时,从ShardingUtil获取自己的分片序号。
- 业务代码按分片序号处理对应数据,如:WHERE MOD(id, 总分片数) = 分片序号。
代码示例:
ShardingUtil.ShardingVO shardingVO = ShardingUtil.getShardingVo();
int shardIndex = shardingVO.getIndex(); // 当前分片序号
int shardTotal = shardingVO.getTotal(); // 总分片数
// 查询本分片数据
List<Order> orders = orderMapper.selectByShard(shardIndex, shardTotal);分片失败处理:
- 执行器宕机:其他执行器的分片任务正常完成,数据部分延迟。下次调度时自动重新分片。
- 新执行器加入:下次调度时自动加入分片,无需人工干预。
- 所有执行器宕机:任务执行失败,触发告警,按配置重试或跳过。
生产环境建议:
- 分片数量=执行器数量或其倍数,便于负载均衡。
- 数据源做好分库分表,避免单库成为瓶颈。
- 任务失败时记录断点,支持从断点续跑。
- 监控每个分片的执行时间,发现慢分片及时优化。
我们用XXL-Job处理每日千万级订单对账,10个执行器分片,单任务处理时间从2小时降到15分钟。
【面试题】8. Sentinel的限流策略有哪些?如何选择固定窗口、滑动窗口、令牌桶?
【标准答案】Sentinel提供了多种限流策略,核心是通过QPS或线程数控制流量入口。
主要限流策略:
固定窗口计数器:将时间划分为固定大小的窗口,如每秒一个窗口,统计窗口内请求数。 优点:实现简单,内存占用小。 缺点:边界问题,窗口切换瞬间可能承受2倍流量(前一窗口末尾+后一窗口开头)。 适用:对突发流量不敏感的场景,如后台任务限流。
滑动窗口:将大窗口划分为多个小格子,如1秒划分10个100ms格子,统计最近1秒内的请求。 优点:更平滑,避免固定窗口的边界问题。 缺点:内存占用略高,需要维护多个格子。 适用:需要精确控制QPS的API接口。
令牌桶:以固定速率往桶中放令牌,请求到达时取令牌,有令牌放行,无令牌拒绝。 优点:允许一定突发流量,桶内有令牌时可以快速响应。 适用:需要应对突发流量的场景,如秒杀、促销活动。
漏桶:请求像水一样流入桶中,以固定速率流出处理。 优点:输出流量绝对平稳。 缺点:无法应对突发流量,多余请求直接丢弃。 适用:需要严格保护下游系统的场景,如调用第三方API。
选择建议:
- 内部服务调用:滑动窗口,精确控制QPS。
- 面向用户的API:令牌桶,允许正常突发。
- 调用外部API:漏桶,保护下游不超载。
- 后台任务:固定窗口,简单够用。
Sentinel中通过FlowRule配置,grade字段指定策略,count指定阈值。我们生产环境80%接口用滑动窗口,核心交易接口用令牌桶+预热模式。
【面试题】9. 分布式链路追踪的TraceId和SpanId如何传递?异步调用时如何保证链路完整?
【标准答案】TraceId标识一次完整请求链路,SpanId标识链路中的每个服务节点。传递机制是分布式追踪的基础。
同步调用传递:
- 请求入口生成TraceId(如UUID或Snowflake ID),初始SpanId=1。
- 调用下游时,通过HTTP Header传递:X-Trace-Id、X-Span-Id、X-Parent-Span-Id。
- 下游服务收到请求,从Header提取TraceId,生成新的SpanId(如1.1、1.2),记录父SpanId。
- 依次类推,形成完整的调用树。
Spring Cloud Sleuth自动完成Header传递,无需业务代码干预。核心组件是TracingFilter,拦截请求自动创建Span。
异步调用传递(难点): 异步场景中,上下文会丢失,需要手动传递:
- 线程池场景:使用装饰过的线程池
ExecutorService executor = new TraceExecutorWrapper(
Executors.newFixedThreadPool(10),
tracing
);或使用TaskDecorator:
@Bean
public TaskDecorator taskDecorator() {
return runnable -> {
Span span = tracer.currentSpan();
return () -> {
try (Tracer.SpanInScope ws = tracer.withSpan(span)) {
runnable.run();
}
};
};
}消息队列场景:发送消息时将TraceId写入消息Header,消费时提取并恢复上下文。 Kafka示例:Producer拦截器注入Header,Consumer拦截器提取Header。
定时任务场景:任务触发时生成新TraceId,或从任务元数据中获取。
生产环境踩坑:
- 记得配置采样率,全量采集对存储压力大。
- 异步方法的@Async注解要配合TaskDecorator。
- 跨线程调用用TransmittableThreadLocal替代InheritableThreadLocal。
我们用SkyWalking,配置10%采样率,链路数据存ES,保留7天。
【面试题】10. Nacos配置中心如何实现配置热更新?客户端配置变更监听的原理是什么?
【标准答案】Nacos配置热更新依赖长轮询机制,实现毫秒级配置变更通知。
热更新原理:
- 客户端启动时向Nacos Server注册监听,发送长轮询请求(默认30秒超时)。
- Nacos Server收到请求后,检查配置是否有变更:
- 无变更:挂起请求,等待配置变更或超时返回。
- 有变更:立即返回新配置。
- 客户端收到响应后:
- 配置变更:回调Listener通知业务代码,触发@RefreshScope更新Bean。
- 超时无变更:立即发起新的长轮询,保持持续监听。
长轮询 vs 轮询:
- 传统轮询:客户端每N秒主动拉取,延迟=N秒,QPS高时服务端压力大。
- 长轮询:请求挂起等待变更,实时性好,无变更时几乎没有无效请求。
客户端监听实现:
configService.addListener("app.yml", "DEFAULT_GROUP", new Listener() {
@Override
public void receiveConfigInfo(String configInfo) {
// 配置变更回调,业务处理
log.info("配置变更: {}", configInfo);
}
});Spring Cloud集成:
- 添加@RefreshScope注解的Bean,配置变更时自动重新创建。
- 使用@Value("${xxx}")注入的配置,变更后自动更新。
- 自定义配置类配合@ConfigurationProperties,实现复杂配置的热更新。
多环境管理:
- 通过Namespace隔离环境(dev/test/prod)。
- 通过Group隔离项目或模块。
- 配置优先级:Profile > Namespace > Group > DataId。
生产环境配置:
- Nacos集群部署,至少3节点,保证高可用。
- 数据库持久化配置,Nacos宕机后配置不丢失。
- 配置变更触发灰度发布,逐步推送到实例。
- 敏感配置加密存储,使用Nacos的加密配置功能。
我们项目用Nacos管理200+配置项,数据库连接池、限流阈值、功能开关都能实时调整,避免了改配置重新部署的繁琐流程。
共 10 道面试题,覆盖:CAP理论、Redisson锁、TCC/Saga、Snowflake、缓存问题、Raft算法、XXL-Job分片、Sentinel限流、链路追踪、Nacos配置中心