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、HBase | Cassandra、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对比:
| 特性 | ACID | BASE |
|---|---|---|
| 一致性 | 强一致性 | 最终一致性 |
| 隔离性 | 严格隔离 | 允许中间状态 |
| 可用性 | 可能牺牲可用性 | 优先保证可用性 |
| 适用场景 | 单机数据库、关键交易 | 分布式系统、大规模应用 |
| 性能 | 相对较低 | 相对较高 |
| 复杂度 | 相对简单 | 相对复杂 |
示例对比:
-- 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),将一致性问题分解为三个子问题:
- Leader Election(领导选举)
- Log Replication(日志复制)
- 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对比:
| 特性 | Raft | Paxos |
|---|---|---|
| 可理解性 | 易于理解(分解为子问题) | 难以理解(证明复杂) |
| Leader | 强Leader(一个Leader处理所有请求) | 无固定Leader(Multi-Paxos优化后有Leader) |
| 日志结构 | 连续的日志序列,无空洞 | 可能有空洞 |
| 选举 | 明确的选举流程 | 隐式选举(通过提案编号) |
| 实现复杂度 | 相对简单 | 相对复杂 |
| 工程应用 | etcd, Consul, TiKV | Chubby, 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锁 | Redlock | Zookeeper锁 |
|---|---|---|---|
| 性能 | 高 | 中 | 中低 |
| 可靠性 | 低(单点) | 中(多实例) | 高(基于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. 最优方案:避免分布式事务
→ 业务设计避免跨服务事务(微服务拆分边界)