分布式-分布式事务基础

基本概念

什么是事务

​ 什么是事务?举个生活中的例子:你去小卖铺买东西,“一手交钱,一手交货”就是一个事务的例子,交钱和交货必须全部成功,事务才算成功,任一个活动失败,事务将撤销所有已成功的活动。
​ 简而言之,事务可以看做是一次大的活动,它由不同的小活动组成,这些活动要么全部成功,要么全部失败

数据库本地事务

​ 在计算机系统中,更多的是通过关系型数据库来控制事务,这是利用数据库本身的事务特性来实现的,因此叫数据库事务,由于应用主要靠关系数据库来控制事务,而数据库通常和应用在同一个服务器,所以基于关系型数据库的事务又被称为本地事务.

回顾一下数据库事务的四大特性 ACID:

A(Atomic):原子性,构成事务的所有操作,要么都执行完成,要么全部不执行,不可能出现部分成功部分失败的情况。

C(Consistency):一致性,在事务执行前后,数据库的一致性约束没有被破坏。比如:张三向李四转100元,转账前和转账后的数据是正确状态这叫一致性,如果出现张三转出100元,李四账户没有增加100元这就出现了数据错误,就没有达到一致性。

I(Isolation):隔离性,数据库中的事务一般都是并发的,隔离性是指并发的两个事务的执行互不干扰,一个事务不能看到其他事务运行过程的中间状态。通过配置事务隔离级别可以避脏读、重复读等问题。

D(Durability):持久性,事务完成之后,该事务对数据的更改会被持久化到数据库,且不会被回滚。

​ 我们的本地事务由资源管理器进行管理,事务的ACID是通过InnoDB日志和锁来保证,简单来说

Mysql Innodb引擎ACID实现原理简述:

1、持久性通过redo log(重做日志)来实现

2、原子性和一致性通过undo log(回滚日志)来实现

3、隔离性由MVCC机制和锁来实现

​ UndoLog的原理很简单,为了满足事务的原子性,在操作任何数据之前,首先将数据备份到一个地方(这个存储数据备份的地方称为UndoLog),是旧数据进行备份。然后进行数据的修改。如果出现了错误或者用户执行了ROLLBACK语句,系统可以利用Undo Log中的备份将数据恢复到事务开始之前的状态。

RedoLog记录的是新数据的备份。在事务提交前,只要将RedoLog持久化即可,不需要将数据持久化。当系统崩溃时,虽然数据没有持久化,但是RedoLog已经持久化。系统可以根据RedoLog的内容,将所有数据恢复到最新的状态。

分布式事务

​ 分布式系统会把一个应用系统拆分为可独立部署的多个服务,因此需要服务与服务之间远程协作才能完成事务操作,这种分布式系统环境下由不同的服务之间通过网络远程协作完成事务称之为分布式事务,例如用户注册送积分事务、创建订单减库存事务,银行转账事务等都是分布式事务。

image-20230315222605196

单一架构下的事务

1
2
3
4
begin transaction;
//1.本地数据库操作:张三减少金额
//2.本地数据库操作:李四增加金额
commit transation;

分布式架构下的事务

1
2
3
4
begin transaction;
//1.本地数据库操作:张三减少金额
//2.远程调用:让李四增加金额
commit transation;

可以设想,李四金额增加了,但是由于网络问题超时响应等原因导致本地事务回滚,就会造成事务不一致。

分布式事物的地产生场景

1、跨服务远程调用,即跨jvm进程

img

2、跨数据库实例产生分布式事务

img

分布式事务的基础

​ 分布式系统之所以叫分布式,是因为提供服务的各个节点分布在不同机器上,相互之间通过网络交互。不能因为有一点网络问题就导致整个系统无法提供服务,网络因素成为了分布式事务的考量标准之一。因此,分布式事务需要更进一步的理论支持。

CAP

