从传统事务到分布式事务

从系统架构上来区分,分为单机事务(传统事务)、分布式事务,在数据处理上,分布式事务又分为刚性事务(强一致性)、柔性事务(弱一致性)。
本篇将糅合前两篇的文章,从浅至深,归纳为整篇文章,篇幅过长,目的在于通过循序渐进的方式,看到最后能把事务在整体上串起来,更容易让人理解,有什么建议,请留痕,看到回复,我会第一时间进行更改

附件,百度网盘:
链接,https://pan.baidu.com/s/1if5E9cRX7iCdf-OBYqZdBA
提取码:f9om

事务

什么是事务

事务是指一组操作,要保证这一组写入操作(新增、修改、删除)同时成功或同时失败。
可以看做是一次大的活动,由不同的小活动组成,这些活动要一起成功,或失败,在数据层面上,可以理解为从一个正确的状态,转变到另一个正确的状态

事务的四大特性ACID

  1. A(Atomic)原子性
    最小的执行单位,不允许对事务再次进行分割。事务本身是一组操作,不允许对事务再次进行分割也就是说这一组操作不能再次进行分割。大白话就是说,这一组操作要么都执行完成,要么全部不执行,不可能出现部分成功部分失 败的情况。
  2. C(Consistency)一致性
    在事务执行前后,数据库的一致性约束没有被破坏,我的理解就是从一个正确状态到另一个正确状态
  3. I(Isolation)隔离性
    被访问,就肯定会有并发,那么并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间是独立的;
  4. D(Durability)持久性
    事务完成之后,该事务对数据的更改会被持久化到数据库,对数据库中数据的改变是持久性的

我认为事务的ACID四大特性,其实就是能够通过AID来保证这个C的过程,C是目的,AID都是手段。

数据库的隔离级别

并发时,需要通过数据库的隔离级别保证在并发时事务的正确性,在了解隔离级别之前,我们需要先了解并发事务导致的问题:

  1. 脏读
    如:A事务进行了数据修改操作,但是还未提交,此时B事务来了,读取数据时,读取到了A事务中尚未提交的数据。这就是脏读,概括起来就是:一个事务读到了另一个事务尚未提交的数据,对数据单条记录来说的
  2. 幻读
    幻读是对数据条数来说的,举例:A事务中含有2次查询操作,在第一次查询的时候返回了5条数据,在进行第二次查询操作的时候,B事务对数据进行了删除或新增,那么A事务在第二次查询操作的时候,与第一次查询操作返回的结果不同,这就是幻读,一个事务执行了ddl操作并提交,被另一个未提交的事务读到了,这是对一个数据的结果集来说的
  3. 不可重复读
    同幻读类似,在一次事务中发生多次查询操作,多次查询操作读取到的数据不一致。举例:A事务读取了一条记录,比如其中一个字段电话号码为XXX,同样在进行下一层查询操作的时候,B事务修改并提交了事务,将电话号码改为了****,那么A事务在下一次读取的时候,数据就不一致了,这就是不可重复读。一个未提交事务读到了另一个事务提交的数据,对数据单条记录来说的

总结起来就是,一个为提交事务读到了另一个事务提交或未提交的数据。

接下来我们来说,数据库的隔离级别

  1. 读未提交(READ-UNCOMMITTED)
    最低的隔离级别,可发生脏读、幻读、不可重复读
  2. 读已提交(READ-COMMITTED)
    允许读取并发事务已经提交的数据,所以可以防止脏读,但是不可避免幻读和不可重复读
  3. 可重复读(REPEATABLE-READ)
    对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
  4. 串行化(SERIALIZABLE)
    最高级别,可防止脏读、幻读、不可重复读,安全由低到高,性能由高到低

mysql默认的隔离级别是可重复读
orcl默认的隔离级别是读已提交

事务的传播特性

事务的传播特性,指的是spring中的事务,之所已在这提起,也是因为面试当中,经常被问到。

单机事务(传统事务)

单机事务是通过关系型数据库来控制事务,就是利用数据库本身的事务特性来实现,因此单机事务,也叫数据库事务,而数据库通常和应用在同一个服务器,所以基于关系型数据库的事务又被称为本地事务。单机事务,没什么好描述的。了解了上面的隔离级别、四大特性,基本就了解单机事务了。

