分布式事务都有哪些
其实说到分布式事务 我们不得不提事务的分类
事务可以分为本地事务,和分布式事务,
本地事务就是单体系统下基于数据库的ACID来实现的事务,而分布式事务是指在分布式环境下保证多个系统事务一致性的问题
而分布式事务 其实又可以从cap的角度去划分,
从ap角度来看有基于MQ的柔性事务,最终一致性事务,弱一致性事务
而从cp角度来看就是刚性事务一般需要用到全局事务来保证强一致性
那我们分别来看 首先看什么是XA协议?
XA协议 其实又叫 XA规范 具体来看就是 XA规范(XA Specification) 是X/OPEN 提出的分布式事务处理规范。XA则规范了TM(事务管理器)与RM(资源管理器)之间的通信接口,在TM与多个RM之间形成一个双向通信桥梁,从而在多个数据库资源下保证ACID四个特性。
目前知名的数据库,如Oracle, DB2,mysql等,都是实现了XA接口的,都可以作为RM
那么基于XA规范下的具体实现就是2PC和3PC
XA规范存在的问题?
- 数据锁定:数据在事务未结束前,为了保障一致性,根据数据隔离级别进行锁定。
- 协议阻塞:本地事务在全局事务 没 commit 或 callback前都是阻塞等待的。
- 性能损耗高:主要体现在事务协调增加的RT成本,并发事务数据使用锁进行竞争阻塞
XA协议比较简单,而且一旦商业数据库实现了XA协议,使用分布式事务的成本也比较低。但是,XA也有致命的缺点,那就是性能不理想,特别是在交易下单链路,往往并发量很高,XA无法满足高并发场景。
那么什么是2PC呢?
2PC简单来看就是2阶段提交,
而2阶段提交协议的角色都有哪些呢?其实是将节点分为协调者(事务管理器) 和参与者角色(资源管理器)
协调者负责协调多个数据库(资源管理器)的事务,具体包括负责向参与者发送发送指令,做出提交或者回滚的决策
而参与者负责接收协调者的指令并执行事务操作,并反馈操作结果
那2PC中的两阶段是那两个阶段呢?
说白了 两阶段指的是阶段一:提交事务请求,
阶段二:执行事务提交,或者执行中断事务(即在阶段一超时或者出现异常时执行中断事务)
具体流程见下图
所以总体来看2PC解决的是分布式系统的强一致性问题,它的特点是
2PC 方案比较适合单体应用里,跨多个库的分布式事务,而且因为严重依赖于数据库层面来搞定复杂的事务,效率很低,绝对不适合高并发的场景。
它的优点是比较简单;
缺点其实也很明显
-
同步阻塞导致性能问题
从流程上我们可以看得出,其最大缺点就在于它的执行过程中间,节点都处于阻塞状态,各个操作数据库的节点此时都占用着数据库资源,只有当所有节点准备完毕,事务协调者才会通知进行全局提交,参与者进行本地事务提交后才会释放资源。
这样的过程会比较漫长,对性能影响比较大。 -
单点故障问题
事务协调者是整个XA模型的核心,一旦事务协调者节点挂掉,会导致参与者收不到提交或回滚的通知,
从而导致参与者节点始终处于事务无法完成的中间状态。 -
丢失消息导致的数据不一致的问题
在第二个阶段,如果发生局部网络问题,一部分事务参与者收到了提交消息,另一部分事务参与者没收到提交消息,那么就会导致节点间数据的不一致问题。 -
不够完善
二阶段提交协议没有设计较为完善的容错机制,任意一个节点的失败都会导致整个事务的失败。
正是因为2PC的两阶段提交有这些问题,因此引入了3PC
3PC又是什么呢?
3PC它作为2PC的改进版,它重新划分了3个阶段
即为CanCommit、PreCommit和do Commit三个阶段。
说白了这三个阶段含义就是询问,然后再锁资源,最后真正提交
整体流程如下
注意 在doCommit阶段,如果参与者无法及时接收到来自协调者的doCommit或者rollback请求时,会在等待超时之后,继续进行事务的提交。
所以对比来看 3PC它主要解决了单点故障问题
因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit。而不会一直持有事务资源并处于阻塞状态。这样一来这种机制也大大降低了整个事务的阻塞时间和影响范围。
另外,通过CanCommit、PreCommit、DoCommit三个阶段的设计,相较于2PC而言,多设置了一个缓冲阶段
保证了在最后提交阶段之前各参与节点的状态是一致的。
以上就是3PC相对于2PC的一个提高(相对缓解了2PC中的前两个问题),
但是3PC依然没有完全解决数据不一致的问题。
‘因为,由于网络原因,协调者发送的rollback命令没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到rollback命令并执行回滚的参与者之间存在数据不一致的情况。
看完了刚性事务 我们具体再来看柔性事务
具体来看柔性事务可以分为补偿型和通知型
补偿型事务包括 TCC, Saga事务
而通知型事务包括MQ事务消息,及最大努力通知型
而补偿型事务都是同步的,而通知型事务都是异步的
我们先看异步通知型事务中的MQ事务消息,
基于mq的事务消息方案 其实主要依靠MQ的半消息机制来实现投递消息和参与者自身本地事务的一致性保障。半消息机制实现原理其实借鉴的2PC的思路,是二阶段提交的广义拓展
具体流程如下
- 事务发起方首先发送半消息到MQ;
- MQ通知发送方消息发送成功;
- 在发送半消息成功后执行本地事务;
- 根据本地事务执行结果返回commit或者是rollback;
- 如果消息是rollback, MQ将丢弃该消息不投递;如果是commit,MQ将会消息发送给消息订阅
方; - 订阅方根据消息执行本地事务;
- 订阅方执行本地事务成功后再从MQ中将该消息标记为已消费;
- 如果执行本地事务过程中,执行端挂掉,或者超时,MQ服务器端将不停的询问producer来获取事
务状态; - Consumer端的消费成功机制有MQ保证;
具体实现上阿里 RocketMQ实现MQ异步确保型事务
当然如果我们用的不是rocketMQ 是其他的消息中间件并且不支持事务消息,那么我们可以基于本地消息表方案
那么MQ事务消息 与 本地消息表有啥区别?
二者的共性:
1、 事务消息都依赖MQ进行事务通知,所以都是异步的。
2、 事务消息在投递方都是存在重复投递的可能,需要有配套的机制去降低重复投递率,实现更友好的
消息投递去重。
3、 事务消息的消费方,因为投递重复的无法避免,因此需要进行消费去重设计或者服务幂等设计。
二者的区别:
MQ事务消息:
需要MQ支持半消息机制或者类似特性,在重复投递上具有比较好的去重处理;
具有比较大的业务侵入性,需要业务方进行改造,提供对应的本地操作成功的回查功能;
DB本地消息表:
使用了数据库来存储事务消息,降低了对MQ的要求,但是增加了存储成本;
事务消息使用了异步投递,增大了消息重复投递的可能性;
我们接着再看 异步通知型事务中的最大努力通知方案
最大努力通知说白了就是发起方通过一定的机制将业务处理结果通知到接收方;
最大努力通知事务主要用于外部系统,因为外部的网络环境更加复杂和不可信,所以只能尽最大努力去通知实现数据最终一致性,比如充值平台与运营商、支付对接、商户通知等等跨平台、跨企业的系统间
业务交互场景;
具体来看 要实现最大努力通知,可以采用 MQ 的 ACK 机制。
并且它在投递之前,跟异步确保流程是差不多的,只不过他又增加了通知服务用于与第三方系统的对接和投递
所以最大努力通知事务有几个特性:
业务主动方在完成业务处理后,向业务被动方(第三方系统)发送通知消息,允许存在消息丢失。
业务主动方提供递增多挡位时间间隔(5min、10min、30min、1h、24h),用于失败重试调用业务
被动方的接口;在通知N次之后就不再通知,报警+记日志+人工介入。
业务被动方提供幂等的服务接口,防止通知重复消费。
业务主动方需要有定期校验机制,对业务数据进行兜底;防止业务被动方无法履行责任时进行业务回滚,确保数据最终一致性。
那么我们在对比下 最大努力通知事务 和 异步确保型事务有啥区别呢?
最大努力通知事务在我认知中,其实是基于异步确保型事务发展而来适用于外部对接的一种业务实现。
他们主要有的是业务差别,如下:
- 从参与者来说:最大努力通知事务适用于跨平台、跨企业的系统间业务交互;异步确保型事务更适用于同网络体系的内部服务交付。
- 从消息层面说:最大努力通知事务需要主动推送并提供多档次时间的重试机制来保证数据的通知;而异步确保型事务只需要消息消费者主动去消费。
- 从数据层面说:最大努力通知事务还需额外的定期校验机制对数据进行兜底,保证数据的最终一致性;而异步确保型事务只需保证消息的可靠投递即可,自身无需对数据进行兜底处理。
看完了通知型事务 我们再来聊聊补偿型事务
补偿型事务大致可以分为TCC 和Saga模式
TCC 事务模型
TCC 分布式事务模型包括三部分:
Try 阶段: 调用 Try 接口,尝试执行业务,完成所有业务检查,预留业务资源。
Confirm 操作: 对业务系统做确认提交,确认执行业务操作,不做其他业务检查,只使用 Try 阶段预留的业务资源。
Cancel 操作: 在业务执行错误,需要回滚的状态下执行业务取消,释放预留资源。
Confirm 或 Cancel 阶段: 两者是互斥的,只能进入其中一个,并且都满足幂等性,允许失败重试。
Try 阶段失败可以 Cancel,如果 Confirm 和 Cancel 阶段失败了怎么办?
TCC 中会添加事务日志,如果 Confirm 或者 Cancel 阶段出错,则会进行重试,所以这两个阶段需要支持幂等;如果重试失败,则需要人工介入进行恢复和处理等。
那么TCC和2PC的异同点是什么呢?
TCC其实本质和2PC是差不多的:
T就是Try,两个C分别是Confirm和Cancel。
Try就是尝试,请求链路中每个参与者依次执行Try逻辑,如果都成功,就再执行Confirm逻辑,如果有失败,就执行Cancel逻辑。
不同点在于
-
**XA是资源层面的分布式事务,强一致性,在两阶段提交的整个过程中,一直会持有资源的锁。**基于数据库锁实现,需要数据库支持XA协议,由于在执行事务的全程都需要对相关数据加锁,一般高并发性能会比较差
-
**TCC是业务层面的分布式事务,最终一致性,不会一直持有资源的锁,性能较好。**但是对微服务的侵入性强,微服务的每个事务都必须实现try、confirm、cancel等3个方法,开发成本高,今后维护改造的成本也高为了达到事务的一致性要求,try、confirm、cancel接口必须实现幂等性操作由于事务管理器要记录事务日志,必定会损耗一定的性能,并使得整个TCC事务时间拉长
TCC事务的问题在哪?
- 就是它需要每个参与者都分别实现Try,Confirm和Cancel接口及逻辑,这对于业务的侵入性是巨大的。
- TCC 方案严重依赖回滚和补偿代码,最终的结果是:回滚代码逻辑复杂,业务代码很难维护。所以,TCC 方案的使用场景并不多
那TCC事务的应用场景有哪些?
比如说跟钱打交道的,支付、交易相关的场景,大家会用 TCC方案,严格保证分布式事务要么全部成功,要么全部自动回滚,严格保证资金的正确性,保证在资金上不会出现问题。
Saga长事务模型
说白了Saga模型原理 就是把一个分布式事务拆分为多个本地事务,每个本地事务都有相应的执行模块和补偿模块(对应TCC中的Confirm和Cancel),当Saga事务中任意一个本地事务出错时,可以通过调用相关的补偿方法恢复之前的事务,达到事务最终一致性。
而saga事务适合于无需马上返回业务发起方最终状态的场景,
我们接下来再看seata
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
Seata 的三大模块
Seata 分三大模块 :
TC :事务协调者。负责我们的事务ID的生成,事务注册、提交、回滚等。
TM:事务发起者。定义事务的边界,负责告知 TC,分布式事务的开始,提交,回滚。
RM:资源管理者。管理每个分支事务的资源,每一个 RM 都会作为一个分支事务注册在 TC。
在Seata的AT模式中,TM和RM都作为SDK的一部分和业务服务在一起,我们可以认为是Client。TC是一个独立的服务作为服务端,通过服务的注册、发现将自己暴露给Client们。
我们先聊聊seata的AT模式
首先我们要知道Seata AT 模式是增强型2pc模式,或者说是增强型的XA模型。总体来说,AT 模式,是 2pc两阶段提交协议的演变,不同的地方,Seata AT 模式不会一直锁表。
Seata AT模式的使用前提
1 必须是支持本地ACID事务的关系型数据库
2 java应用要通过JDBC访问数据库
Seata AT模式的工作流程
它分为2阶段提交
一阶段:业务数据和回滚日志记录在一个本地事务中提交,释放本地锁和连接资源
二阶段:提交异步化,可以非常快速的完成;或者通过一阶段的回滚日志进行反向补偿
现在以余额服务和积分服务为例个描述一下Seata AT模式的工作过程。
一个负责管理用户的余额,另外一个负责管理用户的积分。
当用户充值的时候,首先增加用户账户上的余额,然后增加用户的积分。
第一阶段过程如下:
1)余额服务中的TM,向TC申请开启一个全局事务,TC会返回一个全局的事务ID。
2)余额服务在执行本地业务之前,RM会先向TC注册分支事务。
3)余额服务依次生成undo log、执行本地事务、生成redo log,最后直接提交本地事务。
4)余额服务的RM向TC汇报,事务状态是成功的。
5)余额服务发起远程调用,把事务ID传给积分服务。
6)积分服务在执行本地业务之前,也会先向TC注册分支事务。
7)积分服务次生成undo log、执行本地事务、生成redo log,最后直接提交本地事务。
8)积分服务的RM向TC汇报,事务状态是成功的。
9)积分服务返回远程调用成功给余额服务。
10)余额服务的TM向TC申请全局事务的提交/回滚。
第二阶段其实就简单很多了,
Client和TC之间是有长连接的,如果是正常全局提交,则TC通知多个RM异步清理掉本地的redo和undo log即可。如果是回滚,则TC通知每个RM回滚数据即可。
这里如果回滚是如何实现的呢?
由于我们在操作本地业务操作的前后,做记录了undo和redo log,因此可以通过undo log进行回滚
如果某个事物从本地提交到通知回滚这段时间,这条数据被别的事务修改过,那么直接用undo log日志回滚,也会导致数据不一致,这种情况是如何处理的呢?
这种情况 其实RM会用redo log进行校验,对比数据是否一样,从而得知数据是否有别的事务修改过。
注意:undo log是被修改前的数据,可以用于回滚;redo log是被修改后的数据,用于回滚校验。
总结 seata AT模式 在电商场景中可以用于插入订单 扣减库存等一系列操作
Seata 的数据隔离性
seata的AT模式主要实现逻辑是数据源代理,这也就意味着本地事务的支持是seata 实现AT模式的必要条件
我们先看下写隔离
从前面的工作流程,我们可以很容易知道,Seata的写隔离级别是全局独占的。基于全局锁来实现分布式修改中的写隔离
一个分布式事务的锁获取流程是这样的
1)先获取到本地锁,这样你已经可以修改本地数据了,只是还不能本地事务提交
2)而后,能否提交就是看能否获得全局锁
3)获得了全局锁,意味着可以修改了,那么提交本地事务,释放本地锁
4)当分布式事务提交,释放全局锁。这样就可以让其它事务获取全局锁,并提交它们对本地数据的修改了
这里有两个关键点,1-本地锁获取之前,不会去争取全局锁,
2-全局锁获取之前,不会提交本地锁
不论是分支事务1 提交还是回滚该事务,在它未操作完成之前 全局锁都是被事务1持有,事务2是拿不到的,只有在事务1提交成功后,释放全局锁,然后事务2才能获取到全局锁(如果事务1回滚那么事务2会等全局锁等锁超时,然后放弃全局锁,并回滚本地事务释放本地锁,这时事务1的分支回滚最终成功)
读隔离
Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted) 。
如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE语句的代理。
SELECT FOR UPDATE 语句的执行会申请 全局锁 ,如果 全局锁 被其他事务持有,则释放本地锁(回滚SELECT FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被 block 住的,直到 全局锁 拿
到,即读取的相关数据是 已提交 的,才返回。
Seata at模式的使用步骤
1、引入seata框架,配置好seata基本配置,建立undo_log表
2、消费者引入全局事务注解@GlobalTransactional
3、生产者引入全局事务注解@GlobalTransactional
我们继续来聊聊Seata TCC 模式
TCC 与 Seata AT 事务一样都是两阶段事务,
它与 AT 事务的主要区别为:
1TCC 对业务代码侵入严重:每个阶段的数据操作都要自己进行编码来实现,事务框架无法自动处理。
2 TCC 性能更高:不必对数据加全局锁,允许多个事务同时操作数据。
Seata TCC 其实是由若干个分支事务组成的全局事务,而分支事务要满足两阶段提交的模型要求,即每个分支事务都具备自己的:
一阶段 prepare 行为和二阶段 commit 或 rollback 行为
SEATA Saga 模式
Saga模式是SEATA提供的长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。
它适用场景是:1业务流程长、业务流程多
2参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口
Saga模式的实现:
基于状态机引擎的 Saga 实现:
具体实现机制原理是 :
1. 通过状态图来定义服务调用的流程并生成 json 状态语言定义文件
2. 状态图中一个节点可以是调用一个服务,节点可以配置它的补偿节点
3. 状态图 json 由状态机引擎驱动执行,当出现异常时状态引擎反向执行已成功节点对应的补偿节点将事务回滚
Seata XA 模式
使用Seata XA 模式的前提
支持XA 事务的数据库。
Java 应用,通过 JDBC 访问数据库
Seata XA 模式的整体机制
在 Seata 定义的分布式事务框架内,利用事务资源(数据库、消息服务等)对 XA 协议的支持,以 XA协议的机制来管理分支事务的一种 事务模式。
从编程模型上,XA 模式与 AT 模式保持完全一致。