CAP(定理,又被叫作布鲁尔定理。对于设计分布式系统来说(不仅仅是分布式事务)的架构师来说,CAP就是入门理论。

image-20230317090206074

C - Consistency-一致性

​ 一致性是指写操作后的读操作可以读取到最新的数据状态,当数据分布在多个节点上,从任意结点读取到的数据都是最新的状态。对于数据分布在不同节点上的数据上来说,如果在某个节点更新了数据,那么在其他节点如果都能读取到这个最新的数据,那么就称为强一致,如果有某个节点没有读取到,那就是分布式不一致。

A - Availability-可用性

​ 可用性是指任何事务操作都可以得到响应结果,且不会出现响应超时或响应错误。可用性的两个关键一个是合理的时间,一个是合理的响应。合理的时间指的是请求不能无限被阻塞,应该在合理的时间给出返回。合理的响应指的是系统应该明确返回结果并且结果是正确的,这里的正确指的是比如应该返回50,而不是返回40。

P - Partition tolerance-分区容错性

​ 通常分布式系统的各各结点部署在不同的子网,这就是网络分区,不可避免的会出现由于网络问题而导致结点之间通信失败,此时仍可对外提供服务。

分区容错性是分布式系统具备的基本能力

CAP三者不可兼得

​ 在分布式系统中,网络无法100%可靠,分区其实是一个必然现象,如果我们选择了CA而放弃了P,那么当发生分区现象时,为了保证一致性,这个时候必须拒绝请求,但是A又不允许,所以分布式系统理论上不可能选择CA架构,只能选择CP或者AP架构。

​ 对于CP来说,放弃可用性,追求一致性和分区容错性,我们的zookeeper其实就是追求的强一致。
​ 对于AP来说,放弃一致性(这里说的一致性是强一致性),追求分区容错性和可用性,这是很多分布式系统设计时的选择,后面的BASE也是根据AP来扩展。
​ 顺便一提,CAP理论中是忽略网络延迟,也就是当事务提交时,从节点A复制到节点B,但是在现实中这个是明显不可能的,所以总会有一定的时间是不一致。同时CAP中选择两个,比如你选择了CP,并不是叫你放弃A。因为P出现的概率实在是太小了,大部分的时间你仍然需要保证CA。就算分区出现了你也要为后来的A做准备,比如通过一些日志的手段,是其他机器回复至可用。

BASE

强一致性与最终一致性

​ CAP理论告诉我们一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容忍性(Partition tolerance)这三项中的两项,其中AP在实际应用中较多,AP即舍弃一致性,保证可用性和分区容忍性,但是在实际生产中很多场景都要实现一致性,比如前边我们举的例子主数据库向从数据库同步数据,即使不要一致性,但是最终也要将数据同步成功来保证数据一致,这种一致性和CAP中的一致性不同,CAP中的一致性要求在任何时间查询每个结点数据都必须一致,它强调的是强一致性,但是最终一致性是允许可以在一段时间内每个结点的数据不一致,但是经过一段时间每个结点的数据必须一致,它强调的是最终数据的一致性

Base理论

​ BASE 是 **Basically Available(基本可用)Soft state(软状态)**和 Eventually consistent (最终一致性)三个短语的缩写。BASE理论是对CAP中AP的一个扩展,通过牺牲强一致性来获得可用性,当出现故障允许部分不可用但要保证核心功能可用,允许数据在一段时间内是不一致的,但最终达到一致状态。满足BASE理论的事务,我们称之为“柔性事务

基本可用:

​ 分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。如,电商网站交易付款出现问题了,商品依然可以正常浏览。

软状态:

​ 由于不要求强一致性,所以BASE允许系统中存在中间状态(也叫软状态),这个状态不影响系统可用性,如订单的”支付中”、“数据同步中”等状态,待数据最终一致后状态改为“成功”状态。

最终一致:

​ 最终一致是指经过一段时间后,所有节点数据都将会达到一致。如订单的”支付中”状态,最终会变为“支付成功”或者”支付失败”,使订单状态与实际交易结果达成一致,但需要一定时间的延迟、等待。

2PC协议及其解决方案

什么是2PC

​ 2PC即两阶段提交协议,是将整个事务流程分为两个阶段,准备阶段(Prepare phase)、提交阶段(commitphase),2是指两个阶段,P是指准备阶段,C是指提交阶段。

1. 准备阶段(Prepare phase):事务管理器给每个参与者发送Prepare消息,每个数据库参与者在本地执行事务,并写本地的Undo/Redo日志,此时事务没有提交。(Undo日志是记录修改前的数据,用于数据库回滚,Redo日志是记录修改后的数据,用于提交事务后写入数据文件)

2. 提交阶段(commit phase):如果事务管理器收到了参与者的执行失败或者超时消息时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据事务管理器的指令执行提交或者回滚操作,并释放事务处理过程中使用的锁资源。注意:必须在最后阶段释放锁资源。

XA方案

DTP:国际开放标准组织Open Group定义了分布式事务处理模型DTP(Distributed Transaction Processing Reference Model),这模型规范了分布式事务的模型设计。

DTP模型定义如下角色:

  • **AP(**Application Program):即应用程序,可以理解为使用DTP分布式事务的程序。
  • RM(Resource Manager):即资源管理器,可以理解为事务的参与者,一般情况下是指一个数据库实例,通过资源管理器对该数据库进行控制,资源管理器控制着分支事务。
  • TM(Transaction Manager):事务管理器,负责协调和管理事务,事务管理器控制着全局事务,管理事务生命周期,并协调各个RM。全局事务是指分布式事务处理环境中,需要操作多个数据库共同完成一个工作,这个工作即是一个全局事务。

下面新用户注册送积分为例来说明:

image-20230319165353627

以上三个角色之间的交互方式如下:

  1. TM向AP提供应用程序编程接口,AP通过TM提交及回滚事务。
  2. TM交易中间件通过XA接口来通知RM数据库事务的开始、结束以及提交、回滚等。

执行流程如下:

1、应用程序(AP)持有用户库和积分库两个数据源。
2、应用程序(AP)通过TM通知用户库RM新增用户,同时通知积分库RM为该用户新增积分,RM此时并未提交事
务,此时用户和积分资源锁定。
3、TM收到执行回复,只要有一方失败则分别向其他RM发起回滚事务,回滚完毕,资源锁释放。
4、TM收到执行回复,全部成功,此时向所有RM发起提交事务,提交完毕,资源锁释放

DTP模型定义TM和RM之间通讯的接口规范叫XA,简单理解为数据库提供的2PC接口协议,基于数据库的XA协议来实现2PC又称为XA方案

XA方案的问题

单点问题

​ 事务管理器在整个流程中扮演的角色很关键,如果其宕机,比如在第一阶段已经完成,在第二阶段正准备提交的时候事务管理器宕机,资源管理器就会一直阻塞,导致数据库无法使用。

同步阻塞

​ 在准备就绪之后,资源管理器中的资源一直处于阻塞,直到提交完成,释放资源。

数据不一致

​ 两阶段提交协议虽然为分布式数据强一致性所设计,但仍然存在数据不一致性的可能,比如在第二阶段中,假设协调者发出了事务commit的通知,但是因为网络问题该通知仅被一部分参与者所收到并执行了commit操作,其余的参与者则因为没有收到通知一直处于阻塞状态,这时候就产生了数据的不一致性。

​ 总的来说,XA协议比较简单,成本较低,但是其单点问题,以及不能支持高并发(由于同步阻塞)依然是其最大的弱点。

Seata (AT)方案

是什么

​ Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT(Auto Transaction)、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

​ 传统2PC的问题在Seata中得到了解决,它通过对本地关系数据库的分支事务的协调来驱动完成全局事务,是工作在应用层的中间件。主要优点是性能较好,且不长时间占用连接资源,它以高效并且对业务0侵入的方式解决微服务场景下面临的分布式事务问题,它目前主要提供AT模式(即2PC)及TCC模式的分布式事务解决方案。

​ Seata把一个分布式事务理解成一个包含了若干分支事务的全局事务。全局事务的职责是协调其下管辖的分支事务达成一致,要么一起成功提交,要么一起失败回滚。此外,通常分支事务本身就是一个关系数据库的本地事务,下图是全局事务与分支事务的关系图:

image-20230319233044827

image-20230319233148264

Seata定义了3个组件来协议分布式事务的处理过程:

Transaction Coordinator (TC): 事务协调器,它是独立的中间件,需要独立部署运行,它维护全局事务的运
行状态,接收TM指令发起全局事务的提交与回滚,负责与RM通信协调各各分支事务的提交或回滚。

Transaction Manager (TM): 事务管理器,TM需要嵌入应用程序中工作,它负责开启一个全局事务,并最终
向TC发起全局提交或全局回滚的指令。

Resource Manager (RM): 控制分支事务,负责分支注册、状态汇报,并接收事务协调器TC的指令,驱动分
支(本地)事务的提交和回滚。

拿新用户注册送积分举例Seata的分布式事务过程:

image-20230319233404655

具体的执行流程如下:

  1. 用户服务的 TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID。
  2. 用户服务的 RM 向 TC 注册 分支事务,该分支事务在用户服务执行新增用户逻辑,并将其纳入 XID 对应全局
    事务的管辖。
  3. 用户服务执行分支事务,向用户表插入一条记录。
  4. 逻辑执行到远程调用积分服务时(XID 在微服务调用链路的上下文中传播)。积分服务的RM 向 TC 注册分支事
    务,该分支事务执行增加积分的逻辑,并将其纳入 XID 对应全局事务的管辖。
  5. 积分服务执行分支事务,向积分记录表插入一条记录,执行完毕后,返回用户服务。
  6. 用户服务分支事务执行完毕。
  7. TM 向 TC 发起针对 XID 的全局提交或回滚决议。
  8. TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。

Seata实现2PC(AT模式)与传统2PC(XA模式)的差别

XA Seata AT
架构层次 RM是数据库本身 RM是以jar包形式作为中间件部署在应用程序这一侧
资源锁定 事务性资源锁定到第二阶段完成 第一阶段就提交完成后释放,整体效率较高

3PC协议

是什么

​ 1、在2阶段提交协议的基础上、
​ 2、三阶段提交协议在协调者和参与者中都引入超时机制,
​ 3、并且把两阶段提交协议的第一个阶段拆分成了两步:询问,然后再锁资源,最后真正提交

1、can commit
预执行一次、不占用资源
2、pre commit
预执行一次,记录undo/redo日志,占用资源
3、do commit
事务提交阶段

img

​ 3PC 多了一个阶段其实就是在执行事务之前来确认参与者是否正常,防止个别参与者不正常的情况下,其他参与者都执行了事务,锁定资源。
​ 出发点是好的,但是绝大部分情况下肯定是正常的,所以每次都多了一个交互阶段就很不划算
​ 3PC 在参与者处也引入了超时机制,这样在协调者挂了的情况下,如果已经到了提交阶段了,参与者等半天没收到协调者的情况的话就会自动提交事务。不过万一协调者发的是回滚命令呢?这就数据不一致了。

2PC与3PC

​ 2PC 是一个强一致性的同步阻塞协议,性能已经是比较差的了,

​ 3PC 主要是为了解决两阶段提交协议的阻塞问题,从原来的两个阶段扩展为三个阶段,增加了超时机制。但是多了一个阶段就多了一次通讯的开销,而且是绝大部分情况下无用的通讯。

​ 2PC 还是 3PC 都是协议,可以认为是一种指导思想,和真正的落地还是有差别的。

TCC事务及其解决方案

是什么

TCC是Try、Confifirm、Cancel三个词语的缩写,TCC要求每个分支事务实现三个操作:预处理Try、确认Confifirm、撤销Cancel。Try操作做业务检查及资源预留,Confifirm做业务确认操作,Cancel实现一个与Try相反的操作即回滚操作。TM首先发起所有的分支事务的try操作,任何一个分支事务的try操作执行失败,TM将会发起所有分支事务的Cancel操作,若try操作全部成功,TM将会发起所有分支事务的Confifirm操作,其中Confifirm/Cancel操作若执行失败,TM会进行重试。

TCC分为三个阶段:

1、Try 阶段是做业务检查(一致性)及资源预留(隔离),此阶段仅是一个初步操作,它和后续的Confifirm 一起才能真正构成一个完整的业务逻辑。

2、Confifirm 阶段是做确认提交,Try阶段所有分支事务执行成功后开始执行 Confifirm。通常情况下,采用TCC则认为 Confifirm阶段是不会出错的。即:只要Try成功,Confifirm一定成功。若Confifirm阶段真的出错了,需引入重试机制或人工处理。

3、Cancel 阶段是在业务执行错误需要回滚的状态下执行分支事务的业务取消,预留资源释放。通常情况下,采用TCC则认为Cancel阶段也是一定成功的。若Cancel阶段真的出错了,需引入重试机制或人工处理。

TM事务管理器,TM事务管理器可以实现为独立的服务,也可以让全局事务发起方充当TM的角色,TM独立出来是为了成为公用组件,是为了考虑系统结构和软件复用。

TM在发起全局事务时生成全局事务记录,全局事务ID贯穿整个分布式事务调用链条,用来记录事务上下文,追踪和记录状态,由于Confifirm 和cancel失败需进行重试,因此需要实现为幂等,幂等性是指同一个操作无论请求多少次,其结果都相同。 与之相对的,每个分支注册的事务称为分支事务记录

为什么

TCC事务机制相比于上面介绍的XA,解决了其几个缺点:
1.单点问题:由主业务方发起并完成这个业务活动。业务活动管理器也变成多点,引入集群。
2.同步阻塞:引入超时,超时后进行补偿,并且不会锁定整个资源,将资源转换为业务逻辑形式,粒度变小
3.数据不一致:有了补偿机制之后,由业务活动管理器控制一致性

适用场景

  • 强隔离性,严格一致性要求的活动业务(能够控制粒度)。
  • 执行时间较短的业务

缺点:

​ 1、代码入侵业务

​ 2、实现较复杂

TCC需要注意三种异常处理

空回滚:

​ 未Try就Cancel,直接返回成功

幂等:

​ Try、Confirm、Cancel需要保证幂等,可重复执行以支持TCC的提交重试机制

悬挂:

​ Cancel后再Try,会造成资源被“占用“,要求二阶段即Cancel执行后,不允许执行一阶段即Try

经典案例A 转账 30 元给 B,A和B账户在不同的服务

方案一

账户A:

1
2
3
4
5
try
1、检查余额是否够30
2、扣减30
confirm:空
cancel:增加30

账户B:

1
2
3
try:增加30
confirm:空
cancel:减少30

存在的问题:1、无幂等、空回滚、悬挂校验 2、B在try时新增30可能会被别的线程消费

优化方案

账户A:

1
2
3
4
5
6
7
8
9
try:
1、try幂等校验
2、try悬挂校验
3、减少30元
confirm:空
cancel:
1、cancel幂等校验
2、cancel空回滚校验
3、增加30元

账户B:

1
2
3
4
5
try: 空
confirm
1confirm幂等校验
2、正式增加30
cancel:空

TCC 解决方案

目前市面上的TCC框架众多比如下面这几种:

框架名称 Gitbub地址
tcc-transaction https://github.com/changmingxie/tcc-transaction
Hmily https://github.com/dromara/hmily
ByteTCC https://github.com/liuyangming/ByteTCC
EasyTransaction https://github.com/QNJR-GROUP/EasyTransaction
Seata TCC模式 https://github.com/seata/seata

Saga事务及其解决方案

是什么

​ Saga是30年前一篇数据库伦理提到的一个概念(理论基础:Hector & Kenneth 发表论⽂ Sagas (1987))。其核心思想是将长事务拆分为多个本地短事务,由Saga事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则补偿前面已经成功的操作。

​ Saga的组成:每个Saga由一系列sub-transaction Ti 组成 每个Ti 都有对应的补偿动作Ci,补偿动作用于撤销Ti造成的结果,这里的每个T,都是一个本地事务。 可以看到,和TCC相比,Saga没有“预留 try”动作,它的Ti就是直接提交到库。

Saga模式示意图

Saga的执行顺序有两种:

1、T1, T2, T3, …, Tn

2、T1, T2, …, Tj, Cj,…, C2, C1,其中0 < j < n
Saga定义了两种恢复策略:

1、向后恢复:即上面提到的第二种执行顺序,其中j是发生错误的sub-transaction,这种做法的效果是撤销掉之前所有成功的sub-transation,使得整个Saga的执行结果撤销。

2、向前恢复,适用于必须要成功的场景,执行顺序是类似于这样的:T1, T2, …, Tj(失败), Tj(重试),…, Tn,其中j是发生错误的sub-transaction。该情况下不需要Ci

适用场景

  • 业务流程长、业务流程多
  • 参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口

优势

  • 一阶段提交本地事务,无锁,高性能
  • 事件驱动架构,参与者可异步执行,高吞吐
  • 补偿服务易于实现

缺点

  • 不保证隔离性

拿100元买一瓶水的例子来说,这里定义:

​ T1=扣100元 T2=给用户加一瓶水 T3=减库存一瓶水
​ C1=加100元 C2=给用户减一瓶水 C3=给库存加一瓶水
我们一次进行T1,T2,T3如果发生问题,就执行发生问题的C操作的反向。上面说到的隔离性的问题会出现在,如果执行到T3这个时候需要执行回滚,但是这个用户已经把水喝了(另外一个事务),回滚的时候就会发现,无法给用户减一瓶水了。这就是事务之间没有隔离性的问题。

三种异常处理

空补偿:原服务未执行,补偿服务执行了;允许空补偿, 即没有找到要补偿的业务主键时返回补偿成功并将原业务主键记录下来。

悬挂:补偿服务比原服务先执行;检查当前业务主键是否已经在空补偿记录下来的业务主键中存在,如果存在则要拒绝服务的执行

幂等:原服务与补偿服务都需要保证幂等性, 由于网络可能超时, 可以设置重试策略,重试发生时要通过幂等控制避免业务数据重复更新;原服务与补偿服务都需要保证幂等性, 由于网络可能超时, 可以设置重试策略,重试发生时要通过幂等控制避免业务数据重复更新

隔离性问题如何解决

1、参照华为的解决方案(Servicecomb):从业务层面入手加入一 Session 以及锁的机制来保证能够串行化操作资源;

2、在业务层面通过预先冻结资金的方式隔离这部分资源, 最后在业务操作的过程中可以通过及时读取当前状态的方式获取到最新的更新。

3、不回滚,提供“向前”恢复上下文继续执行的能力, 让业务最终执行成功, 达到最终一致性的目的。

Seata(Saga模式)

最终一致性之可靠消息方案

是什么

​ 可靠消息最终一致性方案是指当事务发起方执行完成本地事务后并发出一条消息事务参与方(消息消费者)一定能够接收消息并处理事务成功,此方案强调的是只要消息发给事务参与方最终事务要达到一致。

​ 比如利用消息中间件来完成可靠消息,如下图

image-20230321224855999

​ 事务发起方(消息生产方)将消息发给消息中间件,事务参与方从消息中间件接收消息,事务发起方和消息中间件之间,事务参与方(消息消费方)和消息中间件之间都是通过网络通信,由于网络通信的不确定性会导致分布式事务问题。因此可靠消息最终一致性方案要解决以下几个问题:

1、本地事务与消息发送的原子性问题

​ 事务发起方在本地事务执行成功后消息必须发出去,否则就丢弃消息。即实现本地事务和消息发送的原子性,要么都成功,要么都失败。本地事务与消息发送的原子性问题是实现可靠消息最终一致性方案的关键问题

先发送消息,再操作数据库:

1
2
3
4
5
begin transaction;
//1.发送MQ
//2.数据库操作
commit transation;
这种情况下无法保证数据库操作与发送消息的一致性,因为可能发送消息成功,数据库操作失败。

先进行数据库操作,再发送消息:

1
2
3
4
begin transaction;
//1.数据库操作
//2.发送MQ
commit transation;

​ 这种情况下貌似没有问题,如果发送MQ消息失败,就会抛出异常,导致数据库事务回滚。但如果是超时异常,数据库回滚,但MQ其实已经正常发送了,同样会导致不一致。

2、事务参与方接受消息的可靠性

​ 事务参与方必须能够从消息队列接收到消息,如果接收消息失败可以重复接收消息。

3、消息重复消费的问题

​ 由于网络2的存在,若某一个消费节点超时但是消费成功,此时消息中间件会重复投递此消息,就导致了消息的重复消费。要解决消息重复消费的问题就要实现事务参与方的方法幂等

本地消息表方案

​ 本地消息表这个方案最初是eBay提出的,此方案的核心是通过本地事务保证数据业务操作和消息的一致性,然后通过定时任务将消息发送至消息中间件,待确认消息发送给消费方成功再将消息删除或者修改状态。

image-20230321234442367

1、在同一事务插入消息表使原子性问题得到解决

2、定时任务轮询消息表保证了消息一定能到达事务参与方

3、通过MQ消费的ACK机制确保消费者消费了消息,消费者需要自身实现幂等

RocketMQ事务消息方案

​ RocketMQ 事务消息设计则主要是为了解决 Producer 端的消息发送与本地事务执行的原子性问题,RocketMQ 的设计中 broker 与 producer 端的双向通信能力,使得 broker 天生可以作为一个事务协调者存在;而RocketMQ本身提供的存储机制为事务消息提供了持久化能力;

​ RocketMQ 的高可用机制以及可靠消息设计则为事务消息在系统发生异常时依然能够保证达成事务的最终一致性。

image-20230322222239901

以注册送积分的例子来描述,Producer 即MQ发送方,本例中是用户服务,负责新增用户。MQ订阅方即消息消费方,本例中是积分服务,负责新增积分。执行流程如下:

1、Producer 发送事务消息
Producer (MQ发送方)发送事务消息至MQ Server,MQ Server将消息状态标记为Prepared(预备状态),注
意此时这条消息消费者(MQ订阅方)是无法消费到的。本例中,Producer 发送 ”增加积分消息“ 到MQ Server。

2、MQ Server回应消息发送成功

​ MQ Server接收到Producer 发送给的消息则回应发送成功表示MQ已接收到消息

3、Producer 执行本地事务

​ Producer 端执行业务代码逻辑,通过本地数据库事务控制。本例中,Producer 执行添加用户操作

4、消息投递

​ 若Producer 本地事务执行成功则自动向MQServer发送commit消息,MQ Server接收到commit消息后将”增加积分消息“ 状态标记为可消费,此时MQ订阅方(积分服务)即正常消费消息;若Producer 本地事务执行失败则自动向MQServer发送rollback消息,MQ Server接收到rollback消息后 将删除”增加积分消息“ 。

​ MQ订阅方(积分服务)消费消息,消费成功则向MQ回应ack,否则将重复接收消息。这里ack默认自动回应,即程序执行正常则自动回应ack。

5、事务回查

​ 如果执行Producer端本地事务过程中,执行端挂掉,或者超时,MQ Server将会不停的询问同组的其他 Producer来获取事务执行状态,这个过程叫事务回查。MQ Server会根据事务回查结果来决定是否投递消息。
​ 以上主干流程已由RocketMQ实现,对用户侧来说,用户需要分别实现本地事务执行以及本地事务回查方法,因此只需关注本地事务的执行状态即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//1、RoacketMQ提供RocketMQLocalTransactionListener接口:
//消息发送成功回调此方法,此方法执行本地事务
public interface RocketMQLocalTransactionListener {
/**
‐ 发送prepare消息成功此方法被回调,该方法用于执行本地事务
@param msg 回传的消息,利用transactionId即可获取到该消息的唯一Id
@param arg 调用send方法时传递的参数,当send时候若有额外的参数可以传递到send方法中,这里能获取到
@return 返回事务状态,COMMIT:提交 ROLLBACK:回滚 UNKNOW:回调
*/
RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg);
/**
@param msg 通过获取transactionId来判断这条消息的本地事务执行状态
@return 返回事务状态,COMMIT:提交 ROLLBACK:回滚 UNKNOW:回调
*/
RocketMQLocalTransactionState checkLocalTransaction(Message msg);
}

