- 概述 -
在 《柔性事务之 TCC 详解》 和《柔性事务之 Saga 详解》两文中我们详细剖析了柔性事务的第一个分支补偿型事务. 在《刚性事务总结和柔性事务概述》中我们介绍过的柔性事务包含补偿型事务和通知型事务.
通知型事务主要包含事务消息和最大努力通知事务两个组成. 通知型事务的主流实现机制是通过 MQ 来通知其他事务参与者自己事务的执行状态. MQ 组件的引入有效的讲事务参与者解耦开, 各个参与者都可以异步执行, 所以通知型事务又称为异步事务. 通知型事务主要适用于那些需要异步更新数据, 并且对数据的实时性要求较低的场景.
事务消息主要适用于内部系统的数据最终一致性保障, 因为内部相对比较可控, 比如订单和购物车, 收货与清算, 支付与结算等等场景; 而最大努力通知事务主要用于外部系统, 因为外部的网络环境更加复杂和不可信, 所以只能尽最大努力去通知实现数据最终一致性, 比如充值平台与运营商, 支付对接等等跨网络系统级别对接.
普通消息是无法解决本地事务执行和消息发送的一致性问题的. 因为消息发送是一个网络通信的过程, 发送消息的过程就有可能出现发送失败, 或者超时的情况. 超时有可能发送成功了, 有可能发送失败了, 消息的发送方是无法确定的, 所以此时消息发送方无论是提交事务还是回滚事务, 都有可能不一致性出现. 所以事务消息的难度在于投递消息和参与者自身本地事务的一致性保障. 目前业界解决这个一致性的方案有两个分支:
基于 MQ 自身的事务消息方案
基于 DB 的本地消息表方案
- 基于 MQ 自身的事务消息方案 -
基于 MQ 的事务消息方案主要依靠 MQ 的半消息机制来实现投递消息和参与者自身本地事务的一致性保障. 半消息机制实现原理其实借鉴的 2PC 的思路, 是二阶段提交的广义拓展, 流程图如下:
事务发起方首先发送 prepare 消息到 MQ;
在发送 prepare 消息成功后执行本地事务;
根据本地事务执行结果返回 commit 或者是 rollback;
如果消息是 rollback, MQ 将删除该 prepare 消息不进行下发, 如果是 commit 消息, MQ 将会消息发送给 consumer 端;
如果执行本地事务过程中, 执行端挂掉, 或者超时, MQ 服务器端将不停的询问 producer 来获取事务状态;
Consumer 端的消费成功机制有 MQ 保证
MQ 事务消息方案因为使用了半消息机制, 对业务页具有比较大侵入性, 有以下注意点:
业务方调用半消息, 并提供对应的回查方法;
MQ 提要提供半消息机制, 并定期扫描长期半消息, 对消息生产者进行回查确认事务.
消费方需要进行幂等消费.
- 基于 BD 的本地消息表方案 -
有时候我们目前的 MQ 组件并不支持事务消息, 或者我们想尽量少的侵入业务方. 这时我们需要另外一种方案 "基于 DB 本地消息表", 流程图如下:
业务方: 直接利用本地事务, 将业务数据和事务消息直接写入数据库.
投递线程: 使用专门的投递工作线程进行事务消息投递到 MQ, 根据投递 ACK 去删除事务消息表记录
本地事务消息表的优势在于方案的通用性, 无需提供回查方法, 进一步减少的业务的侵入. 在某些场景下, 还可以进一步利用注解等形式进行解耦, 有可能实现无业务代码侵入式的实现. 我们上面说了本地事务消息表的基本理论, 那么如果要设计一个高可用的企业级本地事务消息表方案, 就要考虑更多的事情, 在性能上做更大的优化, 降低更多的重复投递率. 以下是一个企业级事务消息的设计流程图:
事务消息服务: 提供通用投递接口, 用于保证事务消息的本地写入, 并将事务消息写入事务内存队列.
使用投递线程池, 继续事务内存队列投递派发分配. 投递工作线程只投递本实例拥有的事务消息, 投递失败线程列入时间轮队列; 重试机制使用失败挡位区分, 默认提供 6 档: 5s,10s,15s,20s,25s,30s.
时间轮线程进行 60 秒转动, 将到期的失败事务消息重入事务内存队列.
因为我们的事务消息服务是无状态化的多实例存在, 所以需要一个持锁线程进行主节点竞争强锁, 处理一些额外的工作.
因为我们的事务内存队列是内存级, 不可避免面临重启等情况下的数据丢失. 这时需要事务消息服务主节点进行定期扫表, 将长期未投递的事务消息取出放入事务消息服务.
事务消息服务主节点还有一个清理线程, 专门用于将已处理成功的历史事务消息进行归档清理, 降低 DB 的数据量.
- 总结 -
咱们上面介绍了 MQ 事务消息方案和 DB 本地消息表方案, 这两个方案有什么区别呢?
MQ 事务消息方案
需要 MQ 支持半消息机制或者类似特性, 在重复投递上具有比较好的去重处理
需要业务方进行改造, 提供对应的本地操作成功回查功能. 具有比较大的业务侵入性.
DB 本地事务消息表方案
使用了数据库来存储事务消息, 降低了对 MQ 的需求, 但是增加了存储成本.
事务消息使用了异步投递, 增大了消息重复投递的可能性.
我们说了两种事务消息的特性和优劣性, 我们在总结下事务消息的共性.
事务消息都依赖 MQ 进行事务通知, 所以都是异步的.
事务消息在投递方都是存在重复投递的可能, 需要有配套的机制去降低重复投递率, 实现更友好的消息投递去重.
事务消息的消费方, 因为投递重复的无法避免, 因此需要进行消费去重设计或者服务幂等设计.
来源: https://www.qcloud.com/developer/article/1655732