与事务隔离级别……
事务隔离级别与分布式事务?
事务隔离级别是方法的调用之间的,分布式事务是调用不同的服务之间引起的事务
手写分布式事务:
定义全局事务注解 @GloalTransaction
定义注解对应的切面,
拿到事务的管理权限

基本理论概念

一:分布式数据一致性

CAP 定理,BASE 理论,请参考:
CAP 定理与 BASE 理论

  • TX 协议:应用或者应用服务器与事务管理器的接口。
  • XA 协议:全局事务管理器与资源管理器的接口。XA 是由 X/Open 组织提出的分布式事务规范。该规范主要定义了全局事务管理器和局部资源管理器之间的接口。主流的数据库产品都实现了 XA 接口。XA 接口是一个双向的系统接口,在事务管理器以及多个资源管理器之间作为通信桥梁。之所以需要 XA 是因为在分布式系统中从理论上讲两台机器是无法达到一致性状态的,因此引入一个单点进行协调。由全局事务管理器管理和协调的事务可以跨越多个资源和进程。全局事务管理器一般使用 XA 二阶段协议与数据库进行交互。

    二:柔性事务

    柔性事务中的服务模式:
  1. 可查询操作:服务操作具有全局唯一的标识,操作唯一的确定的时间。
  2. 幂等操作:重复调用多次产生的业务结果与调用一次产生的结果相同。一是通过业务操作实现幂等性,二是系统缓存所有请求与处理的结果,最后是检测到重复请求之后,自动返回之前的处理结果。
  3. TCC(补偿事务)操作:Try 阶段,尝试执行业务,完成所有业务的检查,实现一致性;预留必须的业务资源,实现准隔离性。Confirm 阶段:真正的去执行业务,不做任何检查,仅适用 Try 阶段预留的业务资源,Confirm 操作还要满足幂等性。Cancel 阶段:取消执行业务,释放 Try 阶段预留的业务资源,Cancel 操作要满足幂等性。TCC 与 2PC(两阶段提交)协议的区别:TCC 位于业务服务层而不是资源层,TCC 没有单独准备阶段,Try 操作兼备资源操作与准备的能力,TCC 中 Try 操作可以灵活的选择业务资源,锁定粒度。TCC 的开发成本比 2PC 高。实际上 TCC 也属于两阶段操作,但是 TCC 不等同于 2PC 操作。
  4. 可补偿操作:Do 阶段:真正的执行业务处理,业务处理结果外部可见。Compensate 阶段:抵消或者部分撤销正向业务操作的业务结果,补偿操作满足幂等性。约束:补偿操作在业务上可行,由于业务执行结果未隔离或者补偿不完整带来的风险与成本可控。实际上,TCC 的 Confirm 和 Cancel 操作可以看做是补偿操作。

该规范主要有3种实现方式:TCC,MQ事务消息,本地消息表

三:两段式提交

两阶段提交协议是协调所有分布式原子事务参与者,并决定提交或取消(回滚)的分布式算法。
在两阶段提交协议中,系统一般包含两类机器(或节点):一类为协调者(coordinator),通常一个系统中只有一个;另一类为事务参与者(participants,cohorts 或 workers),一般包含多个,在数据存储系统中可以理解为数据副本的个数。协议中假设每个节点都会记录写前日志(write-ahead log)并持久性存储,即使节点发生故障日志也不会丢失。协议中同时假设节点不会发生永久性故障而且任意两个节点都可以互相通信。
两个阶段的执行
1.请求阶段(commit-request phase,或称表决阶段,voting phase) 在请求阶段,协调者将通知事务参与者准备提交或取消事务,然后进入表决过程。 在表决过程中,参与者将告知协调者自己的决策:同意(事务参与者本地作业执行成功)或取消(本地作业执行故障)。
2.提交阶段(commit phase) 在该阶段,协调者将基于第一个阶段的投票结果进行决策:提交或取消。 当且仅当所有的参与者同意提交事务协调者才通知所有的参与者提交事务,否则协调者将通知所有的参与者取消事务。 参与者在接收到协调者发来的消息后将执行响应的操作。
当第二个阶段出错了,无论是回滚还是提交事务,都要不断重试。
两阶段提交的缺点
1.同步阻塞问题。执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
2.单点故障。由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)
3.数据不一致。在二阶段提交的阶段二中,当协调者向参与者发送 commit 请求之后,发生了局部网络异常或者在发送 commit 请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了 commit 请求。 而在这部分参与者接到 commit 请求之后就会执行 commit 操作。但是其他部分未接到 commit 请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据部一致性的现象。‘
两阶段提交无法解决的问题
当协调者出错,同时参与者也出错时,两阶段无法保证事务执行的完整性。考虑协调者再发出 commit 消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。
这种分布式事务方案,比较适合单块应用里,跨多个库的分布式事务,而且因为严重依赖于数据库层面来搞定复杂的事务,效率很低,绝对不适合高并发的场景。如果要玩儿,那么基于spring + JTA就可以搞定,