//2、RocketMQ提供用于发送事务消息的API:
//此方法检查事务执行状态
TransactionMQProducer producer = new TransactionMQProducer("ProducerGroup");
producer.setNamesrvAddr("127.0.0.1:9876");
producer.start();
//设置TransactionListener实现
producer.setTransactionListener(transactionListener);
//发送事务消息
SendResult sendResult = producer.sendMessageInTransaction(msg, null);

总结

​ 可靠消息最终一致性事务适合执行周期长且实时性要求不高的场景。引入消息机制后,同步的事务操作变为基于消息执行的异步操作, 避免了分布式事务中的同步阻塞操作的影响,并实现了两个服务的解耦。

最终一致性之最大努力通知

是什么

目标:发起通知方通过一定的机制最大努力将业务处理结果通知到接收方,具体包括:

1、有一定的消息重复通知机制。

​ 因为接收通知方可能没有接收到通知,此时要有一定的机制对消息重复通知。

2、消息校对机制。

​ 如果尽最大努力也没有通知到接收方,或者接收方消费消息后要再次消费,此时可由接收方主动向通知方查询消息信息来满足需求。

最大努力通知与可靠消息一致性有什么不同

1、解决方案思想不同

​ 可靠消息一致性,发起通知方需要保证将消息发出去,并且将消息发到接收通知方,消息的可靠性关键由发起通知方来保证