其实更简单的区分单机事务与分布式事务,是通过事务的提交与回滚,若一个事务的提交或者回滚不在同一个资源中(数据库资源或者应用资源),就是分布式事务,在下面的分布式事务,我会详细说明。

分布式事务

什么是分布式事务

在了解一个东西前,先了解这个名称的含义。随着互联网的快速发展,软件系统由原来的单体应用转变为分布式应用,下图描述了单体应用向微服务的演变:
在这里插入图片描述

分布式系统把一个单体应用,拆分成多个可独立部署的应用,因此需要服务与服务之间远程协助才能完成事务的操作,这种分布式环境下产生的事务称之为分布式事务
比如:

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

但是在分布式环境下,就变成了这样

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

可以想一下,当本地事务提交,而远程事务,因为网络原因并未及时返回,则抛异常,但实际上远程事务已执行成功了,此时,两边的数据就不一致了。

因此,单机事务的方式在分布式环境中根本就不适用。

分布式事务场景

  1. 跨JVM进程产生的分布式事务
    1.1、典型的场景就是服务与服务之间通过远程调用来完成事务操作,比如订单服务、库存服务。
    //TODO 图

    1.2、多个服务访问同一个数据库,也是跨JVM进程产生的分布式事务,两个服务持有了不同的数据库连接进行数据库操作。
    //TODO 图

  2. 跨数据库产生的分布式事务
    单体系统访问多个数据库,由于数据分布在不同的数据库中,需要通过不同的数据库连接去操作数据,此时产生了分布式事务。
    //TODO 图

通过这几个场景分析,其实不难发现,真正产生分布式事务场景的原因:就是一笔业务操作中,因为不同的数据库连接去操作数据库

分布式理论

CAP
Consistency一致性

一致性是指写操作后的读操作可以读取到最新的数据状态,当数据分布在多个节点上,从任意结点读取到的数据都是最新的状态,是对载体数据来说的

如何实现一致性?

写入主数据库后要将数据同步到从数据库,写入主数据库后,在向从数据库同步期间要将从数据库锁定,待同步完成后再释放锁,以免在新数据写入成功 后,向从数据库查询到旧的数据。

分布式系统一致性的特点:
  1. 因为多了个数据同步的过程,所以写操作的响应会增加一定的延迟。
  2. 为了保证数据一致性会对资源暂时锁定,待数据同步完成释放锁定资源,所以在一定程度上,也降低了读库的性能。
  3. 如果请求数据同步失败的结点则会返回错误信息,一定不会返回旧数据。(这是啥意思)//TODO
Availability可用性

在程序运行时,保证服务可用,任何操作都可以得到响应结果,且不会出现响应超时或响应错误。

如何实现可用性?
  1. 写入主数据库后要将数据同步到从数据库,由于要保证从数据库的可用性不可将从数据库中的资源进行锁定
  2. 即时数据还没有同步过来,从数据库也要返回要查询的数据,哪怕是旧数据, **或者 **如果连旧数据也没有则可以 **按照约定返回一个默认信息 **,但绝不能返回错误或响应超时
分布式系统可用性的特点

所有请求都有响应,且不会出现响应超时或响应错误。
不能保证数据的一致性

Partition tolerance分区容忍性

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

如何实现分区容忍性?

使用异步代替同步,这样在一次请求时,结点之间无法通信,但不影响对外服务。另一个则是添加从节点,其中一个节点挂了,其他从节点可以继续提供服务

分布式系统分区容忍性的特点

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

CAP组合方式

在所有分布式事务场景中不会同时具备CAP三个特性,因为在具备了P的前提下C和A是不能共存的。

  1. 如果要实现C保证数据一致性,那么在同步数据的时候,就只能采取同步的方式,并且锁定从数据库的数据,若同步失败,则返回错误信息或超时信息
  2. 如果要实现A保证数据可用性,则不管任何时候都可以向从数据查询数据,并返回数据,无论新旧数据,不会响应超时或返回错误信息

所以,如果要求C一致性的话,但是A可用性在查询的时候,有可能会查到旧数据,保证不了数据的一致性,所以C与A是相互矛盾的,进而两者是不能共存的,结果就只有CP与AP了。

CA

放弃分区容忍性,即不进行分区,不考虑由于网络不通或结点挂掉的问题,则可以实现一致性和可用性。要求的条件环境只是理想化的,系统就不是一个标准的分布式系统,最常用的关系型数据就满足了CA。

