分布式事务
分布式事务是指事务的参与者、资源服务器以及事务管理器分别位于分布式系统的不同节点之上.
1. 基础理论
1.1 本地事务
事务是指由一组操作组成的一个工作单元, 这个工作单元具有原子性(atomicity)、一致性(consistency)、隔离性(isolation)和持久性(durability).
- 原子性: 执行单元中的操作要么全部执行成功, 要么全部失败. 如果有一部分成功一部分失败那么成功的操作要全部回滚到执行前的状态.
- 一致性: 执行一次事务会使用数据从一个正确的状态转换到另一个正确的状态, 执行前后数据都是完整的.
- 隔离性: 在该事务执行的过程中, 任何数据的改变只存在于该事务之中, 对外界没有影响, 事务与事务之间是完全的隔离的. 只有事务提交后其它事务才可以查询到最新的数据.
- 持久性: 事务完成后对数据的改变会永久性的存储起来, 即使发生断电宕机数据依然在.
1.2 分布式事务
在分布式系统中一次操作由多个系统协同完成, 这种一次事务操作涉及多个系统通过网络协同完成的过程称为分布式事务. 这里强调的是多个系统通过网络协同完成一个事务的过程, 并不强调多个系统访问了不同的数据库, 即使多个系统访问的是同一个数据库也是分布式事务.
CAP理论
CAP理论指的是在一个分布式系统中, 一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance).
- 一致性: 在分布式系统中的每个节点需要保证在同一时刻的数据一致性.
- 可用性: 在任何情况下, 分布式系统都能对外提供服务.
- 分区容忍性: 分布式系统都分布在多个子网络. 每个子网络就叫做一个区(partition). 分区容忍性指的是各个区间因为一些原因导致无法通讯, 系统还能正常对外服务.
在所有分布式场景下不会同时具备CAP三个特性. 因为在具备分区容忍性的前提下, C和A是不能共存的.
- AP: 放弃一致性, 追求分区容忍性和可用性. 这是很多分布式系统设计时的选择
- CP: 放弃可用性, 追求一致性和分区容忍性. 比如zookeeper就是追求强一致性.
- CA: 放弃分区容忍性, 即不进行分区. 这样的系统将不再是一个标准的分布式系统, 最常见的就是关系型数据库.
BASE理论
CAP理论告诉我们一个分布式系统只能满足其中二个. 其中AP在实际应用中比较多, 但在实际生产中很多场景还是需要实现一致性的, 即使不追求CAP中的一致性, 最终也要将数据库同步成功保证数据一致, 即要求最终一致性. 这种一致性和CAP中的一致性不同, CAP中的一致性要求在任何时间查询每个节点, 数据都必须一致, 它强调的是强一致性. 但是最终一致性是可以允许在一段时间内每个节点数据不一致, 但是经过一段时间后, 每个节点的数据是一致的, 它强调的是最终数据的一致性.
BASE理论是Basically Available(基本可用), Soft State(软状态)和Eventually Consistent(最终一致性)三个短语的缩写. BASE理论是对CAP中AP的一个扩展, 通过牺牲强一致性来获取可用性, 当出现故障时允许部分不可用, 但是要保证核心功能可用, 允许数据在一段时间内不一致, 但最终达到一致性. 满足BASE理论的事务, 可以称之为"柔性事务".
- 基本可用: 分布式系统出现故障时, 允许损失部分可用功能, 保证核心功能可用. 比如支付系统出现故障, 但是商品系统仍然可以正常游览.
- 软状态: 由于不追求强一致性, 所以BASE允许系统存在中间状态(乱状态), 并认为该状态不影响系统的整体可用性, 即允许系统在多个不同节点的数据副本存在数据延时.
- 最终一致性: 在经过一段时间后, 应该保证所有节点保持数据一致性, 从而达到数据的最终一致性. 这个时间期限取决于网络延时、系统负载、数据复制方案设计等等因素.
2 解决方案
2.1 2PC
2PC即两阶段提交协议, 是将整个事务流程分为两个阶段: 准备阶段(prepare phase), 提交阶段(commit phase). 整个事务过程由事务管理器与事务参与者组成, 事务管理者负责决策整个分布式事务的提交和回滚, 事务参与者负责自己本地事务的提交和回滚. 在计算机中部分关系数据库(oracle, mysql)支持两阶段提交协议.
- 准备阶段: 事务管理器给每个参与者发送Prepare消息, 每个参与者要么直接返回失败(如权限验证失败), 要么在本地执行事务, 写本地的redo和undo日志, 此时事务并没有提交, 而是将资源锁定.(undo日志是记录修改前的数据, 用于数据库回滚. redo日志是记录修改后的数据, 用于提交事务后写入数据文件.)
- 提交阶段: 如果事务管理器收到了参与者的失败消息或者超时消息, 直接给每个参与者发送回滚(Rollback)消息; 否则, 发送提交(Commit)消息; 参与者根据协调者的指令执行提交或者回滚操作, 释放所有事务处理过程中使用的锁资源. (注意:必须在最后阶段释放锁资源)
2.1.1 XA方案
2pc方案是在数据库层面实现的, 为了统一标准减少不必要的对接成本, 需要制定标准化的处理模型以及接口标准, 国际开放标准组织定义了分布式事务处理模型DPT(Distributed Transaction Processing Reference Model).
DTP模型定义如下角色:
- AP(Application Program): 应用程序, 可以理解为使用DTP分布式事务的程序.
- RM(Resource Manager): 资源管理器, 可以理解为事务的参与者, 一般情况下是指一个数据库实例, 通过资源管理器对该数据库进行控制, 资源管理器控制分支事务.
- TM(Transaction Manager): 事务管理器, 负责协调和管理事务, 事务管理器控制着全局事务, 管理事务生命周期, 并协调各个RM.
全局事务是指分布式事务处理环境中, 需要操作多个数据库共同完成一个工作, 这个工作就是一个全局事务. DTP模型定义TM和RM之间通讯的接口规范叫XA, 简单理解为数据库提供的2pc接口协议, 基于数据库的XA协议来实现2pc又称为XA方案.
以上三个角色之间的交互方式如下:
- TM向AP提供应用程序编程接口, AP通过TM提交以及回滚事务.
- TM交易中间件通过XA接口来通知RM数据库事务的开始, 结束以及提交, 回滚等.
以新用户注册送积分为例, 执行流程如下:
- 应用程序(AP)持有用户库和积分库两个数据源.
- 应用程序(AP)通过TM通知用户库RM新增用户, 同时通知积分库RM为该用户新增积分, RM此时并没有提交事务, 此时用户和积分资源锁定.
- TM收到执行回复, 只要有以防失败则分别向其他RM发送回滚事务, 回滚完毕, 资源锁释放.
- TM收到执行回复, 全部成功, 此时向所有RM发起提交事务, 提交完毕, 资源锁释放.
XA方案的问题:
- 需要本地数据库支持XA协议.
- 资源锁需要等到两个阶段结束才能释放, 性能较差.
2.1.2 seata方案
seata是阿里中间件团队发起的开源项目, 是一个分布式事务框架. 传统2PC的问题在seata中得到了解决, 它通过对本地关系数据库的分支事务的协调来驱动完成全局事务, 是工作在应用层的中间件. 主要优点是性能较好, 不会长时间占用连接资源. 目前提供AT模式(即2PC)及TCC模式的分布式事务解决方案.
seata把一个分布式事务理解成一个包含来若干分支事务的全局事务. 全局事务的职责是协调其下管辖的分支事务达成一致, 要么一起成功提交, 要么一起失败回滚. 此外, 通常分支事务本身就是一个关系数据库的本地事务.
- Transaction Coordinator(TC): 事务协调器, 它是独立的中间件, 需要独立部署运行, 维护全局事务的运行状态, 接收TM指令发起全局事务的提交与回滚, 负责与RM通信协调各个分支事务的提交或回滚.
- Transaction Manager(TM): 事务管理器, TM需要嵌入应用程序中工作, 它负责开启一个全局事务, 并最终向TC发起全局提交或全局回滚的指令.
- Resource Manager(RM): 控制分支事务, 负责分支注册、状态汇报, 并接收事务协调器TC的指令, 驱动分支(本地)事务的提交和回滚.
以新用户注册送积分为例, 执行流程如下:
- 用户服务的TM向TC申请开启一个全局事务, 全局事务创建成功并生成一个全局唯一的XID.
- 用户服务的RM向TC注册分支事务, 该分支事务在用户服务执行新增用户逻辑, 并将其纳入XID对应全局事务的管辖.
- 用户服务执行分支事务, 向用户表插入一条记录.
- 逻辑执行到远程调用积分服务时(XID在微服务调用链路的上下文中传播). 积分服务的RM向TC注册分支事务, 该分支事务执行增加积分的逻辑, 并将其纳入XID对应全局事务的管辖.
- 积分服务执行分支事务, 向积分记录表插入一条记录, 执行完毕后, 返回用户服务.
- 用户服务分支事务执行完毕.
- TM向TC发起针对XID的全局提交或回滚决议.
- TC调度XID下管辖的全部分支事务完成提交或回滚请求.
seata实现2PC与传统2PC的差别
架构层次方面, 传统2PC方案的RM实际上是在数据库层, RM本质上就是数据库自身, 通过XA协议实现, 而Seata的RM是以jar包的形式作为中间件层部署在应用程序的这一侧的.
两阶段提交方面, 传统2PC无论第二阶段的决议是commit还是rollbcak, 事务性资源的锁都要保持到第二阶段完成才释放. 而seata的做法是在第一阶段就将本地事务提交, 这样就可以省去第二阶段持锁的时间, 整体提高效率.
2.2 TCC
TCC是Try、Confirm、Cancel三个词语的缩写, TCC要求每个分支事务实现三个操作: 预处理Try、确认Confirm、撤销Cancel.
TCC的三个阶段
Try阶段: 做业务检查(一致性)及资源预留(隔离), 此阶段仅是一个初步操作, 它和后续的Confirm一起才能真正构成一个完整的业务逻辑.
Confirm阶段: 做业务确认操作, Try阶段所有分支事务执行成功后开始执行Confirm. 通常情况下, 采用TCC则认为Confirm阶段是不会出错的. 即只要Try成功, Confirm一定成功. 若Confirm阶段真的出错了, 需引入重试机制或人工处理.
Cancel阶段: 实现一个与Try相反的操作, 即回滚操作. 通常情况下, 采用TCC则认为Cancel阶段也是一定成功的. 若Cancel阶段真的出错了, 需引入重试机制或人工处理.
全局事务管理器在发起全局事务时生成全局事务记录, 全局事务ID贯穿整个分布式事务调用链条, 用来记录事务上下文, 追踪和记录状态, 由于Confirm和Cancel失败需进行重试, 因此需要实现为幂等性是指同一个操作无论请求多少次, 其结果都相同.
seata, tcc-transaction, hmily以上三个框架都是TCC的实现, 详细查看链接内的文档.
2.2.1 TCC注意事项
空回滚
在全局事务管理器没有调用Try方法, 而是直接调用来Cancel方法, Cancel方法需要识别出这是一个空回滚, 然后直接返回成功.
出现原因是当一个分支事务所在服务宕机或网络异常, 分支事务调用记录为失败, 这个时候其实是没有执行Try阶段, 当故障恢复后, 分布式事务进行回滚则会调用Cancel方法, 从而形成空回滚.
解决思路的关键就是要识别出这个空回滚, 需要知道Try阶段是否执行. 前面已经说过全局事务在发起全局事务时会生成全局事务ID, 该ID贯穿整个分布式事务调用链. 再额外增加一张分支事务记录表, 其中有全局事务ID和分支事务ID, Try方法里会插入一条记录, 表示Try阶段执行直接结果. Cancel阶段里读取该记录, 如果该记录存在, 则正常回滚; 如果该记录不存在, 则是空回滚.
幂等
通过前面介绍已经了解到, 为了保证TCC的提交重试机制不会引发数据不一致, 要求TCC的二阶段Try、Confirm和Cancel接口保证幂等, 这样不会重复使用或者释放资源. 如果幂等控制没有做好, 很有可能导致数据不一致等严重问题.
解决思路是在上述"分支事务记录"中增加执行状态, 每次执行前都查询该状态.
悬挂
悬挂就是对于一个分布式事务, 其Cancel阶段比Try阶段先执行.
出现原因是在调用分支事务Try阶段时, 先注册分支事务, 再执行远程调用, 如果此时远程调用发生网络拥堵, 在远程调用超时以后, TM就会通知RM回滚该分布式事务, 可能回滚完成后, 远程调用请求才到达参与者, 开始真正执行. 而一个Try方法预留的业务资源, 只有该分布式事务才能使用, 该分布式事务第一阶段预留的业务资源就再也没有人能够处理了, 对于这种情况, 我们就称为悬挂, 即业务资源预留后无法继续处理.
解决思路是如果二阶段执行完成, 那一阶段就不能再继续执行. 在执行一阶段事务时判断在该全局事务下, "分支事务记录"表中是否已经有二阶段事务记录, 如果有则不执行Try方法.
2.3 可靠消息最终一致性
可靠消息最终一致性方案是指当事务发起方执行完本地事务后发送一条消息到MQ, 事务参与方(消息消费者)一定能够接受消息并处理事务成功. 此方案强调的是只要消息发给事务参与方最终事务要达到一致.
在此方案中, 需要利用消息队列完成. 在事务发起方(消息生产方)将消息发送给消息中间件, 事务参与方(消息消费方)从消息队列中接收消息. 事务发起方(消息生产方)与消息队列之间, 事务参与方(消息消费方)和消息队列之间都是通过网络通信, 由于网络通信的不确定性导致分布式事务问题. 因此可靠消息最终一致性方案要解决以下几个问题:
本地事务与消息发送的原子性问题
事务发起方在本地事务执行成功后消息必须发出去. 即实现本地事务和消息发送的原子性, 要么都成功, 要么都失败. 本地事务与消息发送的原子性问题是实现可靠消息最终一致性方案的关键问题.
事务参与方接收消息的可靠性
事务参与方必须能够从消息队列接收到消息, 如果接收消息失败可以重复接收消息.
消息重复消费的问题
由于网络的存在, 若某一个消费节点超时但是消费成功, 此时消息中间件会重复投递此消息, 就导致来消息的重复消费. 要解决消息重复消费的问题就要实现事务参与方的方法幂等性.
2.3.1 本地消息表方案
此方案的核心是通过本地事务保证数据业务操作和消息的一致性, 然后通过定时任务将消息发送至消息中间件, 待确认消息发送给消费方成功再将消息删除.