解决方法

1:XA方案

最早的,基于2阶段提交的接口标准。实现了XA规范的资源管理器就可以参与XA全局事务。应用承担事务管理器TM工作,数据库承担资源管理器RM工作,TM生成全局事务id,控制RM的提交和回滚。
XA方案的问题: 1、需要本地数据库支持XA协议。 2、资源锁需要等到两个阶段结束才释放,性能较差。

2:Seata方案

Seata实现2PC与传统2PC的差别:
架构层次方面,传统2PC方案的 RM 实际上是在数据库层,RM 本质上就是数据库自身,通过 XA 协议实现,而 Seata的 RM 是以jar包的形式作为中间件层部署在应用程序这一侧的。
两阶段提交方面,传统2PC无论第二阶段的决议是commit还是rollback,事务性资源的锁都要保持到Phase2完成 才释放。而Seata的做法是在Phase1 就将本地事务提交,这样就可以省去Phase2持锁的时间,整体提高效率。

四:三段式提交

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

三个阶段的执行 1.CanCommit 阶段 3PC 的 CanCommit 阶段其实和 2PC 的准备阶段很像。 协调者向参与者发送 commit 请求,参与者如果可以提交就返回 Yes 响应,否则返回 No 响应。
2.PreCommit 阶段 Coordinator 根据 Cohort 的反应情况来决定是否可以继续事务的 PreCommit 操作。 根据响应情况,有以下两种可能。 A.假如 Coordinator 从所有的 Cohort 获得的反馈都是 Yes 响应,那么就会进行事务的预执行: 发送预提交请求。Coordinator 向 Cohort 发送 PreCommit 请求,并进入 Prepared 阶段。 事务预提交。Cohort 接收到 PreCommit 请求后,会执行事务操作,并将 undo 和 redo 信息记录到事务日志中。 响应反馈。如果 Cohort 成功的执行了事务操作,则返回 ACK 响应,同时开始等待最终指令。
B.假如有任何一个 Cohort 向 Coordinator 发送了 No 响应,或者等待超时之后,Coordinator 都没有接到 Cohort 的响应,那么就中断事务: 发送中断请求。Coordinator 向所有 Cohort 发送 abort 请求。 中断事务。Cohort 收到来自 Coordinator 的 abort 请求之后(或超时之后,仍未收到 Cohort 的请求),执行事务的中断。
3.DoCommit 阶段
该阶段进行真正的事务提交,也可以分为以下两种情况:
执行提交
A.发送提交请求。Coordinator 接收到 Cohort 发送的 ACK 响应,那么他将从预提交状态进入到提交状态。并向所有 Cohort 发送 doCommit 请求。 B.事务提交。Cohort 接收到 doCommit 请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。 C.响应反馈。事务提交完之后,向 Coordinator 发送 ACK 响应。 D.完成事务。Coordinator 接收到所有 Cohort 的 ACK 响应之后,完成事务。
中断事务
Coordinator 没有接收到 Cohort 发送的 ACK 响应(可能是接受者发送的不是 ACK 响应,也可能响应超时),那么就会执行中断事务。
三阶段提交协议的缺点
如果进入 PreCommit 后,Coordinator 发出的是 abort 请求,假设只有一个 Cohort 收到并进行了 abort 操作, 而其他对于系统状态未知的 Cohort 会根据 3PC 选择继续 Commit,此时系统状态发生不一致性。
柔性事务,执行 SQL 时实现三个接口,代码侵入性较强,不选择
事务协调器,LCN,维护全局事务的状态,

四:TCC

AT模式要求数据库必须支持事务。而TCC是业务层面的分布式事务。
TCC也是一个两阶段的提交的一种改进,try,commit,cancel的首字母,侵入性太强,改起来太复杂,每个都要写try方法,Confirm方法和Cancel方法。一方面改表结构,冻结,添加新状态。
这个其实是用到了补偿的概念,分为了三个阶段:

  • Try阶段:这个阶段说的是对各个服务的资源做检测以及对资源进行锁定或者预留
  • Confirm阶段:这个阶段说的是在各个服务中执行实际的操作
  • Cancel阶段:如果任何一个服务的业务方法try阶段执行出错,那么这里就需要进行补偿,就是执行已经执行成功的业务逻辑的回滚操作