CP

放弃可用性,追求一致性和分区容错性,比如zookeeper其实就是追求的强一致,同步数据满足一定条件才算完成(同步半数节点以上)。

AP

放弃一致性,追求分区容忍性和可用性,这是很多分布式系统设计时的选择。

总结

CAP是一个已经被证实的理论:一个分布式系统最多只能同时满足 一致性(Consistency)、可用性(Availability)和分区容忍性(Partition tolerance)这三项中的两项。它可以作为我们进行架构设计、技术选型的考量标准。对于多数大型互联网应用的场景,结点众多、部署分散,而且现在的集群规模越来越大,所以节点故障、网络故障是常态,而且要保证服务可用性达到N个9(99.99…%)(一般在描述可用性的时候,使用几个9来形容,比如3个9,那就是保证服务全年99.9%的可用率),并要达到良好的响应性能来提高用户体验,因此一般都会做出如下选择:保证P和A,舍弃C强一致,保证最终一致性。,其次,同样为了保证服务的可用,也允许采取一定的机制来保持程序的正常运行,比如熔断、降级、限流

BASE
  1. Basically Available基本可用
  2. Soft State软状态
  3. Eventual Consistency最终一致性

BASE理论是对CAP中AP的一个扩展,通过牺牲强一致性来获得可用性,当出现故障允许部分不可用但要保证核心功能可用,允许数据在一段时间内是不一致的,但最终达到一致状态。满足BASE理论的事务,我们称之为柔性事务
CAP中的一致性要求在任何时间查询每个结点数据都必须一致,它强调的是强一致性,但是最终一致性是允许可以在一段时间内每个结点的数据不一致,但是经过一段时间每个结点的数据必须一致,它强调的是最终数据的一致性。
所以,可以做个对比,满足CAP中CP事务,就是刚性事务,满足BASE理论的事务,又因为BASE理论是对AP进行的扩展,所以我们称之为柔性事务

为什么说是对CAP中AP的扩展

为什么说是对CAP中AP的扩展呢,因为AP的前提是保证了可用性,没有对事务的要求进行定义,意思就是,我为了保持可用性,在发生异常后,异常的结果数据就不管了,但是我们需要事务后的数据保持一致性,否则,即使保持了可用,但数据是错误的,那又有什么意义呢,所以BASE理论更多的是对AP中的事务提出的要求。

分布式事务解决方案

根据分布式理论,分布式事务解决方案,又被分为刚性事务与柔性事务。刚性事务就是保持数据强一致性,柔性事务,容纳中间过程短时间的不一致,追求数据的最终一致性。

刚性事务(CP)
2PC

