分布式系统数据一致性有哪些知识点?
面试场景
面试官:”微服务架构下,你们是如何保证跨服务的数据一致性的?”
这是分布式系统的核心问题。回答需要体现:
- 理论基础(CAP/BASE)
- 方案选型(刚性/柔性事务)
- 具体实践
理论基础
CAP定理
分布式系统最多只能同时满足三项中的两项:
| 特性 | 含义 | 举例 |
|---|---|---|
| Consistency | 所有节点数据一致 | A扣款100,B必须同时+100 |
| Availability | 每个请求都能收到响应 | 不能超时或报错 |
| Partition Tolerance | 网络分区时仍能工作 | 机房断网也能服务 |
现实选择:由于网络分区必然发生,P必须保证,所以只能在C和A之间选择:
- CP:保证一致性,牺牲可用性(如ZooKeeper)
- AP:保证可用性,牺牲强一致性(如Eureka)
BASE定理
对CAP中AP方案的延伸,用最终一致性替代强一致性:
BASE = Basically Available + Soft State + Eventually Consistent
基本可用 + 软状态 + 最终一致性
| 特性 | 说明 | 示例 |
|---|---|---|
| 基本可用 | 核心功能可用 | 大促时关闭非核心功能 |
| 软状态 | 允许中间状态 | 订单”处理中”状态 |
| 最终一致性 | 最终数据一致 | 5秒后库存和订单对上 |
事务方案分类
┌─────────────────────────────────────────────────────────┐
│ 分布式事务方案 │
├────────────────────────┬────────────────────────────────┤
│ 刚性事务 │ 柔性事务 │
│ (强一致性) │ (最终一致性) │
├────────────────────────┼────────────────────────────────┤
│ • 2PC/XA │ 补偿型: │
│ • 3PC │ • TCC │
│ │ • SAGA │
│ │ 通知型: │
│ │ • 本地消息表 │
│ │ • 事务消息 │
└────────────────────────┴────────────────────────────────┘
刚性事务
2PC(两阶段提交)
阶段一:准备阶段
协调者 → 参与者1: 准备提交?
协调者 → 参与者2: 准备提交?
参与者1: 执行本地事务,不提交,返回OK
参与者2: 执行本地事务,不提交,返回OK
阶段二:提交阶段
协调者收到所有OK后 → 参与者1: 提交!
→ 参与者2: 提交!
问题:
- 同步阻塞:资源锁定等待
- 单点故障:协调者挂了,参与者一直阻塞
- 数据不一致:部分提交成功部分失败
3PC(三阶段提交)
在2PC基础上增加CanCommit预询问阶段,并引入超时机制。
改进点:
- 参与者也有超时机制,超时自动提交
- 预询问可以快速失败
问题:仍无法解决网络分区导致的不一致。
柔性事务
TCC(Try-Confirm-Cancel)
┌─────────────────────────────────────────────────────────┐
│ Try(尝试) │
│ 预留资源,检查业务可行性 │
│ 例:冻结库存100件 │
├─────────────────────────────────────────────────────────┤
│ Confirm(确认) │
│ 真正执行业务 │
│ 例:扣减冻结的库存 │
├─────────────────────────────────────────────────────────┤
│ Cancel(取消) │
│ 释放预留资源 │
│ 例:释放冻结的库存 │
└─────────────────────────────────────────────────────────┘
代码示例:
// Try阶段
public boolean tryDeductStock(Long productId, int count) {
// 冻结库存
int affected = stockMapper.freezeStock(productId, count);
return affected > 0;
}
// Confirm阶段
public boolean confirmDeductStock(Long productId, int count) {
// 扣减冻结的库存
stockMapper.confirmDeduct(productId, count);
return true;
}
// Cancel阶段
public boolean cancelDeductStock(Long productId, int count) {
// 释放冻结的库存
stockMapper.unfreezeStock(productId, count);
return true;
}
优点:性能好,无长时间锁 缺点:侵入性强,需要实现三个接口
SAGA
长事务拆分为多个本地事务,每个本地事务有对应的补偿操作。
T1 → T2 → T3 → T4
↓ 失败
C3 ← C2 ← C1(逆向补偿)
适用场景:业务流程长、参与方多
本地消息表
┌──────────────────────────────────────────────────────────┐
│ 订单服务 │
│ │
│ BEGIN TRANSACTION │
│ 1. INSERT INTO orders ... │
│ 2. INSERT INTO local_message ... (同一事务) │
│ COMMIT │
│ │
│ 定时任务扫描local_message,发送到MQ │
└──────────────────────────────────────────────────────────┘
↓
MQ
↓
┌─────────────────────┐
│ 库存服务 │
│ 消费消息,扣减库存 │
└─────────────────────┘
优点:简单可靠 缺点:需要定时任务,延迟稍高
方案选型
| 方案 | 一致性 | 性能 | 侵入性 | 适用场景 |
|---|---|---|---|---|
| 2PC/XA | 强 | 差 | 低 | 金融核心 |
| TCC | 最终 | 好 | 高 | 复杂业务 |
| SAGA | 最终 | 好 | 中 | 长流程 |
| 本地消息表 | 最终 | 好 | 低 | 一般业务 |
| 事务消息 | 最终 | 好 | 低 | 有RocketMQ |
选型建议
金融级强一致性要求 → 2PC/XA或Seata AT
高性能要求 + 复杂业务 → TCC
长流程业务 → SAGA
一般业务 + 简单实现 → 本地消息表
已有RocketMQ → 事务消息
Seata AT模式
阿里开源的分布式事务框架,自动生成补偿SQL。
@GlobalTransactional // 关键注解
public void createOrder(OrderRequest request) {
// 1. 创建订单
orderService.create(request);
// 2. 扣减库存(远程调用)
stockService.deduct(request.getProductId(), request.getCount());
// 3. 扣减余额(远程调用)
accountService.deduct(request.getUserId(), request.getAmount());
}
原理:
- 业务SQL执行前记录undo_log
- 所有分支事务成功 → 删除undo_log
- 任一分支失败 → 根据undo_log回滚
面试答题框架
理论基础:
- CAP定理:分布式系统只能CP或AP
- BASE理论:最终一致性替代强一致性
方案分类:
- 刚性事务:2PC/3PC,强一致但性能差
- 柔性事务:TCC/SAGA/本地消息表,最终一致
选型原则:
- 金融强一致 → XA/Seata
- 一般业务 → 本地消息表
- 高性能 + 复杂逻辑 → TCC
实际项目用什么:
- Seata AT模式(非侵入式)
- 本地消息表(简单场景)
总结
| 知识点 | 核心内容 |
|---|---|
| CAP | 分布式系统的权衡取舍 |
| BASE | 最终一致性的理论基础 |
| 刚性事务 | 强一致性,性能差 |
| 柔性事务 | 最终一致性,性能好 |
| 选型原则 | 根据业务场景选择合适方案 |