Java分布式架构面试题库
生成时间: 2026-05-23 23:27 题目数量: 10道
本轮 Java分布式架构面试题库(带答案)
【面试题】1. CAP定理中的"三选二"在实际分布式系统中如何做权衡选择?
【标准答案】CAP定理说的是一致性、可用性、分区容错性三者只能满足两个。实际场景中,分区容错性P是必须保证的,因为网络分区是客观存在的,所以真正的选择是在C和A之间。
典型选型策略:
- 选CP放弃A:适合金融交易、订单系统、库存扣减等强一致性场景。比如Redis Cluster、HBase、MongoDB,网络分区时会拒绝写入,牺牲可用性保证一致性
- 选CA放弃P:理论上只存在于单机系统,分布式环境无法避免网络分区,所以CA在实际分布式系统中不现实
- 选AP放弃C:适合社交动态、用户评论、商品浏览等对一致性要求不高的场景。比如Cassandra、DynamoDB,允许短暂数据不一致,优先保证可用性
实际落地:我们项目里,核心交易链路用CP架构(Redis Cluster主从同步),用户中心、推荐系统用AP架构(Cassandra多数据中心)。关键是要根据业务场景评估数据不一致的容忍度和故障恢复时间(RTO),不能一刀切。
【面试题】2. Redis分布式锁的SET NX EX方式有什么缺陷?Redisson如何解决这些问题?
【标准答案】SET NX EX方式是最简单的Redis分布式锁实现,命令是SET lock:resource value NX EX 30,但有几个明显缺陷:
缺陷一:锁无法续期
- 问题:业务执行时间超过锁过期时间,锁自动释放,其他线程拿到锁,导致并发问题
- 解决:Redisson引入Watchdog机制,后台线程每10秒(默认过期时间1/3)自动续期,业务未完成前锁不会失效
缺陷二:锁误删
- 问题:线程A的锁过期释放,线程B拿到锁,线程A执行完删除了线程B的锁
- 解决:Redisson用Lua脚本保证原子性,删除时校验锁的value(UUID+线程ID),只删自己的锁
缺陷三:不可重入
- 问题:同一线程多次获取同一把锁会死锁
- 解决:Redisson通过Hash结构记录重入次数,支持可重入锁
缺陷四:主从切换丢锁
- 问题:主节点加锁成功但未同步到从节点,主节点宕机,从节点升为主,锁丢失
- 解决:Redisson的Redlock算法,向多个独立Redis节点申请锁,超过半数成功才算加锁成功
我们生产环境用的是Redisson的RLock,配置了Watchdog超时30秒,关键业务还会加Redlock防止主从切换丢锁。
【面试题】3. TCC分布式事务模式和2PC有什么区别?什么场景适合用TCC?
【标准答案】TCC和2PC都是分布式事务解决方案,但设计思路完全不同:
核心区别:
- 2PC是强一致:准备阶段锁定资源,提交阶段释放,所有参与者必须同时成功或失败,有阻塞性能问题
- TCC是最终一致:Try阶段预留资源(冻结),Confirm阶段真正执行,Cancel阶段回滚预留,整个过程无阻塞
TCC三阶段详解:
- Try:业务检查+资源预留,比如扣减余额时先冻结金额,订单状态改为"支付中"
- Confirm:确认执行,真正扣减余额,订单状态改为"已支付",无需再检查
- Cancel:取消预留,解冻余额,订单状态改为"已取消"
适用场景:
- 对性能要求高:TCC无阻塞,吞吐量比2PC高很多,适合高并发场景
- 业务可拆分:Try/Confirm/CANCEL三个逻辑能清晰划分,比如支付、库存、积分
- 允许最终一致:短暂数据不一致可接受,比如秒杀场景
不适用场景:
- 业务逻辑简单,无法拆分Try/Confirm/Cancel
- 对一致性要求极高,不允许任何中间状态(用2PC或Seata AT模式)
我们项目用Seata的TCC模式,支付场景:Try冻结余额,Confirm扣减余额并增加流水,Cancel解冻余额。关键是要保证幂等性,Confirm和Cancel可能重试多次,必须用唯一业务ID去重。
【面试题】4. Snowflake雪花算法生成分布式ID有什么缺点?Leaf号段模式如何优化?
【标准答案】Snowflake是64位ID生成算法(1位符号位+41位时间戳+10位机器ID+12位序列号),优点是高性能、趋势递增,但有明显缺点:
Snowflake缺点:
- 时钟回拨问题:机器时钟回拨会生成重复ID,严重导致服务不可用
- 机器ID分配:需要独立服务分配机器ID,或配置文件手动配置,集群扩容麻烦
- 数据倾斜:时间戳在高位,按ID排序是按时间排序,但不同机器并发写入可能导致索引分裂
Leaf号段模式优化方案:
Leaf Segment(号段模式):
- 原理:数据库记录max_id和step,每次批量获取一个号段(如1000个ID)缓存在内存
- 优势:减少数据库访问频率,吞吐量从几百提升到几万QPS
- 缺点:ID不连续,重启会浪费部分ID
Leaf Snowflake优化版:
- 时钟回拨:启动时记录last_timestamp,发现回拨立即报警并拒绝服务
- 机器ID:使用Zookeeper顺序节点自动分配,重启后复用原workId
- 双buffer优化:一个buffer用完后异步加载下一个buffer,无卡顿
对比选型:
- Snowflake:适合对ID连续性要求不高,机器数量少的场景
- Leaf Segment:适合高并发场景,对ID连续性不敏感
- Leaf Snowflake:适合大规模集群,需要趋势递增
我们订单系统用Leaf Segment,每批次获取2000个ID,QPS峰值能到5万,关键是要监控号段剩余量,低于阈值提前加载。
【面试题】5. 缓存穿透、缓存击穿、缓存雪崩的区别和解决方案?
【标准答案】这三个是缓存最经典的故障场景,必须清晰区分:
缓存穿透:
- 定义:查询一个根本不存在的数据,缓存没命中,数据库也没有,导致每次都打到数据库
- 典型场景:恶意攻击查询
id=-1、id=999999999等不存在的key - 解决方案:
- 布隆过滤器:提前加载所有有效key的bitmap,查询前先判断,不存在的直接返回
- 空值缓存:查询数据库为空时,也缓存一个null值,设置较短过期时间(如5分钟)
- 接口层校验:参数合法性校验,过滤明显非法请求
缓存击穿:
- 定义:某个热点key过期瞬间,大量并发请求同时访问,全部打到数据库
- 典型场景:秒杀商品信息、热门文章详情等高并发key
- 解决方案:
- 互斥锁:只允许一个线程查数据库并回写缓存,其他线程等待
- 逻辑过期:不设置过期时间,value中包含逻辑过期时间,后台异步刷新
- 热点数据永不过期:配置中心手动触发刷新
缓存雪崩:
- 定义:大量key同时过期,或Redis宕机,导致所有请求打到数据库
- 典型场景:缓存批量设置相同过期时间、Redis集群故障
- 解决方案:
- 过期时间加随机值:避免同时过期,比如基础时间30分钟+随机0-5分钟
- 多级缓存:本地缓存(Caffeine)+分布式缓存(Redis),Redis挂了还有本地缓存
- 熔断降级:数据库压力过大时触发熔断,返回降级数据
我们项目里,热门商品用逻辑过期方案,秒杀场景预热时不设置过期时间,活动结束后手动清理;普通商品用随机过期时间防止雪崩;不存在的商品用布隆过滤器拦截。
【面试题】6. Raft算法的Leader选举过程是怎样的?如果出现多个Candidate怎么办?
【标准答案】Raft算法的核心是强Leader模式,所有日志复制都通过Leader进行,Leader选举过程:
正常选举流程:
- Follower超时:Follower在election timeout(150-300ms随机)内未收到Leader心跳,转为Candidate
- 发起选举:Candidate增加term,给自己投票,向其他节点发送RequestVote RPC
- 投票规则:每个term每个节点只能投一票,先到先得,候选人日志必须比自己新(至少不旧)
- 获得多数票:获得超过半数节点投票的Candidate成为Leader,立即发送心跳确立权威
多Candidate冲突处理:
- 场景:网络分区导致多个Follower同时超时,出现多个Candidate
- 解决:Split Vote机制
- 多个Candidate瓜分选票,无人获得多数票
- 选举超时,自动进入下一轮选举
- 由于election timeout随机,大概率下一轮只有一个Candidate胜出
- 优化:PreVote机制,Candidate先确认能获得多数票再发起选举,减少无效选举
Leader宕机恢复:
- 新Leader选举产生后,对比日志差异,Follower日志落后会被覆盖,多余日志会被截断
- 强一致性保证:Leader不会覆盖或删除自己的日志,只做追加
我们用Etcd做配置中心,底层是Raft协议,3节点集群,选举超时配置1秒,关键是节点数必须是奇数(3/5/7),保证能获得多数票。
【面试题】7. XXL-Job分布式调度的架构设计是怎样的?如何保证任务不重复执行?
【标准答案】XXL-Job是国内最流行的分布式任务调度平台,架构设计非常清晰:
核心架构:
- 调度中心(Admin):负责任务调度、触发、监控,管理界面
- 执行器(Executor):嵌入到业务应用中,接收调度指令并执行任务
- 通信方式:调度中心通过Netty HTTP协议触发执行器,执行器注册到调度中心
任务不重复执行机制:
方案一:分片广播:
- 调度中心广播任务到所有执行器,每个执行器获取分片参数(分片序号/总分片数)
- 业务逻辑根据分片参数处理不同数据,如
id % totalShard == shardIndex - 适合大数据量任务并行处理
方案二:故障转移:
- 调度中心记录每个任务的执行状态(Running/Success/Fail)
- 任务触发时检查上次执行状态,Running状态的任务不再触发
- 执行器故障时,调度中心自动路由到健康执行器
方案三:并发策略:
- 单机串行:同一执行器上任务排队执行
- 丢弃后续调度:上次执行未完成,新调度直接丢弃
- 覆盖之前调度:停止上次执行,执行新调度
错过重做机制:
- 调度中心记录任务触发记录,调度失败的任务标记为"错过"
- 后台线程定期扫描错过的任务,重新触发执行
我们项目用XXL-Job处理订单超时取消,配置分片广播+单机串行,每台机器处理部分订单,避免重复。关键是要给任务加分布式锁(Redis SET NX),防止调度异常导致重复执行。
【面试题】8. 分布式链路追踪的TraceId和SpanId如何传递?异步调用如何保证链路完整?
【标准答案】链路追踪的核心是TraceId全链路传递,SpanId标记调用层级:
TraceId传递机制:
- 入口生成:网关或第一个服务生成全局唯一TraceId(如UUID或Snowflake)
- HTTP传递:通过Header传递,如
X-Trace-Id、X-Span-Id - RPC传递:Dubbo通过RpcContext,Feign通过RequestInterceptor,将TraceId注入到请求头
- 线程池传递:通过TransmittableThreadLocal(TTL)或MDC(Mapped Diagnostic Context)传递到子线程
SpanId层级关系:
- SpanId格式:
0.1、0.1.1、0.1.1.1,表示调用层级 - 每次调用生成子SpanId,如父SpanId=0.1,子SpanId=0.1.1
- 记录调用关系:Parent SpanId → Current SpanId → Child SpanId
异步调用链路追踪难点:
场景一:线程池异步执行
- 问题:主线程的TraceId无法自动传递到线程池的工作线程
- 解决:使用TransmittableThreadLocal(TTL),自动复制父线程上下文到子线程
- 代码示例:
TtlExecutors.getTtlExecutorService(executor)
场景二:消息队列异步
- 问题:发送消息时TraceId丢失,消费端无法关联
- 解决:将TraceId作为消息Header传递(RocketMQ的Message.getProperties())
- 消费端:消费时从Header提取TraceId,放入当前上下文
场景三:异步回调
- 问题:回调执行时原始上下文已清除
- 解决:回调前保存上下文,回调时恢复上下文(如Reactor的Context)
我们项目用SkyWalking做链路追踪,通过Java Agent字节码增强自动埋点,业务代码无侵入。异步调用需要手动传递TraceId,关键是所有中间件(MQ、Redis、RPC)都要支持上下文传递。
【面试题】9. Nacos作为配置中心如何实现配置热更新?配置变更如何通知所有节点?
【标准答案】Nacos配置中心的核心能力是动态配置,配置变更实时生效:
配置热更新机制:
方案一:长轮询(推荐):
- 客户端发起长轮询请求,timeout=30秒
- 服务端挂起请求,配置变更时立即返回,无变更则30秒后返回
- 客户端收到响应后立即发起新的长轮询,实现准实时推送
方案二:UDP推送:
- 客户端注册UDP端口到服务端
- 配置变更时服务端通过UDP推送到所有客户端
- UDP可靠性低,作为长轮询的补充
配置变更通知流程:
- 用户在Nacos控制台修改配置,点击发布
- 服务端更新配置内容,记录版本号(MD5)
- 服务端通知所有长轮询客户端(或UDP推送)
- 客户端收到通知,对比本地MD5,发现变化则拉取最新配置
- 客户端触发
@RefreshScope注解的Bean重新加载 - 应用使用最新配置,无需重启
多环境管理:
- Namespace隔离环境:dev、test、prod
- Group分组业务:database、mq、redis
- Data ID标识配置:
application.yml、application-dev.yml
配置回滚机制:
- Nacos保留30天配置历史,可一键回滚到任意版本
- 配置变更记录操作人、时间、变更内容
高可用设计:
- Nacos集群部署(至少3节点),数据存储到MySQL
- 客户端缓存本地配置快照,Nacos宕机时使用本地缓存
- 配置变更失败时自动重试,保证最终一致性
我们项目用Nacos Config管理数据库连接、Redis配置、业务参数等,关键配置用@RefreshScope实时刷新,非关键配置启动时加载。生产环境配置变更加审批流程,避免误操作。
【面试题】10. 服务网格Istio的Sidecar模式如何实现流量管理?和传统微服务框架对比有什么优势?
【标准答案】Istio是服务网格的代表性实现,核心是Sidecar代理模式:
Sidecar架构:
- 每个Pod注入一个Envoy容器作为Sidecar
- 所有进出Pod的流量都经过Envoy代理
- Envoy拦截网络流量,执行路由、负载均衡、熔断、限流等策略
- 控制面(Istiod)下发配置到Envoy,数据面(Envoy)执行流量管理
流量管理实现:
路由规则:
- VirtualService定义路由规则:如
weight: 90、v1版本路由90%流量 - DestinationRule定义子集:如
v1、v2、canary - 路由条件:Header、URI、权重等
灰度发布:
- 金丝雀发布:新版本灰度10%流量,逐步放量
- 蓝绿部署:全量切换到新版本,一键回滚
- A/B测试:根据Header路由到不同版本
故障注入:
- 延迟注入:模拟网络延迟,测试超时处理
- 中止注入:模拟服务故障,测试熔断降级
与传统微服务框架对比:
| 维度 | 传统框架(Spring Cloud) | 服务网格(Istio) |
|---|---|---|
| 实现方式 | 业务代码集成SDK | Sidecar代理,业务无感知 |
| 语言绑定 | 绑定Java,多语言需重复实现 | 语言无关,支持多语言 |
| 运维成本 | SDK升级需重新部署 | Sidecar升级透明 |
| 性能损耗 | SDK直接调用,损耗低 | 多一层代理,延迟增加1-3ms |
| 学习成本 | 需要掌握框架API | 学习Istio配置即可 |
| 灵活性 | 业务代码可定制化 | 配置驱动,灵活性受限 |
优势场景:
- 多语言微服务:Java/Go/Python/Node混合,统一治理
- 遗留系统改造:老系统无法改造,通过Sidecar注入治理能力
- 运维友好:配置变更无需重启,监控可观测性开箱即用
劣势场景:
- 性能敏感:高频调用场景,Sidecar延迟不可接受
- 小规模系统:架构复杂度过高,杀鸡用牛刀
我们新项目尝试用Istio,主要看重多语言支持和运维友好性,但老项目还是用Spring Cloud。迁移时可以逐步试点,一个服务一个服务地迁移,新旧架构并存。
总结: 本轮题目覆盖理论基础(CAP、Raft)、架构设计(TCC、Snowflake、缓存)、场景实操(分布式锁、链路追踪)、故障排查(缓存三大问题)、性能优化(调度、配置中心)、面试情景(服务网格)。答案聚焦面试口语表达,简洁落地,突出实战经验。