2PC中的2表示的意思就是两个阶段,第一个阶段是准备阶段Prepare phase,预留资源,第二个阶段是提交阶段Commit phase,提交资源,但是由谁来调度阶段呢, 所以2PC中需要两个角色:事务管理器、事务参与者。

  1. 准备阶段
    事务管理器给每个参与者发送Prepare消息,每个数据库参与者在本地执行事务,并写本地的Undo/Redo日志,此时事务没有提交。 (Undo日志是记录修改前的数据,用于数据库回滚Redo日志是记录修改后的数据,用于提交事务后写入数据文件
  2. 提交阶段
    事务管理器收到了参与者的执行失败或者超时消息时,直接给每个参与者发送回滚(Rollback)消息或者提交(Commit)消息;
    参与者根据事务管理器的指令执行提交或者回滚操作,并释放事务处理过程中使用的锁资源
    注意:必须在最后阶段释放锁资源
    成功时
    在这里插入图片描述
    失败时
    在这里插入图片描述
举例

比如说两个人AA制去饭店吃饭,其中
准备阶段:老板要求张三付款,张三付款。老板要求李四付款,李四付款。
提交阶段:老板出票,两人拿票纷纷落座就餐。
例子中就形成了一个事务,若张三或李四其中一人拒绝付款,或钱不够,店老板都不会给出票,并且会把已收款退回

整个事务过程中,店老板充当了调度的作用,负责决策整个事务的提交或回滚,两人相当于本地事务,负责自己的本地事务的提交或回滚

基于数据库XA协议的2PC方案

什么是XA协议呢,2PC方案在数据库层面执行,那么就需要数据库提供支持,如常用的Oracle、Mysql等数据库,所以为了统一标准,减少行业内不必要的对接成本,需要制定标准化的处理模型及接口标准,国际开放标准组织Open Group定义了分布式事务处理模型 DTP(Distributed Transaction Processing Reference Model)。

DTP模型定义如下角色

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

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

交互流程如下

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

比如:其实就是AP应用程序通过调用TM事务管理器(中间件)提供的接口,来达到多个RM数据库的控制。
在这里插入图片描述

XA总结及优缺点
  1. 在准备阶段RM执行实际的业务操作,但不提交事务,资源锁定;
  2. 在提交阶段TM会接受RM在准备阶段的执行回复,只要有任一个RM执行失败,TM会通知所有RM执行回滚操 作,否则,TM将会通知所有RM提交该事务。提交阶段结束资源锁释放。

XA方案的缺点:

  1. 需要本地数据库支持XA协议,所在在实现上设置了局限性。
  2. 性能较差,资源锁需要等到两个阶段结束才释放,导致系统整体的并发吞吐量变低。
  3. 单点故障问题,事务协调者在链路中有着至关重要的作用,一旦协调者发生故障,参与者会一直阻塞下去,整个系统将无法工作,因此需要投入巨大的精力来保障事务协调者的高可用性。
  4. 数据不一致问题,在阶段二中,如果协调者向参与者发送 commit 请求之后,发生了网络异常,会导致只有一部分参与者接收到了 commit 请求,没有接收到 commit 请求的参与者最终会执行回滚操作,从而造成数据不一致现象。在抢购业务中,这样的数据不一致有可能会对企业或消费者造成巨大的经济损失。
  5. 但其实现简单。

因此 XA 模型是一个理想化的分布式事务模型,并没有考虑到高并发、网络故障等实际因素,即便是在两阶段提交的基础上,诞生了三阶段提交这样的实现方式,也没有办法从根本上解决性能和数据不一致的问题。

Seata方案(阿里开源项目)

Seata是一个开源的分布式框架,XA方案中的2PC问题在Seata中得到了解决,其本质就是通过中间件记录数据库日志,在第一阶段,本地事务就提交事务,由于Seata记录了日志,所以提交时什么都不做,但回滚时,需要根据Seata记录的日志,进行更新操作

Seata有两种模式,一中时AT模式,即2PC,另一种是TCC模式,即下面说的TCC分布式方案

Seata把一个分布式事务理解成一个包含了若干分支事务的全局事务。全局事务的职责是协调其下管辖的分支事务达成一致,要么一起成功提交,要么一起失败回滚。此外,通常分支事务本身就是一个关系数据库的本地事务
在这里插入图片描述
与XA相比,Seata也定义了3个组件来协助分布式事务的处理过程

  1. TC( Transaction Coordinator)事务协调器
    它是独立的中间件,需要独立部署运行,它维护全局事务的运行状态,接收TM指令发起全局事务的提交与回滚,负责与RM通信协调各各分支事务的提交或回滚。
  2. TM(Transaction Manager)事务管理器
    TM需要嵌入应用程序中工作,它负责开启一个全局事务,并最终向TC发起全局提交或全局回滚的指令
  3. RM(Resource Manager)资源管理器
    控制分支事务,负责分支注册、状态汇报,并接收事务协调器TC的指令,驱动分支(本地)事务的提交和回滚。

XA中的RM、TM、AP,与Seata中的RM、TM、TC,其中RM都可以理解为数据库资源,TM是事务管理器,都是嵌套在应用程序中,其中不同就是AP与TC了,一个是应用程序,一个是单独不是的协调器。

在这里插入图片描述

Seata举例

是不是看不懂上面的定义,我也看不懂,那就举个例子来吧。
还用用户注册送积分为例
在这里插入图片描述
具体的执行流程如下:

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

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

模拟转账交易流程

详情请看附件,相应章节。
在这里插入图片描述

  1. 正常提交流程
    在这里插入图片描述
  2. 回滚流程,图中省略了注册流程
    在这里插入图片描述

要点说明

  1. TM开启全局事务开始,将XID全局事务id放在事务上下文中,通过feign调用也将XID传入下游分支事务,每个分支事务将自己的Branch ID分支事务ID与XID关联。
  2. 在第一阶段undo_log中就存放了数据修改前和修改后的值,为事务回滚作好准备,所以第一阶段完成就已经将分 支事务提交,也就释放了锁资源。
  3. 第二阶段全局事务提交,TC会通知各各分支参与者提交分支事务,在第一阶段就已经提交了分支事务,这里各各参与者只需要删除undo_log即可,并且可以异步执行,第二阶段很快可以完成。
  4. 第二阶段全局事务回滚,TC会通知各各分支参与者回滚分支事务,通过 XID 和 Branch ID 找到相应的回滚日 志,通过回滚日志生成反向的 SQL 并执行,以完成分支事务回滚到之前的状态,如果回滚失败则会重试回滚操 作。

通过以上叙述,我们应该不难发现,Seata的二阶段如果是都是commit提交,那倒不是难点,但太理想化了,不可能总是commit提交的,所以它的难点是二阶段的回滚才是Seata设计中的难点,就是产生的脏数据问题,这才是个关键,目前Seata通过全局锁的来加强事务的隔离性

柔性事务(AP+BASE)
TCC

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

confim流程

在这里插入图片描述
cancel流程
在这里插入图片描述

与2PC的区别
  1. 在实现上,2PC方案,不管是XA协议还是seata方案,都是基于数据库层面进行操作,而TCC是基于应用服务方面
  2. 在方案上,2PC的一阶段是预留资源,但其一阶段的执行结果,就是事务成功后的结果,而TCC的预留资源,是通过添加字段,设置一个中间状态的方式,后续confirm操作之后的结果才是事务成功后的最终结果。
  3. 在回滚上,2PC需要根据日志进行回滚,TCC则只需要根据数据库记录进行回滚即可

所以,2PC与TCC还是不同的,TCC要求每个分支事务实现TCC三个操作,在业务方面侵入性较强,还需要在数据库设计上支持中间状态,实现复杂,也是由于中间状态的设计,所以TCC也被归为柔性事务

tcc的三个阶段
  1. try阶段
    Try 阶段是做业务检查(一致性)及资源预留(隔离),此阶段仅是一个初步操作,通过中间状态预留资源,它和后续的Confirm 一起才能 真正构成一个完整的业务逻辑,所以与2PC
  2. confirm阶段
    Confirm 阶段是做确认提交,将中间状态进行确认,达到最终的结果。通常情况下,采用TCC则认为 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。
    若Confirm阶段真的出错了,需引 入重试机制或人工处理。
  3. Cancel阶段
    是在业务执行错误需要回滚的状态下执行分支事务的业务取消,预留资源释放。通常情况下,采 用TCC则认为Cancel阶段也是一定成功的。若Cancel阶段真的出错了,需引入重试机制或人工处理。
TM事务管理器

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

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

具体实现

参考博客:https://www.cnblogs.com/jajian/p/10014145.html
举个例子:完成一个订单服务,还需要完成其他操作

  1. 更改订单的状态为“已支付”
  2. 扣减商品库存
  3. 给会员增加积分
  4. 创建销售出库单通知仓库发货
    在这里插入图片描述
    其实TCC方案的是实现,需要在数据库设计上,增加几个在Try阶段使用的字段,作为中间态进行中转的作用,比如本来库存数量是 100,你别直接 100 - 2 = 98,扣减这个库存!你可以把可销售的库存:100 - 2 = 98,设置为 98 没问题,然后在一个单独的冻结库存的字段里,设置一个 2。也就是说,有 2 个库存是给冻结了。你可以保持积分为 1190 不变,在一个预增加一个字段,比如说 prepare_add_credit 字段,设置一个 10,表示有 10 个积分准备增加。
    这个也是锁,但是它锁的只是自己的资源,类似于分段锁。

Try阶段
在这里插入图片描述
Confirm阶段
在这里插入图片描述
Cancel阶段
在这里插入图片描述

TCC的空回滚

没有调用 Try 方法的情况下,调用了二阶段的 Cancel 方法,Cancel 方法需要识别出这是一个空回滚,然后直接返回成功。

出现原因是分支事务所在服务宕机或网络异常,所以分支事务调用失败,并且这个时候其实是没有执行Try阶段,当故障恢复后,分布式事务进行回滚则会调用二阶段的Cancel方法,从而形成空回滚。

解决思路是关键就是要识别出这个空回滚。思路很简单就是需要知道一阶段是否执行,如果执行了,那就是正常回 滚;如果没执行,那就是空回滚。前面已经说过TM在发起全局事务时生成全局事务记录,全局事务ID贯穿整个分布式事务调用链条。再额外增加一张分支事务记录表,其中有全局事务 ID 和分支事务 ID,第一阶段Try 方法里会插入一条记录,表示一阶段执行了。Cancel 接口里读取该记录,如果该记录存在,则正常回滚;如果该记录不存 在,则是空回滚空回滚

从这一点,我们应该能体会到,2PC与TCC的共同难点都是在回滚的时候,如何保证的一致性,可见回滚的复杂性

幂等

通过前面介绍已经了解到,为了保证TCC二阶段提交重试机制不会引发数据不一致,要求 TCC 的二阶段 Confirm 和 Cancel 接口保证幂等,这样不会重复使用或者释放资源。如果幂等控制没有做好,很有可能导致数据 不一致等严重问题。

解决思路在上述“分支事务记录”中增加执行状态,每次执行前都查询该状态,为什么是状态呢,因为Confirm和Cancel都是对这条分支事务的一个固定状态进行修改的,所以在执行时,加上where状态的条件限制即可。

悬挂

是对于一个分布式事务,其二阶段 Cancel 接口比 Try 接口先执行。 出现原因是在 RPC 调用分支事务try时,先注册分支事务,再进行RPC调用执行Try方法,如果此时 RPC 调用的网络发生拥堵, 通常 RPC 调用是有超时时间的,RPC **超时 **以后, TM就会通知RM回滚整个分布式事务,回滚完成后,Try请求才到达参与者真正执行,也就是说,因为网络的原因,导致Try调用超时,进而导致TM进行事务回滚,此时Try资源带到达事务参与方,进行预留资源。

而一个 Try 方法预留的业务资源,只有该分布式事务才能使用,该分布式事务第一阶段预留的业务资源就再也没有人能够处理了,对于这种情况,我们就称为悬挂,即业务资源预留后没法继续处理

解决思路是我们执行Confirm或Cancel方法,都会往分支事务表插入一条记录,二阶段执行完成,一阶段就不能再继续执行。所以在执行一阶段事务时,判断在该全局事务下,“分支事务记录”表中是否已经有二阶段事务记录,如果有则不执行Try

TCC方案中,通过查询分支事务表的方式,来保证一阶段与二阶段的一致性

最大努力通知
什么是最大努力通知

在这里插入图片描述

  1. 账户系统调用充值系统接口
  2. 充值系统完成支付处理向账户系统发起充值结果通知,若通知失败,则充值系统按策略进行重复通知
  3. 账户系统接收到充值结果通知修改充值状态。
  4. 账户系统未接收到通知会主动调用充值系统的接口查询充值结果

我认为其中努力通知体现在,消费者通过一定的机制最大努力将业务处理结果通知到生产者,在于下游系统,再收不到反馈的话,则生产者直接调用消费者接口。

最大努力通知与可靠消息一致性有什么不同
  1. 解决方案思想不同
    可靠消息一致性,发起通知方需要保证将消息发出去,并且将消息发到接收通知方,消息的可靠性关键由发起通知方来保证。
    最大努力通知,消费者尽最大的努力将业务处理结果通知生产者,但是可能消息接收不到,此时需要生产者主动调用消费者的接口查询业务处理结果,通知的可靠性关键在生产者
  2. 两者的业务场景不同
    可靠消息一致性关注的是交易过程的事务一致,以异步的方式完成交易。 最大努力通知关注的是交易后的通知事务,即将交易结果可靠的通知出去
  3. 技术解决方向不同
    可靠消息一致性要解决消息从发出到接收的一致性,即消息发出并且被接收到。 最大努力通知无法保证消息从发出到接收的一致性,只提供消息接收的可靠性机制。可靠机制是,最大努力的将消 息通知给接收方,当消息无法被接收方接收时,由接收方主动查询消息(业务处理结果)。
利用MQ的ACK机制实现1

发起通知方,在业务系统中,则是消息消费者,消费完成后,将任务处理结果,努力通知给消息生产者,即接受通知方。
在这里插入图片描述
本方案是利用MQ的ack机制由MQ向接收通知方发送通知,流程如下:

  1. 发起通知方将通知发给MQ。 使用普通消息机制将通知发给MQ。 注意:如果消息没有发出去可由接收通知方主动请求发起通知方查询业务执行结果。
  2. 接收通知方监听 MQ。
  3. 接收通知方接收消息,业务处理完成回应ack。
  4. 接收通知方若没有回应ack则MQ会重复通知。 MQ会按照间隔1min、5min、10min、30min、1h、2h、5h、10h的方式,逐步拉大通知间隔 (如果MQ采用 rocketMq,在broker中可进行配置),直到达到通知要求的时间窗口上限。
  5. 接收通知方可通过消息校对接口来校对消息的一致性。
利用MQ的ACK机制实现2

在这里插入图片描述

  1. 发起通知方将通知发给MQ。 使用可靠消息一致方案中的事务消息保证本地事务与消息的原子性,最终将通知先发给MQ。
  2. 通知程序监听 MQ,接收MQ的消息。 方案1中接收通知方直接监听MQ,方案2中由通知程序监听MQ。通知程序若没有回应ack则MQ会重复通知。
  3. 通知程序通过互联网接口协议(如http、webservice)调用接收通知方案接口,完成通知。 通知程序调用接收通知方案接口成功就表示通知成功,即消费MQ消息成功,MQ将不再向通知程序投递通知消 息。
  4. 接收通知方可通过消息校对接口来校对消息的一致性。
方案1和方案2的不同点
  1. 方案1中接收通知方与MQ接口,即接收通知方案监听 MQ,此方案主要应用与内部应用之间的通知。
  2. 方案2中由通知程序与MQ接口,通知程序监听MQ,收到MQ的消息后由通知程序通过互联网接口协议调用接收通知方。方案2主要应用于外部应用之间的通知,例如支付宝、微信的支付结果通知
可靠性消息最终一致性

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

  1. 事务发起方(消息生产方)将消息发给消息中间件
  2. 事务参与方从消息中间件接收消息
    在这里插入图片描述
需要解决的问题

事务发起方和消息中间件之间,事务参与方(消息消费方)和消息中间件之间都是通过网络通信,由于网络通信的不确定性会导致分布式事务问题,所以需要解决下面几个问题

  1. (本地事务与消息发送)的原子性
    本地事务执行成功后消息必须发出去,否则就丢弃消息。即实现本地事务和消息发送的原子性,要么都成功,要么都失败。本地事务与消息发送的原子性问题是实现可靠消息最终一致性方案的关键问题。 有以下几种方式
    1.1 先发送消息,再操作数据库
    begin transaction;
    //1.发送MQ
    //2.数据库操作
    commit transation;
    这种方案最不可行,无法保证数据库操作与发送消息的一致性,因为可能发送消息成功,数据库操作失败,但是消息也追不回来了,下游系统已经将消息消费了。
    1.2先进行数据库操作,再发送消息
    begin transaction;
    //1.数据库操作
    //2.发送MQ
    commit transation;
    这种情况下看着没啥问题,如果发送MQ消息失败,就会抛出异常,导致数据库事务回滚,这个情况是对的。但如果是超时异常,但MQ其实已经正常发送了,数据库回滚,同样会导致不一致。
  2. 事务参与方接收消息的可靠性
    事务参与方必须能够从消息队列接收到消息,如果接收消息失败可以重复接收消息。
  3. 消息重复消费的问题
    由于上图中网络2的存在,若某一个消费节点超时但是消费成功,此时消息中间件会重复投递此消息,就导致了消息的重复消费。 要解决消息重复消费的问题就要实现事务参与方的方法幂等性。
本地消息表

本地消息表这个方案最初是eBay提出的,此方案的 核心是通过本地事务保证数据业务操作和消息的一致性,然后通过定时任务将消息发送至消息中间件,待确认消息发送给消费方成功再将消息删除。
其本质通过本地事务保证消息表与本地业务的一致性,然后通过定时任务,将消息发送给中间件,中间件反馈发送成功,则删除该消息日志,否则等待下一次轮巡。如:
在这里插入图片描述

该方案中,要注意一下三个地方

  1. 本地业务与消息表的一直性,需要通过本地事务来保证
  2. 保证消费者一定能消费到消息
  3. 消费方实现幂等性。网络问题,或许会导致第一次超时异常,消息表记录未被删除,第二次定时任务调起,消息被重复发送,所以消费方需要实现幂等性
  4. 消息的实时性,因为用的时定时任务的方式,所以,在时效上并不是实时的。
基于RocketMQ的消息传递

基于本地消息表的方案,通过本地事务,保证了本地事务与消息的一致性,而通过三方中间件,仍然会存在不一致性问题,首先我们明确,先发送消息,再执行本地事务,是肯定不行的,所以问题在于后发送消息时:
如果事务的参与方在执行本地事务成功后,自己宕机了,就再有没有机会发送异步消息了,因此这样做同样会造成数据不一致的问题。记住:在真实场景中,任何一个应用节点都不是 100% 可靠的,都存在宕机的可能性。

保证事务发起方与mq的一致性

所以,一个可行的方案是引入可以处理事务消息的消息队列集群,用于异步消息的中转。

  1. 首先,事务的参与方发送一笔半事务消息到消息队列,表示自己即将执行本地事务,消息队列集群在收到这个半事务消息后,不会马上进行投递,而是进行暂存
  2. 在执行完本地事务后,事务的参考方再发送一笔确认消息到消息队列集群,告知本地事务的执行状态。如果本地事务执行成功,消息队列集群会将之前收到的半事务消息进行投递;如果本地事务执行失败,消息队列集群直接删除之前收到的半事务消息,这样远程事务就不会被执行,从而保证了最终一致性。

同样,如果事务参与方在执行完本地事务后宕机了怎么办呢?这就需要消息队列集群具备回查机制:如果收到半事务消息后,在特定时间内没有再收到确认消息,会反过来请求事务参与方查询本地事务的执行状态,并给予反馈。这样,即便错过了确认消息,消息队列集群也有能力了解到本地事务的执行状态,从而决定是否将消息进行投递。

在一个微服务应用中,会存在多个对等的应用实例,这也就代表着即便一个事务参与方的实例在执行完本地事务后宕机了,消息队列集群依然可以通过这个实例的兄弟实例了解到本地事务执行的最终状态。

保证mq与消费者之间的一致性
  1. 消息队列集群在将异步消息投递到远程事务参与方的时候,由于网络不稳定,消息没能投递成功
  2. 消息投递成功了,但远程事务参与方还没来得及执行远程事务,就宕机了

这两种情况都会导致远程事务执行失败,所以需要建立一种消息重试机制,让远程事务参与方在完成任务后,告诉消息队列集群一个反馈,告知异步消息已经得到了正确的处理。否则,消息队列会在一定时间后,周期性的重复投递消息,直到它收到了来自远程事务参与方的反馈,以确保远程事务一定能执行成功
在这里插入图片描述

和事务回查机制类似,远程事务参与方也有多个对等的微服务实例,即便某个实例在没来得及执行远程事务的时候宕机,消息队列也可以将任务交给这个实例的兄弟实例来完成

在这里插入图片描述

mq结论

了解到事务消息的原理后,我们不难得出一个结论:消息队列集群在整个流程中起着至关重要的作用,如果消息队列集群不可用,所有涉及到分布式事务的业务都将中止, 因此,我们需要一个高可用的消息队列集群,能够始终保持在工作状态,即便其某个组件出现故障,也能够在短时间内自动恢复,不会影响业务,还能确保接收到的消息不丢失

通过半事务消息以及回查机制保证了,事务发起方与mq之间的消息一致性,通过消息重试机制,保证了mq与消息消费方的一致性,但不管是本地消息表方案还是mq方案,都避免不了消费方必须实现幂等机制

具体实现

见附件

总结

从单机事务到了分布式事务,从2PC一直到可靠性消息传递,在条件允许的情况下,我们尽可能选择本地事务单数据源,因为它减少了网络交互带来的性能损耗,且避免了数据弱一致性带来的种种问题。若某系统频繁且不合理的使用分布式事务,应首先从整体设计角度观察服务的拆分是否 合理,是否高内聚低耦合?是否粒度太小?分布式事务一直是业界难题,因为网络的不确定性,而且我们习惯于拿分布式事务与单机事务ACID做对比。

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

2PC
xa 数据库层面的实现方案
seata 业务侵入性小,无法解决脏数据的问题,有,但是需要加上全局事务锁,进行事务隔离,那这样以来,就又成了串行了。

TCC
seata 需要数据库设计,进行支持,每个分支事务需要实现TCC方法,业务侵入性强,实现复杂。

可靠消息传递—电商
本地消息表,定时任务发送,时效性差
基于mq
最大努力通知
基于mq,

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值