TM生成全局事务ID,贯穿着整个分布式事务调用链条。
支持的框架:
1:tcc-transaction
2:Hmily,支持Dubbo,SpringCloud等RPC框架,Hmily利用AOP对参与分布式事务的本地方法与远程方法进行拦截处理,通过多方拦截,事务参与者能透明的 调用到另一方的Try、Confirm、Cancel方法;传递事务上下文;并记录事务日志,酌情进行补偿,重试等。
Hmily不需要事务协调服务,但需要提供一个数据库(mysql/mongodb/zookeeper/redis/file)来进行日志存 储。 Hmily实现的TCC服务与普通的服务一样,只需要暴露一个接口,也就是它的Try业务。Confirm/Cancel业务 逻辑,只是因为全局事务提交/回滚的需要才提供的,因此Confirm/Cancel业务只需要被Hmily TCC事务框架 发现即可,不需要被调用它的其他业务服务所感知。
3:ByteTCC
4:EasyTransaction

注意:

空回滚:

没有调用try方法,反倒调用了cancel方法,所以Cancel 方法需要识别出这是一个空回 滚,然后直接返回成功。
出现原因:是当一个分支事务所在服务宕机或网络异常,分支事务调用记录为失败,这个时候其实是没有执行Try阶 段,当故障恢复后,分布式事务进行回滚则会调用二阶段的Cancel方法,从而形成空回滚。
解决思路:关键就是要识别出这个空回滚。思路很简单就是需要知道一阶段是否执行,前面已经说过TM在发起全局事务时生成全局事务记录,全局事务ID贯穿整个分 布式事务调用链条。再额外增加一张分支事务记录表,其中有全局事务 ID 和分支事务 ID,第一阶段 Try 方法里会 插入一条记录,表示一阶段执行了。Cancel 接口里读取该记录,如果该记录存在,则正常回滚;如果该记录不存 在,则是空回滚。

幂等:

为了保证TCC二阶段提交重试机制不会引发数据不一致,要求 TCC 的二阶段 Try、 Confirm 和 Cancel 接口保证幂等,这样不会重复使用或者释放资源。如果幂等控制没有做好,很有可能导致数据 不一致等严重问题。 解决思路在上述“分支事务记录”中增加执行状态,每次执行前都查询该状态。

悬挂:

悬挂就是对于一个分布式事务,其二阶段 Cancel 接口比 Try 接口先执行。
原因:
在 RPC 调用分支事务try时,先注册分支事务,再执行RPC调用,如果此时 RPC 调用的网络发生拥堵, 通常 RPC 调用是有超时时间的,RPC 超时以后,TM就会通知RM回滚该分布式事务,可能回滚完成后,RPC 请求 才到达参与者真正执行,而一个 Try 方法预留的业务资源,只有该分布式事务才能使用,该分布式事务第一阶段预 留的业务资源就再也没有人能够处理了,对于这种情况,我们就称为悬挂,即业务资源预留后没法继续处理。
解决思路是如果二阶段执行完成,那一阶段就不能再继续执行。在执行一阶段事务时判断在该全局事务下,“分支 事务记录”表中是否已经有二阶段事务记录,如果有则不执行Try。
案例:
账户A
try:
检查余额是否够30元
扣减30元
confirm:

cancel:
增加30元
账户B
try:
增加30元
confirm:

cancel:
减少30元

五:可靠消息最终一致性

BASE理论的实现
可靠消息最终一致性方案是指当事务发起方执行完成本地事务后并发出一条消息,事务参与方(消息消费者)一定能 够接收消息并处理事务成功,此方案强调的是只要消息发给事务参与方最终事务要达到一致。
A减去30块,通过MQ发给B,B无论如何都能加上30块。
要解决的问题:
1:本地事务与消息发送的原子性问题
begin transaction;
//1.发送MQ
//2.数据库操作
commit transation;
这种情况下无法保证数据库操作与发送消息的一致性,因为可能发送消息成功,数据库操作失败。
begin transaction;
//1.数据库操作
//2.发送MQ
commit transation;
这种情况下,如果发送MQ消息失败,就会抛出异常,导致数据库事务回滚。但如果是超时异常,数 据库回滚,但MQ其实已经正常发送了,同样会导致不一致。
2:事务参与方接受消息的幂等性
3:消息重复消费的问题
优点

  • 基于mq的形式实现,具有重试、消息持久化机制

缺点

  • RabbitMQ实现起来比较麻烦,需要手动添加补单消费者。

    解决方案:

    1:本地消息表

    向本地日志添加增加积分的日志,然后定时任务触发扫描发送。积分添加后手动ACK。

2:RocketMQ事务消息

