分布式事务
分布式系统的数据一致性是复杂的问题。当一个操作需要修改多个数据库或多个服务的数据时,如何保证这些修改要么全部成功,要么全部失败,这就是分布式事务要解决的问题。
CAP 定理
一致性、可用性、分区容错性
CAP 定理指出分布式系统不可能同时满足一致性、可用性、分区容错性,最多只能同时满足两项。
一致性要求所有节点在同一时刻看到相同的数据。写入操作成功后,后续的读取操作都能读到最新的数据。可用性要求每个请求都能得到响应,无论成功或失败,但不保证响应是最新的数据。分区容错性要求系统在网络分区的情况下仍能继续运行,网络分区是指节点之间的网络连接中断或消息丢失。
权衡选择
CA 系统放弃分区容错性,意味着所有节点必须在同一网络,不能容忍网络故障。这样的系统实际上不是分布式系统,因为网络分区是必然发生的。AP 系统放弃强一致性,允许读取到过期数据,通过最终一致性保证数据最终一致。CP 系统放弃可用性,在网络分区时拒绝服务,保证数据一致性。
实际系统通常是 AP 或 CP。BASE 理论是 AP 系统的设计指导:基本可用、软状态、最终一致性。基本可用允许系统在故障时降级服务,例如限制非核心功能、排队请求。软状态允许系统中的数据存在中间状态,这个中间状态不影响系统可用性。最终一致性保证在没有新更新的情况下,经过一段时间后,所有副本的数据最终一致。
2PC 与 3PC
两阶段提交
两阶段提交(2PC,Two-Phase Commit)是最经典的分布式事务协议,包含协调者和参与者两个角色。第一阶段是准备阶段,协调者向所有参与者发送准备请求,参与者执行事务但不提交,返回是否可以提交。第二阶段是提交阶段,如果所有参与者都返回可以提交,协调者发送提交请求,否则发送回滚请求。
2PC 的优势是强一致性,所有参与者要么全部提交,要么全部回滚。劣势是同步阻塞,参与者在准备阶段需要锁定资源,直到收到提交或回滚请求。单点故障,如果协调者故障,参与者无法知道最终决定,会一直等待。数据不一致,如果协调者发送提交请求时网络故障,部分参与者收到提交请求,部分没有收到,导致数据不一致。
三阶段提交
三阶段提交(3PC)在 2PC 基础上增加了 CanCommit 阶段,减少了阻塞时间。第一阶段是 CanCommit,协调者询问参与者是否可以提交,参与者快速检查资源是否足够。第二阶段是 PreCommit,参与者执行事务但不提交,锁定资源。第三阶段是 DoCommit,协调者根据参与者的反馈决定提交或回滚。
3PC 相比 2PC 的改进是在 CanCommit 阶段快速失败,避免资源长时间锁定。但 3PC 仍然无法解决网络分区导致的数据不一致问题,而且增加了协议复杂度和消息往返次数,实际应用中很少使用。
XA 事务
XA 是 2PC 的标准实现,MySQL、PostgreSQL、Oracle 等数据库都支持 XA 事务。XA 事务使用两阶段提交协议,通过 XA START、XA END、XA PREPARE、XA COMMIT、XA ROLLBACK 命令控制。
-- XA 事务示例
XA START 'xid1';
-- 执行 SQL
XA END 'xid1';
XA PREPARE 'xid1';
XA COMMIT 'xid1';XA 事务的优势是标准协议、强一致性。劣势是性能差,锁资源时间长,不适合高并发场景。实际应用中,XA 事务的使用越来越少,取而代之的是最终一致性方案。
TCC
Try-Confirm-Cancel
TCC(Try-Confirm-Cancel)是应用层的分布式事务解决方案,将业务操作分为三个阶段:Try 阶段预留资源,Confirm 阶段确认执行,Cancel 阶段取消操作。
以转账为例:Try 阶段检查账户余额是否足够,冻结相应金额,但不实际转账。Confirm 阶段扣除 A 账户的冻结金额,增加 B 账户余额。Cancel 阶段释放 A 账户的冻结金额,撤销操作。
TCC 的优势是性能好,没有长事务锁资源。劣势是需要业务代码实现三个阶段,开发成本高。Try 阶段预留资源可能导致资源不足,Confirm 和 Cancel 阶段需要保证幂等性。
幂等性设计
分布式环境中,网络超时或重试可能导致同一操作执行多次,幂等性保证多次执行的结果与一次执行相同。TCC 的 Confirm 和 Cancel 阶段必须设计为幂等操作。
实现幂等性的方法包括:唯一键约束,使用业务主键或唯一标识,重复执行时因为唯一约束失败。状态机,记录操作状态,重复执行时检查状态,已完成的直接返回。Token 机制,每个操作分配唯一 Token,服务端记录已处理的 Token,重复请求直接返回。
SAGA
长事务拆分
SAGA 将长事务拆分为多个本地事务,每个本地事务有对应的补偿事务。正常执行时依次执行各本地事务,如果某个本地事务失败,逆向执行之前成功事务的补偿事务,回滚整个长事务。
以订单为例:SAGA 包扣减库存、创建订单、扣款三个本地事务。对应补偿事务是增加库存、取消订单、退款。正常流程是扣减库存 → 创建订单 → 扣款。如果扣款失败,执行退款 → 取消订单 → 增加库存。
SAGA 实现
SAGA 的实现方式有两种:编排式和协程式。编排式由一个中心协调器控制事务流程,记录每个本地事务的执行状态,失败时逆向执行补偿事务。协程式由各本地事务自主执行,通过事件总线通知后续事务,失败时发布补偿事件。
SAGA 的优势是避免了长事务锁资源,适合跨服务的长事务。劣势是不保证隔离性,其他事务可能读取到中间状态。补偿事务的设计复杂,需要保证补偿的正确性。
本地消息表
消息表设计
本地消息表是将业务操作和消息发送放在同一个本地事务中,保证原子性。业务操作成功后,同时将消息写入本地消息表。后台任务定期扫描消息表,发送消息到消息队列,确认消息被消费后删除消息。
以订单为例:创建订单时,在同一个事务中写入订单表和消息表。消息包含操作类型、订单 ID、目标服务等信息。后台任务扫描消息表,将消息发送到 MQ,库存服务消费消息扣减库存。确认库存服务处理成功后,删除消息。
消息表需要包含以下字段:id(主键)、business_id(业务 ID)、business_type(业务类型)、content(消息内容)、status(状态,待发送/发送中/已发送)、retry_times(重试次数)、create_time(创建时间)、update_time(更新时间)。
可靠性保证
本地消息表的可靠性体现在几个方面:原子写入,业务操作和消息写入在同一事务,保证同时成功或失败。可靠发送,后台任务定期扫描未发送消息,支持重试,消息发送成功后才标记为已发送。幂等消费,消费端需要保证消息处理的幂等性,重复消费不会产生副作用。
消息发送的失败处理:如果消息发送失败,增加重试次数并更新状态。重试次数超过阈值,标记为发送失败,告警人工处理。重试间隔采用指数退避,避免频繁重试。
定时任务
后台定时任务扫描消息表,发送待发送的消息。任务执行频率根据业务需求调整,通常每秒或每分钟执行一次。每次扫描一批消息,避免一次扫描过多数据导致内存溢出或长时间占用数据库连接。
-- 扫描待发送的消息
SELECT * FROM local_message
WHERE status = 'PENDING'
AND retry_times < 10
ORDER BY create_time
LIMIT 1000;定时任务需要保证幂等性,可以通过分布式锁或任务状态标识避免重复执行。任务执行过程中如果失败,下次执行会继续处理,不会丢失消息。
事务消息
消息队列支持
事务消息是 RocketMQ 提供的分布式事务解决方案,核心思想是发送消息和本地事务的原子性。事务消息发送流程:发送半消息(Half Message),消息对消费者不可见。执行本地事务。提交或回滚消息,提交后消息对消费者可见,回滚后消息被删除。
如果本地事务执行失败或超时,RocketMQ 会回调事务状态检查接口,询问本地事务的执行结果。根据检查结果决定提交或回滚消息。
实现原理
RocketMQ 的事务消息实现原理是利用消息队列的存储能力,先存储半消息,然后执行本地事务,最后根据本地事务结果决定消息的最终状态。这种方式避免了本地事务和消息发送的两阶段提交,实现了最终一致性。
事务消息的应用场景包括跨服务的业务流程,订单创建后通知库存、物流、支付服务;支付成功后通知订单、积分、营销服务。相比本地消息表,事务消息不需要额外的消息表和后台任务,实现更简洁。
最大努力通知
通知与确认
最大努力通知是一种简化的分布式事务方案,通过多次通知尝试达到最终一致。通知方发起通知,接收方确认收到通知,如果通知失败,按规则重试。这种方式不保证强一致性,但实现简单,适合对一致性要求不高的场景。
以支付回调为例:支付平台发起支付结果通知到商户,商户收到通知后返回确认。如果通知失败或超时,支付平台按一定规则重试通知。商户处理通知时需要保证幂等性,重复通知不会导致重复处理。
通知策略
通知策略包括重试次数、重试间隔、通知方式。重试次数根据业务重要性设定,通常 3-5 次。重试间隔采用指数退避,例如第 1 次 1 分钟后,第 2 次 5 分钟后,第 3 次 30 分钟后。通知方式包括同步 HTTP 调用、异步消息队列、主动查询。
最大努力通知适合支付回调、物流通知、身份验证等场景。这些场景的特点是业务重要性高,但可以容忍短时间的不一致,通过多次通知最终达到一致。
分布式事务没有银弹,不同的方案有不同的适用场景和权衡。2PC/3PC 提供强一致性但性能差,TCC/SAGA 适合长事务但开发成本高,本地消息表和事务消息提供最终一致性但需要业务配合。根据业务特点选择合适的方案,在一致性和可用性之间找到平衡点。