​ 最大努力通知,发起通知方尽最大的努力将业务处理结果通知为接收通知方,但是可能消息接收不到,此时需要接收通知方主动调用发起通知方的接口查询业务处理结果,通知的可靠性关键在接收通知方

2、两者的业务应用场景不同

​ 可靠消息一致性关注的是交易过程的事务一致,以异步的方式完成交易。

​ 最大努力通知关注的是交易后的通知事务,即将交易结果可靠的通知出去。

3、技术解决方向不同

​ 可靠消息一致性要解决消息从发出到接收的一致性,即消息发出并且被接收到。

​ 最大努力通知无法保证消息从发出到接收的一致性,只提供消息接收的可靠性机制。可靠机制:最大努力的将消息通知给接收方,当消息无法被接收方接收时,由接收方主动查询消息(业务处理结果)。

技术方案

消息通知方 + MQ(ack机制) + 接受通知方

image-20230325160143614

1、消息通知方发送通知MQ(普通消息)

2、接受通知方监听MQ

3、接收通知方接受消息成功,业务处理完成后回应ack,若无回应,则MQ会重复通知

4、接收通知方可以通过消息校对接口(通知方提供)来校对消息的一致性

消息通知方 + MQ(ack机制) + 通知程序+接受通知方

image-20230325161104590

