本轮 Java分布式架构面试题库(带答案)
【面试题】1. CAP定理中为什么只能三选二?在实际系统中如何权衡选择?
【标准答案】CAP定理指出分布式系统最多只能同时满足一致性(C)、可用性(A)、分区容错性(P)中的两项。原因是网络分区是客观存在的(网络总会出问题),P必须保证,所以实际是在C和A之间做选择。
典型场景:- CP系统(如Zookeeper):保证一致性,牺牲可用性。当Leader挂了进入选举时,集群不可用,但数据始终一致。
- AP系统(如Eureka):保证可用性,牺牲强一致性。节点挂了其他节点继续服务,但可能读到旧数据。
实战建议:大多数业务选AP更合适,因为短暂不一致可接受,但服务不可用直接影响用户体验。只有金融转账、库存扣减等必须强一致的场景才选CP。
【面试题】2. Redis分布式锁用SET NX EX有什么问题?Redisson如何解决?
【标准答案】SET NX EX原生命令的三大问题:
- 锁续期问题:业务执行超时,锁自动释放,其他线程抢到锁,导致并发问题
- 误删问题:线程A锁超时释放,线程B抢到锁,线程A执行完删了B的锁
- 单点故障:主节点挂了,锁未同步到从节点,锁丢失
Redisson解决方案:
- Watch Dog看门狗:后台线程定时续期(默认30秒锁,每10秒续期),业务没执行完锁不失效
- Lua脚本原子操作:加锁时写入线程标识,释放时先判断标识再删,只有自己能删自己的锁
- Redlock算法:向多个Redis节点申请锁,超过半数成功才算加锁成功,解决单点问题
代码示例:```java RLock lock = redisson.getLock("myLock"); lock.lock(); // 自动续期 try { // 业务逻辑 } finally { lock.unlock(); // Lua脚本安全释放 }
---
#### 【面试题】3. TCC模式和Saga模式在分布式事务中有什么区别?各适合什么场景?
【标准答案】**核心区别**:
| 维度 | TCC | Saga |
|------|-----|------|
| **事务类型** | 补偿型事务 | 长运行事务 |
| **实现复杂度** | 高(需写Try/Confirm/Cancel三套逻辑) | 中(正向+补偿两套) |
| **锁资源** | Try阶段锁定资源,隔离性好 | 无锁,最终一致性 |
| **性能** | 较低(资源被锁定) | 较高(异步执行) |
| **失败处理** | Cancel立即补偿 | 补偿事务逆序执行 |
**适用场景**:
- **TCC**:金融转账、库存预占、票务系统等需要资源隔离、强业务约束的场景。例如:下单时Try冻结库存,支付成功Confirm扣减,取消订单Cancel解冻。
- **Saga**:跨系统长流程(如订单→支付→物流→通知)、资源竞争不激烈的场景。例如:电商下单后,支付失败触发逆向补偿。
**实战建议**:TCC适合短事务、高隔离场景;Saga适合长流程、跨系统编排场景。Seata框架同时支持两种模式。
---
#### 【面试题】4. Snowflake雪花算法的41位时间戳用完了怎么办?时钟回拨怎么处理?
【标准答案】**时间戳耗尽问题**:
- 41位时间戳可用约69年(从固定epoch开始计算)
- **解决方案**:调整epoch起始时间(如从2020年开始),或者扩展到42位(牺牲一位机器ID)
**时钟回拨问题**(更常见):
1. **轻度回拨**(<几毫秒):自旋等待,等时间追上来再生成
2. **重度回拨**(>几毫秒):直接拒绝生成,上报告警,人工介入
3. **优化方案**:
- **UidGenerator**:使用当前秒+序列号,回拨时复用上一秒的序列
- **Leaf号段模式**:从数据库批量取号,本地缓存认发,不受时钟影响
- **双Buffer机制**:一个buffer用完切到另一个,后台异步预取下一批
生产建议:优先用Leaf号段模式或美团的Leaf,避免时钟回拨问题。如果必须用Snowflake,部署NTP同步,监控时钟偏差。
---
#### 【面试题】5. Redis缓存穿透、击穿、雪崩的区别和解决方案?
【标准答案】**三者的区别**:
| 问题 | 触发条件 | 危害 |
|------|---------|------|
| **穿透** | 查询不存在的数据(如id=-1) | 请求直接打到数据库 |
| **击穿** | 热点key过期瞬间大量请求 | 数据库瞬时压力暴增 |
| **雪崩** | 大量key同时过期或Redis宕机 | 数据库全面崩溃 |
**解决方案**:
1. **穿透防护**:
- 布隆过滤器:请求前先判断key是否存在(不存在直接返回)
- 空值缓存:查不到也缓存null,设置短TTL(如5分钟)
2. **击穿防护**:
- 互斥锁:只让一个线程查库重建缓存,其他等待
- 逻辑过期:缓存中存过期时间,后台异步刷新
3. **雪崩防护**:
- 过期时间加随机值:避免同时失效(如基础TTL + random(0,300))
- 多级缓存:本地缓存(Caffeine) + Redis,Redis挂了本地兜底
- 熔断降级:Redis不可用时,直接返回默认数据或降级页面
---
#### 【面试题】6. Raft算法的Leader选举过程是怎样的?选举超时为什么要有随机值?
【标准答案】**Leader选举流程**:
1. **初始状态**:所有节点都是Follower,启动选举超时定时器
2. **超时触发**:Follower在选举超时内没收到Leader心跳,转为Candidate
3. **发起投票**:
- Candidate自增term(任期号)
- 给自己投票,向其他节点发RequestVote
4. **投票结果**:
- 获得多数票 → 成为Leader,立即发心跳确立地位
- 收到更高term的投票请求 → 退回Follower
- 超时未获多数票 → term+1,重新选举
**选举超时为什么随机**:
- 防止多个节点同时超时同时成为Candidate,导致票数分散、选举失败
- 随机值(如150-300ms)让一个节点先超时,其他节点大概率投票给它
- 避免选举活锁(反复选举但选不出Leader)
实战注意:选举超时设置要合理(通常150-300ms随机),太短频繁选举,太长故障恢复慢。
---
#### 【面试题】7. Zookeeper的ZNode有哪些类型?临时节点在分布式锁中的作用?
【标准答案】**ZNode四大类型**:
| 类型 | 特点 | 用途 |
|------|------|------|
| **持久节点** | 节点永久存在,需手动删除 | 配置存储、注册中心 |
| **持久顺序节点** | 持久+自动递增序号后缀 | 分布式队列 |
| **临时节点** | 会话断开自动删除 | 分布式锁、服务注册 |
| **临时顺序节点** | 临时+顺序编号 | 公平锁实现 |
**临时节点在分布式锁中的应用**:/lock(持久节点) ├── node-00000001(临时顺序节点,持有者) ├── node-00000002(临时顺序节点,等待者1) └── node-00000003(临时顺序节点,等待者2)
加锁流程:1. 客户端在/lock下创建临时顺序节点
2. 判断自己是否最小序号节点 → 是则获得锁
3. 不是则监听前一个节点的删除事件(只监听前一个,避免惊群效应)
4. 前一个节点释放(删除),收到通知后尝试获取锁
优势:
- 会话断开自动释放锁,避免死锁
- 顺序节点实现公平锁,先到先得
- 只监听前一节点,避免大量watch导致的惊群效应
---
#### 【面试题】8. 分布式限流中令牌桶和漏桶算法有什么区别?为什么推荐令牌桶?
【标准答案】**核心区别**:
| 特性 | 令牌桶 | 漏桶 |
|------|--------|------|
| **流量模式** | 允许一定程度的突发流量 | 严格恒定速率输出 |
| **实现方式** | 定时放令牌,请求消耗令牌 | 请求进入队列,定时匀速消费 |
| **弹性能力** | 有令牌堆积时可以应对突发 | 无弹性,超出队列拒绝 |
| **适用场景** | API网关、业务接口限流 | 流量整形、数据库写入控制 |
**为什么推荐令牌桶**:
- 业务流量本身就是波动的,令牌桶允许短时间内处理更多请求(秒杀场景)
- 空闲时积累令牌,高峰期消耗,资源利用率更高
- 可以灵活控制速率(调整令牌生成速度)
**实战方案**:
- **Guava RateLimiter**:令牌桶实现,支持预热模式(冷启动时慢速放开)
- **Redis + Lua**:分布式令牌桶,保证原子性
- **Sentinel**:支持QPS限流、并发线程数限流、熔断降级一体化
---
#### 【面试题】9. Seata的AT模式和TCC模式有什么区别?AT模式如何保证一致性?
【标准答案】**AT vs TCC对比**:
| 维度 | AT模式 | TCC模式 |
|------|--------|---------|
| **侵入性** | 无侵入,自动代理 | 高侵入,需写三套逻辑 |
| **性能** | 较高,本地事务提交 | 较低,资源被锁定 |
| **隔离级别** | 默认读未提交,可配置读已提交 | 读已提交/可重复读 |
| **回滚方式** | 自动生成反向SQL回滚 | 业务补偿代码回滚 |
| **适用场景** | 简单CRUD、常规业务 | 复杂业务逻辑、资源隔离 |
**AT模式一致性保证机制**:
1. **一阶段提交**:
- 执行业务SQL
- 解析SQL,记录前后镜像到undo_log表
- 本地事务提交(释放锁)
2. **二阶段提交**:
- 删除undo_log(无需额外操作)
3. **二阶段回滚**:
- 从undo_log读取前后镜像
- 校验当前数据是否等于后镜像(防止脏写)
- 用前镜像覆盖当前数据(反向SQL)
- 删除undo_log
**脏写问题处理**:
- 全局锁:修改前先申请全局锁,防止其他事务并发修改
- 校验后镜像:回滚时对比数据是否被修改,被修改则人工介入
---
#### 【面试题】10. SkyWalking链路追踪中TraceId和SpanId如何传递?异步调用如何处理?
【标准答案】**TraceId/SpanId传递机制**:
1. **入口处生成**:请求进入时生成全局唯一的TraceId,创建根Span(SpanId=0)
2. **跨进程传递**:通过HTTP Header传递:
- `sw-trace-id`:链路唯一标识
- `sw-span-id`:当前Span标识
- `sw-parent-span-id`:父Span标识
3. **线程内传递**:使用ThreadLocal存储当前Span,整个调用链共享
4. **跨线程传递**:通过TransmittableThreadLocal(TTL)或手动传递Context
**异步调用处理方案**:
1. **线程池场景**:
```java
// 使用SkyWalking的CallableWrapper包装任务
ExecutorService executor = Executors.newFixedThreadPool(10);
executor.submit(Context.wrap(() -> {
// 异步任务中可以拿到完整的TraceId
// 链路继续追踪
}));消息队列场景:
- 生产者发送消息时,将TraceContext写入消息Header
- 消费者收到消息后,从Header恢复TraceContext
- SkyWalking Agent自动处理Kafka/RabbitMQ的链路串联
@Async注解:
- Spring @Async方法需要手动传递Context,或使用Agent自动增强
最佳实践:
- 避免在异步任务中创建新的Span(会增加调用链复杂度)
- 合理设置采样率(高并发场景建议1%-10%)
- 使用异步刷盘减少性能损耗(Reporter异步发送到Collector)