1:本地发送事务消息,MQ发送方发送消息到MQ Server,此时MQ将消息状态标记为Prepared (预备状态),此时这条消息是无法被消费者(MQ订阅方)消费到的。
2:MQ Server回应消息发送成功
3:本地继续执行本地事务。执行成功则自动向MQ Server发送commit消息,MQ Server接受到commit消息后将“增加积分消息”状态标记为可消费此时MQ订阅方即可正常消费消息。如果本地事务执行失败,则自动向MQ server发送rollback消息,删除“增加积分消息”
4:MQ订阅方消费消息,消费成功回应ACK,否则重复接受消息。
注意:怕commit的时候网络故障丢失消息,MQ server会发起事务回查,向发起方检查该事务是否commit

六:最大努力通知

充值成功后充值系统最大努力通知账户系统充值是否成功,并且提供查询接口,防止还通知不到可以让账户系统自己去查询。

需要解决的问题:
1:有一定的消息重复通知机制
因为接收通知方可能没有接收到通知,此时要有一定的机制对消息重复通知。
2、消息校对机制。
如果尽最大努力也没有通知到接收方,或者接收方消费消息后要再次消费,此时可由接收方主动向通知方查询消息 信息来满足需求。
与可靠消息一致性有什么不同?
1:解决方案思想不同
可靠消息一致性,消息的可靠性关键由发起通知方来保证。
最大努力通知,通知的可靠性关键在接收通知方。
2:、两者的业务应用场景不同
可靠消息一致性关注的是交易过程的事务一致,以异步的方式完成交易。
最大努力通知关注的是交易后的通知事务,即将交易结果可靠的通知出去。
3、技术解决方向不同
可靠消息一致性要解决消息从发出到接收的一致性,即消息发出并且被接收到。
最大努力通知无法保证消息从发出到接收的一致性,只提供消息接收的可靠性机制。可靠机制是,最大努力的将消 息通知给接收方,当消息无法被接收方接收时,由接收方主动查询消息(业务处理结果)。

解决方案:

对比

2PC TCC 可靠消息 最大努力通知
一致性 强一致性 最终一致性 最终一致性 最终一致性
吞吐量
实现复杂度 难(三方法)

能用本地就用本地,如果分布式事务太多就要考虑服务拆分是否合理,

解决方式:

二:AT

业务无侵入的模式,使用某种方式在commit前wait一下调用的或者被调用者,

判断是否为第一个事务,创建事务组,
用事务管理器创建本地事务:生成事务UUID,再去new一个Transaction对象
注册本地事务

事务管理者TXManager统一管理是否要将事务组进行提交,对于他的commit方法,

四:saga

先减余额,调用其他系统时如果出错再把余额加回来。称为冲正。写两个方法,一个扣款方法,一个扣款失败修复方法。

五:LCN框架

LCN并不生产事务,LCN只是本地事务的协调工
核心步骤

  1. 创建事务组:在事务发起方开始执行业务代码之前先调用TxManager创建事务组对象,然后拿到事务表示GroupId的过程。简单来说就是对这次下订单的操作在事务管理中心里创建一个对象,拿到一个id。
  2. 加入事务组:参与方在执行完业务方法后,将该模块的事务信息通知给TxManager的操作。也就是指各个数据源(各个服务)完成操作后,和事务管理中心说一声,注册一下自己。
  3. 通知事务组:发起方执行业务代码后,将发起方执行结果状态通知给TxManager,TxManager将根据事务最终状态和事务组的信息来通知相应的参与模块提交或回滚事务,并返回结果给事务发起方。和客户打交道的下订单服务会收到减库存和加订单是否成功消息,它会把这两个消息通知给事务管理者,事务管理者根据情况通知两个库存服务提交事务或回滚事务。

优点

  1. 保证数据的强一致性

缺点

  1. 可能会造成死锁的现象,比如,订单服务调用派单服务成功以后,订单服务还没执行完毕就宕机,此时,TxManage并没有收到通知,派单服务的事务也不能顺利进行,导致死锁。
  2. lcn的性能不是特别强大

六:Seata

详情参考14:SpringCloudAlibaba # Seata
seata 是立即提交,减少锁住的时间,如果第二个事务出错了,自动调用 seata 的undo_log中生成的语句对第一个事务进行回滚。
优点

  1. seata的性能比lcn要好
  2. seata不会造成死锁的情况

缺点

  1. seata没有管理化界面
  2. seata会造成数据的脏读,不能保证数据的强一致性,只能保证最终一致性

LCN与Seata的区别:
seata和lcn大致的实现思路是一致的,但是回滚的机制不一样。
lcn是采取代理数据源的模式,采用假关闭,再根据发起方执行本地事务的结果进行回滚或者提交
seata直接插入数据。采取的是根据undo_log日志表,进行逆向生成sql语句,来解决回滚。
lcn能够保证强一致性,但可能发生死锁的现象(发起方突然宕机,锁住资源)
seata能保证最终一致性,但可能造成脏读