1、消息通知方发送通知MQ(普通消息)

2、通知程序监听 MQ监听MQ

3、通知程序通过互联网接口协议(如http、webservice)调用接收通知方案接口,完成通知。
通知程序调用接收通知方接口成功就表示通知成功,即消费MQ消息成功,MQ将不再向通知程序投递通知消
息。

4、接收通知方可以通过消息校对接口(通知方提供)来校对消息的一致性

举例:充值结果通知

充值系统:通知方

账户系统:被通知方

image-20230326195650362

交互流程如下:

1、用户请求充值系统进行充值
2、充值系统完成充值将充值结果发给MQ
3、账户系统监听MQ,接收充值结果通知,如果接收不到消息,MQ会重复发送通知。接收到充值结果通知账户系
统增加充值金额
4、账户系统也可以主动查询充值系统的充值结果查询接口,增加金额

综合案例分析

​ 结合互联网金融项目中的业务场景,来进行分布式事务解决方案可行性分析

系统介绍

P2P介绍

​ P2P金融又叫P2P信贷。其中P2P是 peer-to-peer 或 person-to-person 的简写,意思是:个人对个人。P2P金融指个人与个人间的小额借贷交易,一般需要借助电子商务专业网络平台帮助借贷双方确立借贷关系并完成相关交易手续。借款者可自行发布借款信息,包括金额、利息、还款方式和时间,实现自助式借款;投资者根据借款人发布的信息,自行决定出借金额,实现自助式借贷。
​ 目前,国家对P2P行业的监控与规范性控制越来越严格,出台了很多政策来对其专项整治。并主张采用“银行存管模式”来规避P2P平台挪用借投人资金的风险,通过银行开发的“银行存管系统”管理投资者的资金,每位P2P平台用户在银行的存管系统内都会有一个独立账号,平台来管理交易,做到资金和交易分开,让P2P平台不能接触到资金,就可以一定程度避免资金被挪用的风险。
​ 什么是银行存管模式?
银行存管模式涉及到2套账户体系,P2P平台和银行各一套账户体系。投资人在P2P平台注册后,会同时跳转到银行再开一个电子账户,2个账户间有一一对应的关系。当投资人投资时,资金进入的是平台在银行为投资人开设的二级账户中,每一笔交易,是由银行在投资人与借款人间的交易划转,P2P平台仅能看到信息的流动。

