HiHuo
首页
博客
手册
工具
关于
首页
博客
手册
工具
关于
  • 技术面试完全指南

    • 技术面试完全指南
    • 8年面试官告诉你:90%的简历在第一轮就被刷掉了
    • 刷了500道LeetCode,终于明白大厂算法面试到底考什么
    • 高频算法题精讲-双指针与滑动窗口
    • 03-高频算法题精讲-二分查找与排序
    • 04-高频算法题精讲-树与递归
    • 05-高频算法题精讲-图与拓扑排序
    • 06-高频算法题精讲-动态规划
    • Go面试必问:一道GMP问题,干掉90%的候选人
    • 08-数据库面试高频题
    • 09-分布式系统面试题
    • 10-Kubernetes与云原生面试题
    • 11-系统设计面试方法论
    • 前端面试高频题
    • AI 与机器学习面试题
    • 行为面试与软技能

09-分布式系统面试题

本章导读

分布式系统是大型互联网应用的基础架构,也是面试的重点考察方向。本章涵盖分布式一致性、分布式锁、CAP定理、服务发现等核心知识点,包含40+道经典面试题,结合实际案例深入剖析分布式系统的设计与实现。

主要内容:

  • 分布式理论基础(CAP、BASE)
  • 分布式一致性算法(Paxos、Raft)
  • 分布式锁与协调
  • 分布式事务解决方案
  • 服务发现与注册

第一部分:分布式理论基础

1. CAP定理是什么?如何理解?

考察点: 分布式系统基础理论

详细解答:

CAP定理定义:

一个分布式系统不可能同时满足以下三个特性:

  • C - Consistency(一致性): 所有节点在同一时间看到相同的数据
  • A - Availability(可用性): 每个请求都能得到响应(成功或失败)
  • P - Partition Tolerance(分区容错性): 系统在网络分区时仍能继续工作

核心结论: 在分布式系统中,P是必须保证的,因此只能在C和A之间做权衡,即CP或AP。


详细解释:

一致性(Consistency):

定义:所有节点在同一时刻的数据是一致的

示例:
客户端写入数据到节点A:x = 1
立即从节点B读取:期望得到 x = 1

如果节点B返回 x = 0(旧值),则不满足一致性

可用性(Availability):

定义:系统总是返回结果,不会出现超时或拒绝服务

示例:
客户端发送请求到节点A
即使节点A与其他节点失联,仍需返回结果(可能是旧数据)

如果节点A拒绝服务或超时,则不满足可用性

分区容错性(Partition Tolerance):

定义:系统在网络分区(部分节点无法通信)时仍能运行

示例:
网络故障导致节点A和节点B无法通信
系统仍能继续提供服务(可能牺牲一致性或可用性)

如果系统停止服务,则不满足分区容错性

为什么只能三选二:

场景分析:

初始状态:
节点A:x = 0
节点B:x = 0
节点AB正常通信

操作1:客户端写入x=1到节点A
  节点A:x = 1
  节点B:x = 0(尚未同步)

网络分区发生,节点A和B失联

操作2:客户端读取x从节点B

此时面临选择:

选择1:返回 x = 0(旧值)
   A(可用性):返回了结果
   P(分区容错性):网络分区时仍提供服务
  ✗ C(一致性):返回的是旧数据

选择2:拒绝返回,等待网络恢复
  ✗ A(可用性):拒绝了请求
   P(分区容错性):网络分区时仍在运行
   C(一致性):不返回不一致的数据

选择3:不考虑网络分区(不现实)
  ✗ P(分区容错性):网络分区时系统停止
   A(可用性):正常时可用
   C(一致性):正常时数据一致

典型系统分类:

1. CP系统(一致性 + 分区容错性)

特点:
- 网络分区时,牺牲可用性,拒绝服务
- 保证数据强一致性

典型系统:
- Zookeeper
- HBase
- Redis Cluster(部分场景)
- Consul

应用场景:
- 配置中心:需要保证配置的一致性
- 分布式锁:需要保证锁的唯一性
- 元数据存储:需要保证元数据准确性

示例:Zookeeper
当网络分区导致无法达成多数派时:
- 少数派节点拒绝写入请求(保证一致性)
- 牺牲了可用性

2. AP系统(可用性 + 分区容错性)

特点:
- 网络分区时,保持可用性
- 接受最终一致性

典型系统:
- Cassandra
- DynamoDB
- Eureka
- CouchDB

应用场景:
- 用户评论:允许短暂不一致
- 购物车:允许不同节点看到不同状态
- 日志收集:允许数据延迟同步

示例:Cassandra
当网络分区发生时:
- 各分区独立提供服务(保证可用性)
- 分区恢复后通过冲突解决机制达到最终一致性
- 牺牲了强一致性

3. CA系统(一致性 + 可用性,不考虑分区容错)

特点:
- 单机系统或局域网系统
- 不考虑网络分区(实际不存在)

典型系统:
- 单机数据库(MySQL单机)
- 传统RDBMS集群(同机房)

限制:
- 只适用于不会发生网络分区的环境
- 在分布式系统中不现实

实际案例分析:

案例1:电商订单系统

需求分析:
- 订单数据必须准确(不能出现重复扣款)
- 允许短暂不可用(用户可以稍后重试)

选择:CP系统

架构设计:
- 使用MySQL主从 + 半同步复制
- 主库故障时,等待从库提升为主库(可能短暂不可用)
- 保证订单数据强一致性

