分布式事务
分布式事务是在分布式环境中保证多个服务的原子性操作,要么全部成功,要么全部失败。分布式事务是分布式系统的难点,涉及数据一致性、性能、可用性的权衡。
为什么需要分布式事务?单体应用的事务(ACID)可以通过数据库事务保证,但微服务架构下,业务操作涉及多个服务、多个数据库,无法使用本地事务。分布式事务的应用场景:订单创建(扣减库存、创建订单、扣减余额)、支付(扣款、入账、记录流水)、跨行转账(转出行扣款、转入行入账)。
一致性权衡
CAP 定理指出分布式系统无法同时满足一致性、可用性、分区容错性。分布式事务需要在一致性和可用性之间权衡。
强一致性事务(CP)保证数据一致,但性能差、可用性低。例如 2PC、3PC。最终一致性事务(AP)保证高可用,但数据短暂不一致,需要应用层处理。例如 Saga、TCC。
ACID vs BASE
ACID(Atomicity、Consistency、Isolation、Durability)是传统数据库的事务特性,强调强一致性。
BASE(Basically Available、Soft state、Eventual consistency)是分布式系统的设计理念,强调高可用和最终一致性。
分布式事务可以追求 ACID,也可以接受 BASE,取决于业务需求。
2PC(两阶段提交)
2PC 的流程
2PC(Two-Phase Commit)是最早的分布式事务协议,包含两个角色:协调者(Coordinator)和参与者(Participant)。
准备阶段(Prepare):协调者向所有参与者发送 Prepare 请求,询问是否可以提交。参与者执行事务但不提交,如果可以提交则回复 Yes,否则回复 No。
提交阶段(Commit):如果所有参与者都回复 Yes,协调者发送 Commit 请求,参与者提交事务。如果有一个参与者回复 No,协调者发送 Rollback 请求,参与者回滚事务。
2PC 的问题
同步阻塞:参与者在准备阶段需要阻塞,等待协调者的指令。如果协调者故障,参与者无法释放资源,导致系统阻塞。
单点故障:协调者是单点,如果协调者故障,参与者无法决定提交或回滚,导致系统阻塞。
数据不一致:协调者发送 Commit 请求后崩溃,部分参与者收到 Commit 并提交,部分参与者未收到 Commit 并阻塞,导致数据不一致。
2PC 的优化
3PC(Three-Phase Commit)增加 CanCommit 阶段,减少阻塞时间。CanCommit 阶段协调者询问参与者是否可以提交,PreCommit 阶段参与者预提交,DoCommit 阶段协调者发送提交指令。
3PC 减少了阻塞时间,但仍然存在数据不一致风险,且增加了通信次数,性能更差。
TCC(Try-Confirm-Cancel)
TCC 的原理
TCC(Try-Confirm-Cancel)是应用层的分布式事务协议,将业务逻辑分成三个阶段。
Try(尝试执行):预留资源,检查业务是否可以执行。例如扣减库存,Try 阶段冻结库存而不是直接扣减。
Confirm(确认执行):使用预留资源,真正执行业务。例如扣减库存,Confirm 阶段扣除冻结的库存。
Cancel(取消执行):释放预留资源,回滚业务。例如扣减库存,Cancel 阶段解冻库存。
TCC 的问题
代码侵入:需要实现 Try、Confirm、Cancel 三个接口,业务逻辑被拆分,代码复杂度高。
幂等性:Confirm 和 Cancel 可能重复调用,需要保证幂等性。例如 Confirm 重复调用不会重复扣减库存。
空回滚:Try 未执行但 Cancel 被调用,需要处理空回滚。例如 Try 超时,Cancel 被调用,此时没有冻结库存,直接返回成功。
悬挂:Cancel 已执行但 Try 被调用,需要处理悬挂。例如 Cancel 超时,Try 被调用,此时库存已解冻,Try 直接返回失败。
TCC 的实现
TCC 框架:Seata(阿里)、Hmily、ByteTCC。TCC 框架提供事务管理、日志记录、重试机制,简化 TCC 的实现。
TCC 的事务日志:TCC 需要记录事务日志,记录 Try、Confirm、Cancel 的执行状态。如果 Confirm 或 Cancel 失败,根据日志重试。
TCC 的幂等性:幂等性可以通过唯一 ID 实现,记录已执行的 Confirm 或 Cancel,重复调用时直接返回成功。
Saga
Saga 的原理
Saga 是长事务的解决方案,将长事务拆分成多个本地事务,每个本地事务有补偿事务。Saga 的执行:依次执行本地事务,如果某个事务失败,则执行已执行事务的补偿事务。
例如订单创建:1. 扣减库存(补偿:增加库存),2. 创建订单(补偿:删除订单),3. 扣减余额(补偿:增加余额)。如果扣减余额失败,则执行补偿:删除订单、增加库存。
Saga 的问题
补偿事务可能失败:增加库存可能失败(库存已满),需要重试或人工介入。
脏读:未提交的数据被其他事务读取。例如订单已创建但余额未扣减,其他事务查询订单会看到脏数据。
隔离性差:Saga 不提供隔离性,需要应用层控制。例如扣减库存和扣减余额并发执行,可能导致库存扣减但余额未扣减。
Saga 的实现
Saga 的编排:编排器(Orchestrator)负责协调各个本地事务的执行和补偿。编排器记录事务状态,如果失败则执行补偿。
Saga 的 choreography:事件驱动的 Saga,各个服务监听事件,执行本地事务,发布事件。如果失败则发布补偿事件,其他服务执行补偿。
本地消息表
本地消息表的原理
本地消息表是将消息存储在本地数据库,在同一事务中写入业务数据和消息,然后异步发送消息。消息发送失败时,定时任务重试。
例如订单创建:1. 开启数据库事务,2. 创建订单记录,3. 写入消息(扣减库存消息),4. 提交事务。5. 后台任务读取消息,发送到消息队列。6. 库存服务消费消息,扣减库存。
本地消息表的问题
需要定时任务:定时任务扫描未发送的消息,重试发送。定时任务的实现需要考虑并发、性能。
可能重复发送:消息可能发送多次,需要消费端幂等性。例如扣减库存消息重复发送,库存服务需要幂等扣减。
本地消息表的实现
本地消息表的设计:消息表包含消息 ID、业务 ID、消息内容、发送状态、重试次数、创建时间、更新时间。
定时任务的逻辑:扫描发送状态为未发送的消息,发送到消息队列,更新状态。如果发送失败,增加重试次数,超过重试次数后标记为失败。
消息的确认:消费端消费消息后,发送确认给生产端,生产端标记消息为已发送。
MQ 事务消息
事务消息的原理
事务消息是 RocketMQ 提供的分布式事务方案,利用消息队列的事务能力。生产者发送半消息(Half Message),消息队列返回成功后,生产者执行本地事务。如果本地事务成功,生产者提交消息,消息队列对消费者可见。如果本地事务失败,生产者回滚消息,消息队列删除消息。
如果消息队列未收到生产者的提交或回滚,则回调生产端查询本地事务状态,根据状态提交或回滚消息。
事务消息的问题
需要消息队列支持:不是所有消息队列都支持事务消息,RocketMQ 支持,Kafka 不支持。
本地事务状态查询:需要提供查询本地事务状态的接口,消息队列会回调该接口。
分布式事务的对比
| 方案 | 一致性 | 可用性 | 性能 | 复杂度 |
|---|---|---|---|---|
| 2PC | 强一致 | 低 | 低 | 低 |
| 3PC | 强一致 | 中 | 中 | 中 |
| TCC | 最终一致 | 高 | 中 | 高 |
| Saga | 最终一致 | 高 | 高 | 高 |
| 本地消息表 | 最终一致 | 高 | 高 | 中 |
| 事务消息 | 最终一致 | 高 | 高 | 中 |
分布式事务的最佳实践
根据业务需求选择方案:强一致性场景选择 2PC,最终一致性场景选择 TCC 或 Saga。
幂等性是必须的:所有分布式事务方案都需要幂等性,重复执行不会出错。
事务日志很重要:记录事务日志,便于故障恢复和问题排查。
补偿事务要考虑失败:补偿事务可能失败,需要重试或人工介入。
监控和报警:监控分布式事务的成功率、耗时,失败时及时报警。
分布式事务是分布式系统的难点,理解分布式事务的原理和权衡,有助于设计合适的数据一致性方案。