image-20230410084845209

总体业务流程

image-20230410085033602

业务术语

术语 描述
银行的存管模式 此种模式下,涉及到2套账户体系,P2P平台和银行各一套账户体系。投资人在P2P平台注册后,会同时跳转到银行再开一个电子账户,2个账户间有一一对应的关系。当投资人投资时,资金进入的是平台在银行为投资人开设的二级账户中,每一笔交易,是由银行在投资人与借款人间的交易划转,P2P平台仅能看到信息的流动
标的 P2P业内,习惯把借款人发布的投资项目称为“标的”
发标 借款人在P2P平台中创建并发布“标的”过程
投标 投资人在认可相关借款人之后进行的一种借贷行为,对自己中意的借款标的进行投资操作,一个借款标可由单个投资人或多个投资人承接
满标 单笔借款标筹集齐所有借款资金即为满标,计息时间是以标满当日开始计息,投资人较多的平台多数

模块说明

统一账号服务
用户的登录账号、密码、角色、权限、资源等系统级信息的管理,不包含用户业务信息。
用户中心
提供用户业务信息的管理,如会员信息、实名认证信息、绑定银行卡信息等,“用户中心”的每个用户与“统一账号服务”中的账号关联
交易中心
提供发标、投标等业务
还款服务
提供还款计划的生成、执行、记录与归档
银行存管系统(模拟)
模拟银行存管系统,进行资金的存管,划转