配置:
MySQL半同步复制:
  rpl_semi_sync_master_enabled = 1
  rpl_semi_sync_slave_enabled = 1
  rpl_semi_sync_master_wait_for_slave_count = 1
  # 至少1个从库确认后才返回客户端

权衡:
 数据强一致性(不会重复扣款)
✗ 主库故障时短暂不可用(可接受)

案例2:社交媒体点赞功能

需求分析:
- 点赞数不需要绝对准确(允许短暂不一致)
- 必须高可用(用户点赞不能失败)

选择:AP系统

架构设计:
- 使用Cassandra存储点赞数据
- 多数据中心部署,本地读写
- 异步同步到其他数据中心

配置:
Cassandra配置:
  replication_factor = 3
  consistency_level = ONE
  # 写入一个节点即返回成功

权衡:
 高可用性(任何时候都能点赞)
✗ 短暂数据不一致(不同地区看到的点赞数可能不同)
 最终一致性(数据最终会同步)

案例3:微服务注册中心

需求分析:
- 服务注册信息必须高可用
- 允许短暂不一致(服务发现可以容忍)

选择:AP系统

架构设计:
- 使用Eureka作为注册中心
- 每个数据中心部署独立Eureka集群
- 集群间异步同步

Eureka特点:
- 优先保证可用性
- 网络分区时各节点独立提供服务
- 分区恢复后自动同步

权衡:
 高可用性(服务注册/发现不中断)
✗ 短暂数据不一致(不同节点看到的服务列表可能不同)
 最终一致性(服务列表最终同步)

总结:

特性CP系统AP系统
一致性强一致性最终一致性
可用性分区时部分不可用始终可用
适用场景金融、交易、配置社交、监控、注册
典型系统Zookeeper、HBaseCassandra、Eureka
权衡策略宁可不可用,不能数据错宁可数据短暂不一致,不能不可用

2. BASE理论是什么?与ACID有什么区别?

考察点: 分布式系统设计思想

详细解答:

BASE理论定义:

BASE是对CAP中一致性和可用性权衡的结果,是以下三个词的缩写:

  • BA - Basically Available(基本可用)
  • S - Soft state(软状态)
  • E - Eventually consistent(最终一致性)

BASE详细解释:

1. Basically Available(基本可用)

定义:系统在出现故障时,允许损失部分可用性,但核心功能仍可用

示例1:响应时间损失
正常情况:查询响应时间 0.5秒
故障情况:查询响应时间 2秒(仍可用,但变慢)

示例2:功能降级
正常情况:显示个性化推荐
故障情况:显示通用推荐(功能降级)

示例3:部分服务不可用
正常情况:所有功能可用
故障情况:非核心功能暂时关闭(如评论功能)

2. Soft state(软状态)

定义:允许系统中的数据存在中间状态,该状态不影响系统可用性

示例1:数据同步中间态
节点A:库存 = 100
节点B:库存 = 99(同步中)
节点C:库存 = 99(同步中)

中间状态:各节点数据不一致,但系统仍可提供服务

示例2:缓存数据
Redis缓存:user_name = "Alice"(可能过期)
数据库:user_name = "Alice Updated"
中间状态:缓存和数据库数据不一致

3. Eventually consistent(最终一致性)

定义:系统不保证数据实时一致,但保证在一定时间后数据最终一致

示例:
T1: 用户在节点A修改密码
    节点A:password = "new_password"
    节点B:password = "old_password"(尚未同步)

T2: 数据同步中
    节点B开始同步

T3: 同步完成(最终一致)
    节点A:password = "new_password"
    节点B:password = "new_password"

时间窗口:T1到T3之间,数据不一致,但可接受

ACID vs BASE对比:

特性ACIDBASE
一致性强一致性最终一致性
隔离性严格隔离允许中间状态
可用性可能牺牲可用性优先保证可用性
适用场景单机数据库、关键交易分布式系统、大规模应用
性能相对较低相对较高
复杂度相对简单相对复杂

示例对比:

-- ACID示例:银行转账

-- 事务开始
BEGIN TRANSACTION;

-- 扣减账户A
UPDATE accounts SET balance = balance - 100 WHERE id = 1;

-- 增加账户B
UPDATE accounts SET balance = balance + 100 WHERE id = 2;

-- 提交事务(要么全成功,要么全失败)
COMMIT;

-- 特点:
--  强一致性:任何时刻查询,两账户总额不变
--  原子性:全成功或全失败
-- ✗ 可能锁表,影响并发性能
# BASE示例:电商库存扣减

# Step 1: 预扣库存(允许中间状态)
redis.decr("stock:item_123")
# 库存立即减少,订单尚未创建(软状态)

# Step 2: 创建订单(异步)
order_service.create_order(item_id, user_id)

# Step 3: 如果订单创建失败,补偿操作
if not order_created:
    redis.incr("stock:item_123")  # 恢复库存

# 特点:
#  高可用:库存扣减立即返回
#  最终一致性:订单失败时恢复库存
# ✗ 中间状态:库存已扣减但订单未创建

实际应用案例:

案例1:微博发帖与推送

业务流程(BASE模式):

T1: 用户发布微博
    - 写入作者的微博表(立即成功)
    - 返回用户"发布成功"
    状态:发布成功,但粉丝尚未看到(软状态)

T2: 异步推送给粉丝
    - 消息队列通知推送服务
    - 逐步推送到粉丝的时间线

T3: 推送完成
    - 所有粉丝都能看到微博(最终一致)