以新用户注册送积分为例, 执行流程如下:
- 用户服务在本地事务新增用户和增加"积分消息日志". (用户表和消息表通过本地事务保证一致)
- 定时任务扫描日志. 经过第一步, 消息已经写到消息日志表中, 可以启动独立的线程, 定时对消息日志表中的消息进行扫描并发送至消息中间件, 在消息中间件反馈发送成功后删除该消息日志, 否则等待定时任务下一周期重试.
- 消费方消费消息. 这里可以使用MQ的ack(即消息确认)机制, 消费者监听MQ, 如果消费者接收到消息并且业务处理完成后向MQ发送ack(即消息确认), 此时说明消费者正常消费消息完成, MQ将不再向消费者推送消息, 否则MQ会不断向消费者投递消息. 由于消息会重复投递, 消费方需要实现消息幂等性.
2.3.2 RocketMQ事务消息方案
RocketMQ是一个来自阿里巴巴的分布式消息队列, 支持消息生产者发送事务消息. 实际上, 该方案是对本地消息表的一个封装, 将本地消息表移动到MQ内部.

以新用户注册送积分为例, MQ发送方即消息生产方, 是用户服务, 负责新增用户. MQ订阅方即消息消费方, 是积分服务, 负责新增积分. 执行流程如下:
- MQ发送方发送事务消息至MQ, MQ将消息状态标记为Prepared(预览状态), 注意此时这条消息消费者(MQ订阅方)是无法消费到的.
- MQ接收到MQ发送方发送的消息则回应发送成功, 表示MQ已接收到消息.
- MQ发送方执行本地事务.
- 消息投递. 如果MQ发送方的本地事务执行成功, 则向MQ发送commit消息, MQ接收到commit消息后将"增加积分消息"状态标记为可消费, 此时MQ订阅方(积分服务)就可以正常消费消息; 如果MQ发送方的本地事务执行失败, 则向MQ发送rollback消息, MQ接收到rollback消息后将删除"增加积分消息". MQ订阅方(积分服务)消费消息, 消费成功则向MQ回应ack, 否则MQ将重复投递消息. 这里ack默认自动回应, 即程序执行正常则自动回应ack.
- 事务回查. 如果执行MQ发送方的本地事务过程中, 执行端挂掉或者超时, MQ将会不停的询问同组的其他Producer来获取事务执行状态, 这个过程叫事务回查. MQ会根据事务回查结果来决定是否投递消息. 第五步的流程已经由RocketMQ实现, 对用户则来说, 用户需要分别实现本地事务执行以及本地事务回查方法, 因此只需关注本地事务的执行状态即可.
public interface RocketMQLocalTransactionListener {
/**
* 发送prepare消息成功此方法被回调, 该方法用于执行本地事务
* @param msg 回传的消息, 利用transactionId即可获取到该消息的唯一Id
* @param arg 调用send方法时传递的参数, 当send时候若有额外的参数可以传递到send方法中, 这里能获取到
* @return 返回事务状态, COMMIT: 提交; ROLLBACK: 回滚;
*/
RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg);
/**
* @param msg 通过获取transactionId来判断这条消息的本地事务执行状态
* @return 返回事务状态, COMMIT: 提交; ROLLBACK: 回滚;
*/
RocketMQLocalTransactionState checkLocalTransaction(Message msg);
}
2.4 最大努力通知