注册账号

业务流程

​ 采用用户、账号分离设计(这样设计的好处是,当用户的业务信息发生变化时,不会影响的认证、授权等系统机制),因此需要保证用户信息与账号信息的一致性。

​ 用户向用户中心发起注册请求,用户中心保存用户业务信息,然后通知统一账号服务新建该用户所对应登录账号。

image-20230410093050816

解决方案分析

针对注册业务,如果用户与账号信息不一致,则会导致严重问题,因此该业务对一致性要求较为严格,即当用户服

务和账号服务任意一方出现问题都需要回滚事务。

根据上述需求进行解决方案分析:

1 、采用可靠消息一致性方案

可靠消息一致性要求只要消息发出,事务参与者接到消息就要将事务执行成功,不存在回滚的要求,所以不适用。

2 、采用最大努力通知方案

最大努力通知表示发起通知方执行完本地事务后将结果通知给事务参与者,即使事务参与者执行业务处理失败发起

通知方也不会回滚事务,所以不适用。

3 、采用Seata实现2PC

在用户中心发起全局事务,统一账户服务为事务参与者,用户中心和统一账户服务只要有一方出现问题则全局事务
回滚,符合要求。

实现方法如下:

​ 1 、用户中心添加用户信息,开启全局事务

​ 2 、统一账号服务添加账号信息,作为事务参与者

​ 3 、其中一方执行失败Seata对SQL进行逆操作删除用户信息和账号信息,实现回滚。

4 、采用Hmily实现TCC

TCC也可以实现用户中心和统一账户服务只要有一方出现问题则全局事务回滚,符合要求。

实现方法如下:

1 、用户中心

try:添加用户,状态为不可用

confirm:更新用户状态为可用

cancel:删除用户

2 、统一账号服务

try:添加账号,状态为不可用

confirm:更新账号状态为可用

cancel:删除账号

存管开户

业务流程