优点:
 发布微博立即成功(基本可用)
 不阻塞用户(高性能)
 粉丝最终能看到(最终一致性)

如果使用ACID:
- 发布微博时,必须等待所有粉丝时间线更新完成
- 如果有100万粉丝,可能需要等待数十秒
- 用户体验差

案例2:电商订单与库存

业务流程(BASE模式):

T1: 用户下单
    - 预扣减库存(Redis)
    - 创建订单(MySQL)
    - 发送消息到订单处理队列
    - 返回"订单提交成功"
    状态:订单已创建,但未支付(软状态)

T2: 用户支付
    - 调用支付服务
    - 支付成功后,更新订单状态
    状态:订单已支付,库存已扣减(软状态)

T3: 异步扣减数据库库存
    - 消费消息队列
    - 更新MySQL库存
    状态:所有数据一致(最终一致)

T4: 如果30分钟未支付,释放库存
    - 定时任务检查未支付订单
    - 恢复库存(补偿机制)

优点:
 下单响应快(秒级)
 高并发支持(Redis性能高)
 最终一致(定时任务保证)

如果使用ACID:
- 下单时必须锁定数据库库存
- 高并发时数据库压力大
- 可能导致性能瓶颈

案例3:分布式缓存更新

业务流程(BASE模式):

T1: 更新数据库
    UPDATE users SET name = 'Alice Updated' WHERE id = 123;
    状态:数据库已更新,缓存未更新(软状态)

T2: 删除缓存(Cache-Aside模式)
    redis.del("user:123");
    状态:缓存已删除,等待下次查询重建

T3: 下次查询时重建缓存
    SELECT * FROM users WHERE id = 123;
    redis.set("user:123", data, "EX", 3600);
    状态:缓存和数据库一致(最终一致)

时间窗口:T1到T3之间,可能读取到旧缓存

优化:延迟双删
T1: 更新数据库
T2: 删除缓存
T3: 等待500ms(等待可能的缓存重建)
T4: 再次删除缓存

优点:
 高性能(缓存命中率高)
 最终一致(缓存最终更新)

如果使用ACID:
- 必须使用分布式事务保证缓存和数据库同时更新
- 性能开销大
- 实现复杂

如何选择ACID还是BASE:

场景推荐模式理由
银行转账ACID数据绝对不能错
订单支付ACID金额必须准确
库存扣减BASE允许预扣后补偿
用户发帖BASE允许异步推送
配置更新ACID配置必须一致
日志收集BASE允许延迟同步

第二部分:分布式一致性算法

3. Paxos算法的核心思想是什么?

考察点: 分布式一致性理论

详细解答:

Paxos算法简介:

Paxos是分布式系统中最经典的一致性算法,用于在多个节点之间达成共识(Agreement),即使存在节点故障或消息丢失,也能保证一致性。

核心目标: 多个节点对某个值达成一致。


角色定义:

1. Proposer(提议者)
   - 提出提案(Proposal)
   - 包含提案编号(Proposal Number)和提案值(Value)

2. Acceptor(接受者)
   - 接受或拒绝提案
   - 需要多数派(Quorum)接受提案才能通过

3. Learner(学习者)
   - 学习已通过的提案
   - 不参与提案决策

注:一个节点可以同时担任多个角色

算法流程(Basic Paxos):

Phase 1: Prepare阶段

Proposer行为:
1. 选择一个提案编号n
2. 向多数派Acceptor发送Prepare(n)请求

Acceptor行为:
1. 如果n大于已见过的所有提案编号:
   - 承诺不再接受编号小于n的提案
   - 返回已接受的最大编号提案(如果有)
2. 否则:
   - 拒绝该请求

Phase 2: Accept阶段

Proposer行为:
1. 如果收到多数派Acceptor的响应:
   - 如果响应中有已接受的提案,选择编号最大的提案的值
   - 否则,选择自己的值
   - 向多数派Acceptor发送Accept(n, value)请求
2. 否则:
   - 提案失败,重新开始(使用更大的编号)

Acceptor行为:
1. 如果n >= 承诺的最小编号:
   - 接受提案(n, value)
   - 返回成功
2. 否则:
   - 拒绝该请求

Learner行为:
- 当多数派Acceptor接受提案后,学习该值

示例场景:

场景:3个节点(A、B、C)对一个值达成一致

初始状态:
A、B、C都未接受任何提案

轮次1:Proposer1提议value=X

Step 1: Prepare阶段
Proposer1 -> A, B, C: Prepare(n=1)

A响应:OK(承诺不接受n<1的提案)
B响应:OK
C响应:OK(但消息丢失)

Proposer1收到多数派响应(A、B),进入Accept阶段

Step 2: Accept阶段
Proposer1 -> A, B: Accept(n=1, value=X)

A接受:(n=1, value=X)
B接受:(n=1, value=X)

结论:value=X达成共识

---

轮次2:Proposer2并发提议value=Y(冲突场景)

Step 1: Prepare阶段
Proposer2 -> A, B, C: Prepare(n=2)

A响应:OK,已接受(n=1, value=X)
B响应:OK,已接受(n=1, value=X)
C响应:OK

Proposer2收到多数派响应,发现已有提案value=X

Step 2: Accept阶段
Proposer2必须采用已有的value=X(不能使用Y)
Proposer2 -> A, B, C: Accept(n=2, value=X)

A接受:(n=2, value=X)
B接受:(n=2, value=X)
C接受:(n=2, value=X)

