Skip to main content

分布式事务方案选型

前置知识点

  • 事务要解决的是数据一致性问题,要么全成功,要么全失败。
  • 分布式事务就是涉及多个节点的一致性问题。
    • 这个节点可能是,上下游服务,或者服务和中间件,也可能是服务与第三方服务
  • 分布式事务处理成本高,可以考虑将强相关的微服务合并,减少分布式事务的需求。尽量在做服务拆分时就把原子操作放在单服务,单进程,单数据库执行,使用本地事务保证 ACID。
  • 分布式事务无通用方案,需结合实际场景权衡,根据业务不同可以组合使用。
  • 在实现分布式事务需求的同时,还需考虑:性能损耗、维护成本、改造成本

根据应用场景确定方案需求

强一致还是高可用

CAP 理论指出,在分布式系统中,以下三个特性无法同时满足,系统必须在它们之间做出权衡:

  1. 一致性(Consistency):所有节点在同一时间看到的数据是一致的。
  2. 可用性(Availability):每个请求都能在有限时间内得到响应,无论成功还是失败。
  3. 分区容错性(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