分布式事务方案选型
前置知识点
- 事务要解决的是数据一致性问题,要么全成功,要么全失败。
- 分布式事务就是涉及多个节点的一致性问题。
- 这个节点可能是,上下游服务,或者服务和中间件,也可能是服务与第三方服务
- 分布式事务处理成本高,可以考虑将强相关的微服务合并,减少分布式事务的需求。尽量在做服务拆分时就把原子操作放在单服务,单进程,单数据库执行,使用本地事务保证 ACID。
- 分布式事务无通用方案,需结合实际场景权衡,根据业务不同可以组合使用。
- 在实现分布式事务需求的同时,还需考虑:性能损耗、维护成本、改造成本
根据应用场景确定方案需求
强一致还是高可用
CAP 理论指出,在分布式系统中,以下三个特性无法同时满足,系统必须在它们之间做出权衡:
- 一致性(Consistency):所有节点在同一时间看到的数据是一致的。
- 可用性(Availability):每个请求都能在有限时间内得到响应,无论成功还是失败。
- 分区容错性(Partition Tolerance):系统在遇到网络分区(部分节点无法通信)时仍能正常运行。
在分布式系统中,分区容错性是必须满足的(不能一个服务宕机,整个服务不可用),因此系统设计者需要在一致性和可用性之间做出选择。
- CP:强一致,允许一定时长的阻塞
- AP:可用性优先,必须立即响应结果,高可用,最终一致(BASE 理论)
成本考量
- 性能损耗
- 维护成本,是否引入协调者、MQ
- 引入的中间件高可用
失败策略
- 服务异常退出
- 自身不稳定
- 下游不稳定
- 业务失败策略: 直接回退,还是努力通知型
- 所有的回退事务均为努力通知型,否则就套娃了
- 网络不通失败
- 异常报错失败
需要分布式事务的典型场景
电商系统中的订单处理
- 场景:用户下单时,系统需要同时完成以下操作:
- a. 创建订单(订单服务)。
- b. 扣减库存(库存服务)。
- 问题:
- 如果创建订单成功,但扣减库存超时,订单状态失败,但可能扣减库存已经成功了,此时出现不一致
分布式事务可选方案
本地消息表
实现步骤:
- 事务与消息的原子性
- 在执行本地事务时,将业务操作和消息写入本地数据库的同一事务中,确保两者同时成功或失败。
- 异步可靠投递
- 通过定时任务轮询本地消息表,将未发送的消息投递到消息队列,下游服务消费后更新消息状态。
- 消息状态字段:UNSENT(未发送)、SENT(已发送)、PROCESSED(已处理)。
- 定时任务补偿:若消息发送失败(如 MQ 不可用),定时任务需重试发送。
- 投递成功,可以删除消息记录
- 通过定时任务轮询本地消息表,将未发送的消息投递到消息队列,下游服务消费后更新消息状态。
- 防重与幂等
- 消息唯一标识 + 消费端幂等,避免消息重复消费。
- 消息唯一 ID:使用 UUID 或 业务 ID+操作类型 作为消息唯一标识。
- 消费端幂等表:下游服务记录已处理的消息 ID 。
- 消息唯一标识 + 消费端幂等,避免消息重复消费。
- 异常处理
- 生产端重试:消息发送失败后,通过指数退避策略重试。
- 消费端重试:消费失败时,将消息重新放入队列或记录死信队列。
- 人工兜底:对多次重试失败的消息,提供人工干预界面。
- 性能优化
- 批量发送消息:定时任务批量查询和发送消息,减少数据库压力。
- 消息分区键:按业务 ID 分区,避免同一业务的消息并发冲突。
依赖:数据库事务,消息中间件,定时任务
优点:
- 灵活多变,适用范围广,可根据业务定制扩展实现
- 大流量,高可用
缺点:
- 消息表耦合到业务系统中
消息表示例:
CREATE TABLE `secure_invoke_record` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'id',
`secure_invoke_json` json NOT NULL COMMENT '请求快照参数json',
`status` tinyint(8) NOT NULL COMMENT '状态 1待执行 2已失败',
`next_retry_time` datetime(3) NOT NULL COMMENT '下一次重试的时间',
`retry_times` int(11) NOT NULL COMMENT '已经重试的次数',
`max_retry_times` int(11) NOT NULL COMMENT '最大重试次数',
`fail_reason` text COMMENT '执行失败的堆栈',
`create_time` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
`update_time` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '修改时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_next_retry_time` (`next_retry_time`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='本地消息表';
事务消息(RocketMQ)
原理:本地消息表进阶版本,通过半消息机制,确保本地事务与消息发送原子性。
优点:高可靠,减少业务侵入。
缺点:强依赖消息中间件的事务能力
引入 Seata 中间件
如果可以承受多部署一个事务中间件的成本,也可以使用 Seata https://seata.apache.org/zh-cn/docs/dev/mode/at-mode
Seata AT 模式
原理:基于全局锁和 undo log 自动回滚,无侵入式写入代理。
优点:
- 开发简单,兼容主流框架。
缺点:
- 数据库必须是关系型数据库
- 需要创建 undo_log 表,跨组跨部门不方便
- 需要对所有交易生成前后镜像并持久化,有损性能,并引入全局锁,存在死锁风险 适用场景:代码无侵入,适用异常直接回滚的场景,适合管理后台。
TCC(Try-Confirm-Cancel)
原理:业务拆分为 Try(预留资源)、Confirm(提交)、Cancel(补偿)三阶段。自定义三个阶段的代码,中间件统筹执行。
优点:
- 高灵活性,性能较好,无长期资源锁。
- 数据库支持事务即可
缺点:
- 开发复杂,需业务侵入式编码。
- 预留资源可能导致稀缺资源竞争
- 需要新增列,用来记录预留资源,比如:
- 当前库存 冻结库存
适用场景:业务上需要补偿回滚的场景,业务定制, 高并发。
SAGA
原理:将事务拆分为多个本地事务,每个事务对应补偿操作,失败时逆向补偿。
优点:
- 可记录链路
- 通过状态机事务补偿,适合事务中存在第三方调用场景
缺点:
- 补偿逻辑复杂,需保证幂等性。
- 引入状态机,有一定门槛
适用场景:跨部门的长流程业务(如订单 → 库存 → 物流),支持并发流程、子流程。
2PC/XA(两阶段提交)(不推荐)
原理:协调者分两阶段(准备、提交/回滚)协调参与者,确保所有节点一致。
优点:强一致性,实现简单(如 XA 协议)。
缺点:同步阻塞、单点故障、数据不一致风险。
适用场景:适用场景少,强一致建议高性能分布式数据库。比如 OceanBase 。
相关链接:
https://seata.apache.org/zh-cn/docs/user/quickstart/
https://juejin.cn/post/7445732463153070095
https://blog.csdn.net/ly853602/article/details/146768586
https://seata.apache.org/zh-cn/blog
https://github.com/seata/awesome-fescar/tree/master/slides/meetup/201912%40hangzhou