结论:仍然是value=X,保持一致性

---

关键点:
- Proposer2不能提议value=Y,必须采用已接受的value=X
- 这保证了一旦value=X被多数派接受,后续提案都是X

Paxos的关键性质:

1. Safety(安全性)
   - 只有一个值会被选定
   - 只有被提议的值才能被选定
   - 只有被多数派接受的值才能被学习

2. Liveness(活性)
   - 最终会有一个值被选定(在没有持续冲突的情况下)

3. Fault Tolerance(容错性)
   - 允许少于半数节点故障
   - 允许消息丢失和乱序

Multi-Paxos优化:

Basic Paxos每次决策都需要两轮通信,效率较低。Multi-Paxos优化如下:

优化思路:选举一个Leader,由Leader统一提交提案

流程:
1. 选举Leader(使用Basic Paxos选举)
2. Leader在任期内提交提案,只需Accept阶段(省略Prepare)
3. Follower直接接受Leader的提案

优点:
- 减少通信轮次(一轮即可)
- 提高吞吐量
- 简化冲突处理

应用:
- Chubby(Google)
- Zookeeper的ZAB协议(类似Multi-Paxos)

实际应用:

1. Google Chubby
   - 分布式锁服务
   - 使用Paxos保证锁的一致性

2. Apache Zookeeper
   - ZAB协议(类似Multi-Paxos)
   - 配置管理、分布式协调

3. etcd
   - Raft算法(Paxos的简化版本)
   - Kubernetes的元数据存储

4. Raft算法与Paxos有什么区别?

考察点: 分布式一致性算法对比

详细解答:

Raft算法简介:

Raft是Paxos的简化版本,设计目标是"易于理解"(Understandability),将一致性问题分解为三个子问题:

  1. Leader Election(领导选举)
  2. Log Replication(日志复制)
  3. Safety(安全性)

Raft核心概念:

角色:

1. Leader(领导者)
   - 处理所有客户端请求
   - 向Follower复制日志
   - 一个Term内只有一个Leader

2. Follower(跟随者)
   - 被动接收Leader的日志
   - 响应Leader和Candidate的请求
   - 如果超时未收到Leader心跳,转为Candidate

3. Candidate(候选者)
   - 发起选举
   - 尝试成为Leader

Term(任期):

时间被划分为Term(任期),每个Term最多一个Leader

Term 1: Leader A
Term 2: Leader B(A故障,重新选举)
Term 3: Leader C

每个Term由选举开始:
- 如果选举成功,进入正常状态(一个Leader,多个Follower)
- 如果选举失败(平票),开始下一个Term重新选举

Raft流程详解:

1. Leader Election(领导选举)

触发条件:
- Follower超时未收到Leader心跳(election timeout: 150-300ms)

选举流程:
Step 1: Follower转为Candidate
  - 增加当前Term
  - 投票给自己
  - 向其他节点发送RequestVote RPC

Step 2: 其他节点投票
  - 如果Candidate的日志至少和自己一样新,且本Term未投票,则投票
  - 每个Term每个节点只能投一票

Step 3: 选举结果
  - 收到多数派投票 → 成为Leader
  - 收到其他Leader的心跳 → 转为Follower
  - 超时无结果 → 开始下一个Term重新选举

示例:
节点:A, B, C, D, E(5个节点)

T1: A超时,转为Candidate,Term=1
    A -> B, C, D, E: RequestVote(term=1)

T2: B, C, D收到请求,投票给A
    A收到3票(包括自己),成为Leader

T3: A向所有节点发送心跳
    B, C, D, E转为Follower

2. Log Replication(日志复制)

流程:
Step 1: 客户端发送请求到Leader
  Client -> Leader: SET x=5

Step 2: Leader追加日志条目
  Leader日志:[term=1, index=1, cmd="SET x=5"]

Step 3: Leader向Follower发送AppendEntries RPC
  Leader -> Followers: AppendEntries(term=1, entries=[...])

Step 4: Follower追加日志
  Follower日志:[term=1, index=1, cmd="SET x=5"]
  返回成功

Step 5: Leader收到多数派确认
  多数派已复制,标记日志为committed

Step 6: Leader应用日志到状态机
  执行命令:SET x=5
  返回客户端成功

Step 7: Leader通过心跳通知Follower提交日志
  Follower应用日志到状态机

日志结构:
[term, index, command]
[1, 1, "SET x=5"]
[1, 2, "SET y=10"]
[2, 3, "SET z=15"]

3. Safety(安全性)

关键约束:

1. Election Safety(选举安全性)
   一个Term内最多一个Leader

2. Leader Append-Only(Leader只追加)
   Leader不会覆盖或删除日志,只追加

3. Log Matching(日志匹配)
   如果两个日志条目有相同的term和index,则:
   - 它们包含相同的命令
   - 它们之前的所有日志条目都相同

4. Leader Completeness(Leader完整性)
   如果某个日志条目在某个Term被提交,则所有后续Term的Leader都包含该日志

5. State Machine Safety(状态机安全性)
   如果某节点已应用某个日志条目到状态机,其他节点不会应用不同的日志条目

实现:
- 投票限制:只投给日志至少和自己一样新的Candidate
- 日志新旧判断:
  1. 最后一条日志的term更大,则更新
  2. term相同,index更大则更新

Raft vs Paxos对比:

特性RaftPaxos
可理解性易于理解(分解为子问题)难以理解(证明复杂)
Leader强Leader(一个Leader处理所有请求)无固定Leader(Multi-Paxos优化后有Leader)
日志结构连续的日志序列,无空洞可能有空洞
选举明确的选举流程隐式选举(通过提案编号)
实现复杂度相对简单相对复杂
工程应用etcd, Consul, TiKVChubby, Spanner

实际案例:etcd使用Raft

// etcd客户端写入数据

client.Put(ctx, "key", "value")

// etcd内部流程(Raft):

// 1. 客户端请求到达Leader
// Leader: etcd-node-1

// 2. Leader追加日志
log = {term: 5, index: 100, cmd: "PUT key value"}

// 3. Leader复制日志到Follower
Leader -> Follower1: AppendEntries(term=5, entries=[log])
Leader -> Follower2: AppendEntries(term=5, entries=[log])

// 4. Follower返回成功
Follower1 -> Leader: Success
Follower2 -> Leader: Success

// 5. Leader提交日志(多数派已复制)
committed_index = 100

// 6. Leader应用到状态机
state_machine["key"] = "value"

// 7. 返回客户端成功
Client <- Leader: Success

// 8. Leader通过心跳通知Follower提交
Leader -> Followers: AppendEntries(committed_index=100)

// 9. Follower应用到状态机
state_machine["key"] = "value"

选择建议:

选择Raft的场景:
- 团队对算法理解要求高
- 需要快速实现和维护
- 开源工具支持(etcd, Consul)
- 推荐用于大多数场景

选择Paxos的场景:
- 已有成熟实现(如Zookeeper的ZAB)
- 需要极致性能优化
- 团队有深厚的分布式理论基础

第三部分:分布式锁

5. 如何实现分布式锁?有哪些方案?

考察点: 分布式协调机制

详细解答:

分布式锁的要求:

1. 互斥性(Mutual Exclusion)
   任何时刻,只有一个客户端能持有锁

2. 避免死锁(Deadlock Free)
   即使持有锁的客户端崩溃,其他客户端也能获取锁

3. 容错性(Fault Tolerance)
   只要多数节点正常,就能获取和释放锁

4. 性能(Performance)
   获取和释放锁的开销要低

方案1:基于Redis的分布式锁

基本实现(SETNX + EXPIRE):

# 加锁(错误示例)
SETNX lock_key my_random_value
EXPIRE lock_key 30SETNX和EXPIRE不是原子操作
# 如果SETNX成功后进程崩溃,锁永不过期

# 正确实现(原子操作)
SET lock_key my_random_value NX EX 30
# NX:仅在key不存在时设置
# EX:过期时间(秒)

# 解锁(Lua脚本保证原子性)
local value = redis.call('GET', KEYS[1])
if value == ARGV[1] then
    return redis.call('DEL', KEYS[1])
else
    return 0
end

Java实现示例:

import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;

public class RedisDistributedLock {
    private Jedis jedis;
    private String lockKey;
    private String lockValue;
    private int expireTime = 30; // 秒

    public boolean tryLock() {
        lockValue = UUID.randomUUID().toString();
        SetParams params = SetParams.setParams().nx().ex(expireTime);
        String result = jedis.set(lockKey, lockValue, params);
        return "OK".equals(result);
    }

    public void unlock() {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                        "return redis.call('del', KEYS[1]) else return 0 end";
        jedis.eval(script, Collections.singletonList(lockKey),
                   Collections.singletonList(lockValue));
    }

    public void lockWithRetry(int retryTimes, long retryInterval)
            throws InterruptedException {
        for (int i = 0; i < retryTimes; i++) {
            if (tryLock()) {
                return;
            }
            Thread.sleep(retryInterval);
        }
        throw new RuntimeException("Failed to acquire lock");
    }
}

// 使用示例
try {
    lock.lockWithRetry(10, 100); // 重试10次,间隔100ms
    // 执行业务逻辑
    processOrder();
} finally {
    lock.unlock();
}

Redis分布式锁的问题:

问题1:锁过期时间难以设置
场景:
T1: 客户端A获取锁,设置30秒过期
T2: 业务处理超时(35秒)
T3: 30秒后锁自动释放
T4: 客户端B获取锁
T5: 客户端A处理完成,释放锁(错误释放了B的锁)

解决:
1. 使用UUID标识锁,释放时检查
2. 使用看门狗(Watchdog)自动续期
3. 设置足够长的过期时间

问题2:主从切换导致锁失效
场景:
T1: 客户端A在Master获取锁
T2: Master崩溃,Slave提升为新Master
T3: 新Master没有锁数据(异步复制未完成)
T4: 客户端B在新Master获取到同一把锁
结果:A和B同时持有锁,违反互斥性

解决:
使用Redlock算法(见下文)

方案2:Redlock算法(Redis官方推荐)

原理:

在多个独立的Redis实例上获取锁(通常5个)

加锁流程:
1. 获取当前时间戳T1
2. 依次在N个Redis实例上尝试获取锁
   - 每个实例设置较短的超时时间(远小于锁的总超时时间)
   - 例如:锁总超时10秒,单个实例超时5-50毫秒
3. 计算获取锁的耗时:T2 - T1
4. 如果在多数实例上成功(N/2+1),且耗时 < 锁的有效时间,则成功
5. 锁的实际有效时间 = 初始有效时间 - 获取锁的耗时 - 时钟漂移
6. 如果获取失败,释放所有实例上的锁

解锁流程:
- 在所有实例上释放锁(不管成功与否)

示例:

import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;

