深入理解分布式事务:从2PC到Saga模式实战
在微服务架构中,分布式事务是最具挑战性的问题之一。当业务操作跨越多个服务、多个数据库时,如何保证数据一致性?本文将深入剖析分布式事务的六种主流解决方案,从经典的2PC到现代化的Saga模式,结合生产环境实战经验,帮你构建可靠的事务处理架构。
一、为什么需要分布式事务
1.1 单体架构到微服务的演变
在单体应用中,所有数据操作共享同一个数据库,ACID特性天然可用。但当系统演进为微服务架构后,数据库按服务拆分,一次订单创建可能涉及订单服务(MySQL)、库存服务(PostgreSQL)和账户服务(MongoDB),此时本地事务无法跨库保证一致性。
1.2 分布式事务的核心挑战
CAP定理的制约:在分区容忍性(P)必须保证的前提下,一致性(C)和可用性(A)不可兼得。BASE理论放宽了强一致性要求,采用最终一致性模型。
FLP不可能原理:在异步通信模型中,哪怕只有一个进程可能崩溃,也没有确定性算法能达成共识。
二、2PC:两阶段提交协议
2.1 协议流程
两阶段提交(Two-Phase Commit)是最经典的分布式事务协议,分为:
阶段一(Prepare):协调者向所有参与者发送准备请求,参与者执行事务操作但不提交,将undo/redo日志写入,然后回复同意或中止。
阶段二(Commit/Rollback):如果所有参与者都同意,协调者发送提交命令;否则发送回滚命令。
// 2PC 协调者伪代码(Node.js风格)
class TwoPCCoordinator {
async execute(participants, txn) {
// 阶段一:准备
const votes = await Promise.all(
participants.map(p => p.prepare(txn).catch(e => 'abort'))
);
// 阶段二:决定
if (votes.every(v => v === 'agree')) {
await Promise.all(participants.map(p => p.commit(txn)));
return { status: 'committed' };
} else {
await Promise.all(participants.map(p => p.rollback(txn)));
return { status: 'aborted' };
}
}
}
2.2 优缺点分析
优点:实现简单,保证强一致性,数据库原生支持(MySQL XA)。
缺点:同步阻塞(参与者等待期间锁资源)、单点故障(协调者挂则全体阻塞)、数据不一致风险(协调者崩溃后部分提交)。
三、三阶段提交(3PC)
3.1 对2PC的改进
三阶段提交引入超时机制和预提交阶段,将第一阶段拆分为CanCommit和PreCommit两阶段,并给参与者添加超时自动提交逻辑,降低阻塞范围。
3.2 实际局限性
3PC在网络分区场景下仍可能出现不一致(超时提交时其他节点尚未收到消息),因此实际生产中使用较少,了解即可。
四、TCC:Try-Confirm-Cancel
4.1 TCC三阶段
Try阶段:业务检查与资源冻结(如预扣库存、预扣分润)。
Confirm阶段:真正的资源扣减,此阶段必须幂等。
Cancel阶段:回滚Try阶段占用的资源,同样需要幂等。
// TCC 接口定义示例(Java + Seata TCC模式)
@LocalTCC
public interface InventoryService {
@TwoPhaseBusinessAction(name = "deductInventory", commitMethod = "confirm", rollbackMethod = "cancel")
boolean tryDeduct(BusinessActionContext ctx,
@BusinessActionContextParameter("productId") Long productId,
@BusinessActionContextParameter("count") int count);
boolean confirm(BusinessActionContext ctx);
boolean cancel(BusinessActionContext ctx);
}
4.2 TCC的幂等与悬挂处理
幂等性:通过事务状态表记录执行状态,重复调用直接返回已有结果。
空回滚:Try未执行时收到Cancel请求,需要记录取消标记,防止后续Try被拒绝。
悬挂:Cancel先于Try执行,此时应在Try时检查是否已有取消标记。
五、本地消息表模式
5.1 核心思想
将分布式事务拆分为本地事务+消息传递。本地消息表方案由eBay提出(BASE理论的经典应用):
1. 业务操作与消息记录在同一本地事务中完成
2. 定时任务扫描未发送消息,投递到MQ
3. 消费方执行远程操作,成功后标记消息为已消费
4. 消费失败则重试,确保最终一致性
-- 本地消息表结构
CREATE TABLE local_message (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
biz_id VARCHAR(64) NOT NULL,
biz_type VARCHAR(32) NOT NULL,
msg_body TEXT NOT NULL,
status TINYINT DEFAULT 0, -- 0:待发送 1:发送中 2:已完成 3:失败
retry_count INT DEFAULT 0,
max_retry INT DEFAULT 5,
next_retry_time DATETIME,
created_at DATETIME DEFAULT NOW(),
UNIQUE KEY uk_biz (biz_id, biz_type)
);
5.2 可靠性保障
• 消息定时任务补偿:每60秒扫描待发送或失败的记录
• 重试策略:指数退避(1s、2s、4s、8s、最大32s)
• 死信队列:超过最大重试后转入DLQ,人工介入处理
• 定期清理:已确认超过7天的消息自动归档或删除
六、Saga模式
6.1 核心原理
Saga将一个长事务拆分为多个本地短事务(Ti),每个Ti对应一个补偿操作(Ci)。正常流程依次执行T1到T2到T3,若T3失败则逆序回滚C2到C1。
6.2 两种协调方式
编排模式(Choreography):无中心协调者,每个服务完成后触发下一个服务的事件。优点是去中心化,缺点是链路复杂度高时难以维护。
协调器模式(Orchestration):由编排器统一调度各参与者,根据结果决定下一步。优点是流程清晰、易于管理,缺点是编排器单点(可用集群化解)。
// 编排式 Saga 示例(Node.js + Event-Driven)
class OrderSagaOrchestrator {
async execute(order) {
const sagaId = generateUUID();
const sagaLog = new SagaLog(sagaId);
try {
// 步骤1: 创建订单
const orderResult = await orderService.create(order);
await sagaLog.logStep('createOrder', orderResult);
// 步骤2: 扣减库存
const stockResult = await stockService.deduct(order.items);
await sagaLog.logStep('deductStock', stockResult);
// 步骤3: 扣款
const payResult = await paymentService.charge(order.userId, order.amount);
await sagaLog.logStep('chargePayment', payResult);
// 步骤4: 发货
await shippingService.ship(orderResult.id);
await sagaLog.complete();
} catch (error) {
// 补偿回滚(逆序)
await this.compensate(sagaLog);
throw error;
}
}
async compensate(sagaLog) {
const steps = sagaLog.getCompletedSteps().reverse();
for (const step of steps) {
await this.compensateStep(step);
}
}
}
6.3 Saga的一致性保障
Saga不保证ACID中的隔离性,中间状态可能被其他事务读到。常用补偿手段:
语义锁:在操作上加语义标记(如pending状态),防止其他事务读到不一致中间态。
Permanent ID:每个操作和补偿操作记录永久ID,用于去重和幂等。
七、Seata框架实战
7.1 架构概览
Seata是阿里的开源分布式事务框架,支持AT、TCC、Saga、XA四种模式。其核心组件包括:
TC(Transaction Coordinator):事务协调器,维护全局事务状态。
TM(Transaction Manager):事务管理器,定义全局事务范围。
RM(Resource Manager):资源管理器,管理分支事务。
7.2 AT模式详解
AT(Automatic Transaction)模式是Seata的默认模式,通过解析SQL自动生成回滚日志,实现无侵入的分布式事务。
# Seata TC部署(Docker方式)
docker run -d --name seata-server \
-p 8091:8091 \
-p 7091:7091 \
-e SEATA_PORT=8091 \
-e STORE_MODE=db \
-e SEATA_IP=192.168.1.100 \
seataio/seata-server:1.7.0
// Spring Boot接入Seata AT模式
@Service
public class OrderBusinessService {
@GlobalTransactional(timeoutMills = 300000, name = "create-order")
public OrderResult createOrder(OrderRequest request) {
// 1. 本地订单创建
Order order = orderMapper.insert(request.toOrder());
// 2. 远程库存扣减(自动纳入全局事务)
inventoryFeignClient.deduct(request.getProductId(), request.getCount());
// 3. 远程账户扣款(自动纳入全局事务)
accountFeignClient.debit(request.getUserId(), request.getAmount());
return OrderResult.success(order.getId());
}
}
7.3 Seata Saga状态机
Seata通过JSON定义Saga状态机,支持复杂业务编排:
{
"startState": "CreateOrder",
"states": {
"CreateOrder": {
"type": "serviceTask",
"serviceName": "orderServiceImpl",
"methodName": "create",
"input": ["$.[request]"],
"output": "$.[orderResult]",
"next": "DeductStock",
"catch": [{"error": "*", "next": "CompensateCreateOrder"}]
},
"DeductStock": {
"type": "serviceTask",
"serviceName": "stockServiceImpl",
"methodName": "deduct",
"compensateState": "CompensateStock",
"next": "ChargePayment"
},
"ChargePayment": {
"type": "serviceTask",
"serviceName": "paymentServiceImpl",
"methodName": "charge",
"compensateState": "CompensatePayment",
"end": true
},
"CompensateStock": {
"type": "serviceTask",
"serviceName": "stockServiceImpl",
"methodName": "compensateDeduct",
"end": true
}
}
}
八、选型指南与生产实践
8.1 模式对比
2PC/XA:短事务、低延迟、强一致性,适合单体应用内的跨库事务,不适用于微服务长流程。
AT(Seata):无侵入、自动补偿,适合基于关系型数据库的中等复杂度业务。
TCC:性能高、可控性强,适合对一致性要求高、可设计补偿接口的核心链路。
Saga:长事务、最终一致性,适合跨机构、跨系统的长时间业务流程。
本地消息表:简单可靠,适合已有的、允许最终一致性的异步场景。
8.2 生产环境最佳实践
1. 默认不用分布式事务:优先考虑业务设计,避免跨服务的事务需求(最终一致性优于强一致)。
2. 先AT后升级:初期使用Seata AT快速接入,关键链路后期迁移到TCC。
3. 监控先行:部署事务监控 dashboard(Seata默认提供),确保异常实时告警。
4. 限流降级:TC层配置连接池和超时,避免事务积累拖垮系统。
5. 压测验证:上线前模拟TC宕机、网络分区、高并发锁竞争等故障。
九、总结
分布式事务没有银弹,只有合适的方案。从2PC的强一致到Saga的最终一致,每种模式都在性能与一致性之间做出权衡。在实际工程中,我们更应关注如何通过合理的服务边界划分来减少分布式事务的需求。毕竟,解决分布式事务的最佳方式是避免分布式事务。
当必须面对时:能不用就不用,必须用就选最轻的,最后再上强一致。这就是分布式事务的处理之道。

发表评论 取消回复