​ 根据政策要求,P2P业务必须让银行存管资金,用户的资金在银行存管系统的账户中,而不在P2P平台中,因此用户要在银行存管系统开户。

image-20230412083838985

​ 用户向用户中心提交开户资料,用户中心生成开户请求号并重定向至银行存管系统开户页面。用户设置存管密码并确认开户后,银行存管立即返回“请求已受理”。在某一时刻,银行存管系统处理完该开户请求后,将调用回调地址通知处理结果,若通知失败,则按一定策略重试通知。同时,银行存管系统应提供开户结果查询的接口,供用户中心校对结果。

解决方案分析

​ P2P平台的用户中心与银行存管系统之间属于跨系统交互,银行存管系统属于外部系统,用户中心无法干预银行存管系统,所以用户中心只能在收到银行存管系统的业务处理结果通知后积极处理,开户后的使用情况完全由用户中心来控制。
根据上述需求进行解决方案分析:

1 、采用Seata实现2PC

需要侵入银行存管系统的数据库,由于它的外部系统,所以不适用。

2 、采用Hmily实现TCC

TCC侵入性更强,所以不适用。

3 、基于MQ的可靠消息一致性

如果让银行存管系统监听 MQ则不合适 ,因为它的外部系统。如果银行存管系统将消息发给MQ用户中心监听MQ是可以的,但是由于相对银行存管系统来说用户中心属于外部系统,银行存管系统是不会让外部系统直接监听自己的MQ的,基于MQ的通信协议也不方便外部系统间的交互,所以本方案不合适。

4 、最大努力通知方案

银行存管系统内部使用MQ,银行存管系统处理完业务后将处理结果发给MQ,由银行存管的通知程序专门发送通
知,并且采用互联网协议通知给第三方系统(用户中心)。

image-20230412084557915

满标审核

业务流程

​ 在借款人标的募集够所有的资金后,P2P运营管理员审批该标的,触发放款,并开启还款流程。

解决方案分析

根据上述需求进行解决方案分析:

1 、采用Seata实现2PC

Seata在事务执行过程会进行数据库资源锁定,由于事务执行时长较长会将资源锁定较长时间,所以不适用。

2 、采用Hmily实现TCC

本需求对业务一致性要求较低,因为生成还款计划的时长较长,所以不要求交易中心修改标的状态为“还款中”就立
即生成还款计划 ,所以本方案不适用。

3 、基于MQ的可靠消息一致性

满标审批通过后由交易中心修改标的状态为“还款中”并且向还款服务发送消息,还款服务接收到消息开始生成还款
计划,基本于MQ的可靠消息一致性方案适用此场景 。

4 、最大努力通知方案

满标审批通过后由交易中心向还款服务发送通知要求生成还款计划,还款服务并且对外提供还款计划生成结果校对
接口供其它服务查询,最大努力 通知方案也适用本场景 。

总结

能不用分布式事务就不用

​ 能不用分布式事务就不用,如果非得使用的话,结合自己的业务分析,看看自己的业务比较适合哪一种,是在乎强一致,还是最终一致即可。上面对解决方案只是一些简单介绍,如果真正的想要落地,其实每种方案需要思考的地方都非常多,复杂度都比较大,所以最后再次提醒一定要判断好是否使用分布式事务。

没有银弹

​ 无论是数据库层的XA、还是应用层TCC、可靠消息、最大努力通知等方案,都没有完美解决分布式事务问题,它们不过是各自在性能、一致性、可用性等方面做取舍,寻求某些场景偏好下的权衡。

合理的服务拆分

​ 必须使用分布式事务时,若某系统频繁且不合理的使用分布式事务,应首先从整体设计角度观察服务的拆分是否合理,是否高内聚低耦合?是否粒度太小?一个合理的服务划分可以大大减少不必要的分布式事务场景。

各方案特点总结

2PC TCC Saga 可靠消息 最大努力通知
一致性 强一致性 强一致性 强一致性 最终一致 最终一致
吞吐量
实现复杂度 较难
特性/方案 XA AT TCC SAGA
一致性 强一致 强一致 强一致 强一致
代码入侵 较强
数据库事务 支持XA 事务的数据库 支持本地 ACID 事务的关系型数据库 不依赖 不依赖
实现复杂度 简单 简单 复杂 较复杂

强一致性方案

分布式事务模式 介绍 技术栈
XA 分布式强一致性的解决方案,依赖支持XA 事务的数据库,但性能低而使用较少。 seata、shardingsphere
AT 无侵入的分布式事务解决方案,依赖支持本地 ACID 事务的关系型数据库,适用于不希望对业务进行改造的场景,几乎0学习成本(sql都由框架托管统一执行) seata、shardingsphere
TCC 高性能分布式事务解决方案,适用于核心系统等对性能有很高要求的场景(不适合长事务和嵌套事务(service a->service b->service c)),实现较为灵活,代码入侵大(应用自己定义数据操作的粒度,使得降低锁冲突、提高吞吐量成为可能,对应用的侵入性非常强,业务逻辑的每个分支都需要实现try、confirm、cancel三个操作。此外,其实现难度也比较大) seata、service-comb
Saga 长事务解决方案,适用于业务流程长且需要保证事务最终一致性的业务系统(存在隔离性问题),代码存在入侵。 seata、shardingsphere、service-comb