public class RedlockExample {
    public static void main(String[] args) {
        // 配置多个Redis实例
        Config config1 = new Config();
        config1.useSingleServer().setAddress("redis://127.0.0.1:6379");

        Config config2 = new Config();
        config2.useSingleServer().setAddress("redis://127.0.0.1:6380");

        Config config3 = new Config();
        config3.useSingleServer().setAddress("redis://127.0.0.1:6381");

        RedissonClient client1 = Redisson.create(config1);
        RedissonClient client2 = Redisson.create(config2);
        RedissonClient client3 = Redisson.create(config3);

        // 创建Redlock
        RLock lock1 = client1.getLock("myLock");
        RLock lock2 = client2.getLock("myLock");
        RLock lock3 = client3.getLock("myLock");

        RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);

        try {
            // 加锁(等待时间100ms,锁自动释放时间10s)
            boolean isLocked = redLock.tryLock(100, 10000, TimeUnit.MILLISECONDS);
            if (isLocked) {
                // 执行业务逻辑
                processOrder();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            redLock.unlock();
        }
    }
}

Redlock的争议:

Martin Kleppmann(分布式系统专家)的批评:
1. 时钟同步问题:依赖系统时钟,时钟漂移可能导致安全性问题
2. GC暂停问题:长时间GC暂停可能导致锁被其他客户端获取
3. 不如使用基于共识的分布式锁(如Zookeeper)

Redlock适用场景:
- 性能要求高
- 可以容忍极小概率的锁失效
- 不涉及强一致性要求的业务(如限流、去重)

不适用场景:
- 强一致性要求(如金融交易)
- 对正确性要求极高的场景

方案3:基于Zookeeper的分布式锁

原理:

利用Zookeeper的临时顺序节点(Ephemeral Sequential Node)

加锁流程:
1. 在/locks目录下创建临时顺序节点,如/locks/lock-0000000001
2. 获取/locks下所有子节点,排序
3. 如果自己的节点序号最小,获取锁成功
4. 否则,监听前一个节点的删除事件,等待锁释放

解锁流程:
- 删除自己创建的节点
- 下一个节点收到通知,获取锁

优势:
- 天然避免惊群效应(每个节点只监听前一个节点)
- 会话失效自动释放锁(临时节点)
- 基于ZAB协议,强一致性

Java实现(Curator框架):

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;

public class ZookeeperLockExample {
    public static void main(String[] args) throws Exception {
        // 创建Zookeeper客户端
        CuratorFramework client = CuratorFrameworkFactory.builder()
                .connectString("localhost:2181")
                .sessionTimeoutMs(5000)
                .retryPolicy(new ExponentialBackoffRetry(1000, 3))
                .build();
        client.start();

        // 创建分布式锁
        InterProcessMutex lock = new InterProcessMutex(client, "/locks/mylock");

        try {
            // 加锁(等待最多10秒)
            if (lock.acquire(10, TimeUnit.SECONDS)) {
                // 执行业务逻辑
                processOrder();
            } else {
                throw new RuntimeException("Failed to acquire lock");
            }
        } finally {
            // 释放锁
            lock.release();
        }

        client.close();
    }
}

Zookeeper锁的详细流程:

节点结构:
/locks
  ├── lock-0000000001 (临时顺序节点,客户端A)
  ├── lock-0000000002 (临时顺序节点,客户端B)
  └── lock-0000000003 (临时顺序节点,客户端C)

时间线:
T1: 客户端A创建/locks/lock-0000000001
    查询所有子节点,发现自己最小,获取锁成功

T2: 客户端B创建/locks/lock-0000000002
    查询所有子节点,发现lock-0000000001更小
    监听lock-0000000001的删除事件

T3: 客户端C创建/locks/lock-0000000003
    查询所有子节点,发现lock-0000000002更小
    监听lock-0000000002的删除事件

T4: 客户端A释放锁,删除lock-0000000001
    客户端B收到通知,获取锁成功

T5: 客户端B释放锁,删除lock-0000000002
    客户端C收到通知,获取锁成功

优势:
- 避免惊群:只有下一个节点收到通知(不是所有等待者)
- 公平锁:按照创建顺序获取锁(FIFO)
- 自动释放:客户端崩溃,临时节点自动删除

方案对比:

特性Redis锁RedlockZookeeper锁
性能高中中低
可靠性低(单点)中(多实例)高(基于Paxos/ZAB)
实现复杂度低中低(使用Curator)
惊群效应有有无
自动释放基于过期时间基于过期时间基于会话
适用场景高性能、弱一致性中等性能、中等可靠性强一致性、高可靠性

选择建议:

1. 高并发、对一致性要求不高(如限流、去重)
   → Redis单机锁

2. 中等并发、需要更高可靠性
   → Redlock或Zookeeper锁

3. 强一致性要求(如金融交易、库存扣减)
   → Zookeeper锁或etcd锁

4. 已有Zookeeper/etcd基础设施
   → 直接使用Zookeeper/etcd锁

5. 性能优先
   → Redis锁 + 业务层补偿机制

第四部分:分布式事务

6. 分布式事务有哪些解决方案?

考察点: 分布式系统数据一致性

详细解答:

分布式事务的挑战:

单机事务:
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;

分布式事务(跨多个数据库/服务):
Service A: UPDATE accounts SET balance = balance - 100 WHERE id = 1;
Service B: UPDATE orders SET status = 'paid' WHERE id = 123;
Service C: UPDATE inventory SET stock = stock - 1 WHERE item_id = 456;