这边以充值案例来说明:
- 账户系统调用充值系统接口
- 充值系统完成支付处理后, 向账户系统发送充值结果. 若发送失败, 则充值系统按策略进行重复发送.
- 账户系统接收到充值结果后修改充值状态.
- 账户系统未接收到通知会主动调用充值系统的接口查询充值结果.
通过上边的案例可以了解到最大努力通知方案的目标: 消息发送方(充值系统)通过一定的机制最大努力将业务处理结果通知到消息接收方(账户系统).
其中消息发送方必须实现:
- 有一定的消息重复通知机制. 因为接受消息方可能没有接收到消息, 此时要有一定的机制对消息重复通知.
- 消息校对机制. 如果尽最大努力也没有通知到接收方或者接收方消费消息后要再次消费, 此时可由接收方主动向通知方查询消息信息来满足需求.
最大努力通知与可靠消息一致性有什么不同
- 解决方案思想不同. 可靠消息一致性方案中, 消息发送方需要保证消息发送成功, 消息的可靠性关键由消息发起方来保证. 而最大努力通知方案中, 消息发送方尽最大的努力将业务处理结果发送给消息接收方, 但是并不保证消息接收方一定能接收到消息, 此时需要消息接收方主动调用消息发送方的接口进行业务处理结果的查询操作, 通知的可靠性关键在消息接收方.
- 两者的业务应用场景不同. 可靠消息一致性关注的是交易过程的事务一致, 以异步的方式完成交易. 最大努力通知关注的是交易后的通知事务, 即将交易结果可靠的通知出去.
- 技术解决方向不同. 可靠消息一致性要解决消息从发出到接收的一致性, 即消息发出并且被接收到. 最大努力通知无法保证消息从发出到接收的一致性, 只提供消息接收的可靠性机制. 可靠性机制指的是最大努力的将消息通知给接收方, 当消息无法被接收方接收时, 由接收方主动查询业务处理结果.
2.4.1 解决方案
- 发起通知方将通知发送到MQ. 使用可靠消息一致方案中的事务消息保证本地事务与消息的原子性.
- 通知程序监听MQ, 接收MQ的消息. 如果通知程序没有回应ack则MQ会重复发送消息.
- 通知程序通过互联网接口协议(如http、webservice)调用接收通知方提供的接口, 完成通知.
- 接收通知方可通过消息校对接口来校对消息的一致性.
浙公网安备 33010602011771号