问题:
- 如何保证三个操作要么全成功,要么全失败?
- 如果Service B成功,Service C失败,如何回滚Service A?

方案1:2PC(Two-Phase Commit,两阶段提交)

角色:

  • 协调者(Coordinator): 事务管理器
  • 参与者(Participant): 资源管理器(数据库)

流程:

Phase 1: 准备阶段(Prepare)

Coordinator行为:
1. 发送Prepare请求给所有Participant
2. 等待所有Participant响应

Participant行为:
1. 执行事务操作(不提交)
2. 写undo和redo日志
3. 如果成功,返回Yes;如果失败,返回No

示例:
Coordinator -> Participant A: Prepare
Participant A执行:UPDATE accounts SET balance = balance - 100 WHERE id = 1
Participant A写入日志,锁定资源
Participant A -> Coordinator: Yes

Coordinator -> Participant B: Prepare
Participant B执行:UPDATE orders SET status = 'paid' WHERE id = 123
Participant B写入日志,锁定资源
Participant B -> Coordinator: Yes

Phase 2: 提交阶段(Commit)

如果所有Participant返回Yes:

Coordinator行为:
1. 发送Commit请求给所有Participant

Participant行为:
1. 提交事务
2. 释放资源
3. 返回ACK

示例:
Coordinator -> Participant A, B: Commit
Participant A提交事务,释放锁
Participant B提交事务,释放锁
Participant A, B -> Coordinator: ACK

---

如果任一Participant返回No:

Coordinator行为:
1. 发送Rollback请求给所有Participant

Participant行为:
1. 回滚事务
2. 释放资源
3. 返回ACK

示例:
Coordinator -> Participant A, B: Rollback
Participant A回滚事务,释放锁
Participant B回滚事务,释放锁
Participant A, B -> Coordinator: ACK

2PC的问题:

问题1:同步阻塞
- 准备阶段,参与者锁定资源,等待协调者指令
- 锁定时间长,影响并发性能

问题2:单点故障
- 如果协调者故障,参与者一直阻塞
- 无法决定提交还是回滚

问题3:数据不一致
- 提交阶段,如果协调者发送Commit后崩溃
- 部分参与者收到Commit(提交),部分未收到(超时回滚)
- 导致数据不一致

问题4:保守
- 任一参与者故障,整个事务回滚
- 可用性低

适用场景:

- 传统企业应用(EJB、JTA)
- 对一致性要求极高,可容忍性能损失
- 参与者数量少(2-3个)
- 网络稳定,故障率低

方案2:3PC(Three-Phase Commit,三阶段提交)

改进: 在2PC基础上增加CanCommit阶段,减少阻塞时间。

流程:

Phase 1: CanCommit
Coordinator -> Participant: CanCommit?
Participant检查资源是否可用,返回Yes/No

Phase 2: PreCommit
如果所有Participant返回Yes:
  Coordinator -> Participant: PreCommit
  Participant执行事务,写日志,锁资源,返回ACK
否则:
  Coordinator -> Participant: Abort

Phase 3: DoCommit
如果所有Participant返回ACK:
  Coordinator -> Participant: DoCommit
  Participant提交事务,释放资源
否则:
  Coordinator -> Participant: Abort

改进:
- 超时机制:如果Participant超时未收到Coordinator消息,自动提交
- 减少了阻塞时间(CanCommit阶段不锁资源)

缺点:
- 仍存在数据不一致风险(网络分区)
- 实现复杂,实际应用较少

方案3:TCC(Try-Confirm-Cancel)

原理: 业务层实现的补偿型事务

三个阶段:

1. Try(尝试)
   - 预留资源,不提交
   - 检查业务规则

2. Confirm(确认)
   - 提交资源
   - 完成业务逻辑

3. Cancel(取消)
   - 释放资源
   - 回滚业务逻辑

示例:转账业务

// Try阶段:预留资金
public boolean tryTransfer(String fromAccount, String toAccount, BigDecimal amount) {
    // 冻结fromAccount的金额
    accountDao.freeze(fromAccount, amount);

    // 记录预留资源
    tccLogDao.insert(new TccLog(fromAccount, toAccount, amount, "TRY"));

    return true;
}

// Confirm阶段:确认转账
public void confirmTransfer(String fromAccount, String toAccount, BigDecimal amount) {
    // 扣减fromAccount的冻结金额
    accountDao.deduct(fromAccount, amount);

    // 增加toAccount的金额
    accountDao.add(toAccount, amount);

    // 更新状态
    tccLogDao.updateStatus(fromAccount, toAccount, "CONFIRM");
}

// Cancel阶段:取消转账
public void cancelTransfer(String fromAccount, String toAccount, BigDecimal amount) {
    // 解冻fromAccount的金额
    accountDao.unfreeze(fromAccount, amount);

    // 更新状态
    tccLogDao.updateStatus(fromAccount, toAccount, "CANCEL");
}

// 协调器调用
public void transfer(String fromAccount, String toAccount, BigDecimal amount) {
    try {
        // Try
        boolean result1 = tryTransfer(fromAccount, toAccount, amount);
        boolean result2 = otherService.tryOtherOperation();

        if (result1 && result2) {
            // Confirm
            confirmTransfer(fromAccount, toAccount, amount);
            otherService.confirmOtherOperation();
        } else {
            // Cancel
            cancelTransfer(fromAccount, toAccount, amount);
            otherService.cancelOtherOperation();
        }
    } catch (Exception e) {
        // Cancel
        cancelTransfer(fromAccount, toAccount, amount);
        otherService.cancelOtherOperation();
    }
}

TCC的优缺点:

优点:
- 性能高(不长时间锁定资源)
- 灵活(业务自定义补偿逻辑)
- 最终一致性

缺点:
- 实现复杂(需要实现Try、Confirm、Cancel三个接口)
- 对业务侵入性强
- 需要考虑空回滚、幂等、悬挂等问题

空回滚:
  Try阶段因网络超时未执行,但协调器认为失败,调用Cancel
  解决:Cancel时检查是否有Try记录

幂等:
  Confirm或Cancel可能被多次调用
  解决:记录状态,避免重复执行

悬挂:
  Cancel先执行,Try后执行(网络延迟)
  解决:Cancel时标记状态,Try时检查状态

方案4:SAGA(长事务补偿)

原理: 将分布式事务拆分为本地事务,每个本地事务有对应的补偿事务。

两种实现方式:

1. 协同式(Choreography):

事件驱动,每个服务监听事件并触发下一步

流程:
Step 1: Order Service创建订单 → 发布OrderCreated事件
Step 2: Payment Service监听事件 → 扣款 → 发布PaymentCompleted事件
Step 3: Inventory Service监听事件 → 扣库存 → 发布InventoryDeducted事件
Step 4: 完成

如果失败:
Step 1: Order Service创建订单 → 发布OrderCreated事件
Step 2: Payment Service扣款失败 → 发布PaymentFailed事件
Step 3: Order Service监听事件 → 取消订单(补偿)

2. 编排式(Orchestration):

由协调器统一调度

流程:
Saga Coordinator:
  1. 调用Order Service: createOrder()
  2. 调用Payment Service: deductPayment()
  3. 调用Inventory Service: deductInventory()
  4. 完成

如果失败(Inventory Service失败):
  1. 调用Inventory Service: compensateInventory()(补偿)
  2. 调用Payment Service: refundPayment()(补偿)
  3. 调用Order Service: cancelOrder()(补偿)

补偿顺序:逆序执行

Java示例(Seata SAGA):

{
  "Name": "OrderSaga",
  "States": [
    {
      "Name": "CreateOrder",
      "Type": "ServiceTask",
      "ServiceName": "orderService",
      "ServiceMethod": "createOrder",
      "CompensateMethod": "cancelOrder",
      "Next": "DeductPayment"
    },
    {
      "Name": "DeductPayment",
      "Type": "ServiceTask",
      "ServiceName": "paymentService",
      "ServiceMethod": "deduct",
      "CompensateMethod": "refund",
      "Next": "DeductInventory"
    },
    {
      "Name": "DeductInventory",
      "Type": "ServiceTask",
      "ServiceName": "inventoryService",
      "ServiceMethod": "deduct",
      "CompensateMethod": "add",
      "Next": "Succeed"
    },
    {
      "Name": "Succeed",
      "Type": "Succeed"
    }
  ]
}

SAGA的优缺点:

优点:
- 性能高(异步执行)
- 无长时间锁
- 适合长事务

缺点:
- 不保证隔离性(其他事务可能看到中间状态)
- 需要设计补偿逻辑
- 可能出现补偿失败(需要人工介入)

方案5:本地消息表(最终一致性)

原理: 利用本地事务和消息队列实现最终一致性。

流程:

Step 1: 本地事务
BEGIN TRANSACTION;
  -- 执行业务操作
  UPDATE accounts SET balance = balance - 100 WHERE id = 1;

  -- 插入消息表
  INSERT INTO message_table (msg_id, content, status)
  VALUES ('msg123', 'transfer to account 2', 'PENDING');
COMMIT;

Step 2: 定时任务扫描消息表
SELECT * FROM message_table WHERE status = 'PENDING';

Step 3: 发送消息到MQ
mqProducer.send('transfer', 'msg123');

Step 4: 更新消息状态
UPDATE message_table SET status = 'SENT' WHERE msg_id = 'msg123';

Step 5: 消费者接收消息
mqConsumer.onMessage(message -> {
    // 执行远程操作
    accountService.addBalance(2, 100);

    // 发送确认
    mqProducer.send('ack', 'msg123');
});

Step 6: 收到确认,删除消息
DELETE FROM message_table WHERE msg_id = 'msg123';

优缺点:

优点:
- 简单易实现
- 利用本地事务保证可靠性
- 最终一致性

缺点:
- 需要定时扫描消息表
- 消息表可能成为性能瓶颈
- 需要考虑消息重复消费(幂等性)

方案对比:

方案一致性性能复杂度适用场景
2PC强一致低中传统企业应用
TCC最终一致高高金融交易、订单
SAGA最终一致高中长事务、工作流
本地消息表最终一致中低异步通知、数据同步
最大努力通知最终一致高低支付回调、通知

选择建议:

1. 强一致性要求(金融核心系统)
   → 2PC(XA事务)或避免分布式事务

2. 高性能 + 最终一致性(电商订单)
   → TCC或SAGA

3. 异步通知场景(支付回调)
   → 本地消息表或最大努力通知

4. 长事务、工作流(审批流程)
   → SAGA

5. 简单场景(数据同步)
   → 本地消息表

6. 最优方案:避免分布式事务
   → 业务设计避免跨服务事务(微服务拆分边界)

Prev
08-数据库面试高频题
Next
10-Kubernetes与云原生面试题