SpringCloud Alibaba系列——16Seata原理及应用

学习目标

  1. 了解分布式事务。
  2. 理解Seata的概念
  3. Springboot如何整合Seata

第1章 分布式系统关注点

1.1 CAP理论

  • C:consistency(强一致性):所有的节点上的数据时刻保持同步

  • A:Avaliablity(可用性):每个请求都能接受到一个响应,无论响应成功或失败

  • P:Partition Tolerance(分区容错):系统应该能持续提供服务,即使系统内部有消息丢失(分区)

高可用、数据一致是很多系统设计的目标,但是分区又是不可避免的事情:

CA:如果不要求P(不允许分区),则C(强一致性)和A(可用性)是可以保证的。但其实分区不是你想不想的问题,而是始终会存在,因此CA的系统更多的是允许分区后各子系统依然保持CA。(单体架构)

CP:如果不要求A(可用),相当于每个请求都需要在Server之间强一致,而P(分区)会导致同步时间无限延长,如此CP也是可以保证的。很多传统的数据库分布式事务都属于这种模式。

AP:要高可用并允许分区,则需放弃一致性。一旦分区发生,节点之间可能会失去联系,为了高可用,每个节点只能用本地数据提供服务,而这样会导致全局数据的不一致性。现在众多的NoSQL都属于此类。

1.2 Base理论

BASE是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网分布式系统实践的总结,是基于CAP定律逐步演化而来。其核心思想是即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。

1、Basically Available(基本可用):假设系统,出现了不可预知的故障,但还是能用,相比较正常的系统而言;

  • 响应时间上的损失:正常情况下的搜索引擎0.5秒即返回给用户结果,而基本可用的搜索引擎可以在2秒作用返回结果。

  • 功能上的损失:在一个电商网站上,正常情况下,用户可以顺利完成每一笔订单。但是到了大促期间,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面。

2、Soft State(软状态):相对于原子性而言,要求多个节点的数据副本都是一致的,这是一种“硬状态”。软状态允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据延时。

3、Eventually Consistent(最终一致性):上面说软状态,然后不可能一直是软状态,必须有个时间期限。在期限过后,应当保证所有副本保持数据一致性,从而达到数据的最终一致性。这个时间期限取决于网络延时、系统负载、数据复制方案设计等等因素。

第2章 数据一致性问题

2.1 2PC

2PC 是二阶段提交(Two-phase Commit)的缩写,顾名思义,这个协议分两阶段完成。第一个阶段是准备阶段,第二个阶段是提交阶段,准备阶段和提交阶段都是由事务管理器(协调者)发起的,协调的对象是资源管理器(参与者)。二阶段提交协议的概念来自 X/Open 组织提出的分布式事务的规范 XA 协议,协议主要定义了(全局)事务管理器和(局部)资源管理器之间的接口。XA 接口是双向的系统接口,在事务管理器以及一个或多个资源管理器之间形成通信桥梁。Java 平台上的事务规范 JTA(Java Transaction API)提供了对 XA 事务的支持,它要求所有需要被分布式事务管理的资源(由不同厂商实现)都必须实现规定接口(XAResource 中的 prepare、commit 和 rollback 等)。

  • 准备阶段:协调者向参与者发起指令,参与者评估自己的状态,如果参与者评估指令可以完成,参与者会写 redo 和 undo 日志,然后锁定资源,执行操作,但是并不提交。

  • 提交阶段:如果每个参与者明确返回准备成功,也就是预留资源和执行操作成功,协调者向参与者发起提交指令,参与者提交资源变更的事务,释放锁定的资源;如果任何一个参与者明确返回准备失败,也就是预留资源或者执行操作失败,协调者向参与者发起中止指令,参与者取消已经变更的事务,执行 undo 日志,释放锁定的资源。

SpringCloud Alibaba系列——16Seata原理及应用_第1张图片

两阶段提交协议在准备阶段锁定资源,是一个重量级的操作,并能保证强一致性,但是实现起来复杂、成本较高,不够灵活,更重要的是它有如下致命的问题:

阻塞:从上面的描述来看,对于任何一次指令必须收到明确的响应,才会继续做下一步,否则处于阻塞状态,占用的资源被一直锁定,不会被释放。

单点故障:如果协调者宕机,参与者没有了协调者指挥,会一直阻塞,尽管可以通过选举新的协调者替代原有协调者,但是如果之前协调者在发送一个提交指令后宕机,而提交指令仅仅被一个参与者接受,并且参与者接收后也宕机,新上任的协调者无法处理这种情况。

脑裂:协调者发送提交指令,有的参与者接收到执行了事务,有的参与者没有接收到事务,就没有执行事务,多个参与者之间是不一致的。

举个例子:假如一对夫妻要离婚,夫妻二人就是离婚这个事务的参与者,这个时候有一个律师则作为协调者;

首先第一阶段,律师给夫妻二人发律师函,通知二人打印离婚协议,然后在离婚协议上签字,OK,如果二人都完成了,然后就给律师一个回复通知;

第二阶段,律师收到两个人的完成通知,然后在通知夫妻二人可以去民政局,正式提交离婚。

2.2 3PC

三阶段提交协议(3PC 协议)是两阶段提交协议的改进版本。它通过超时机制解决了阻塞的问题,并且把两个阶段增加为三个阶段:

  • 询问阶段:协调者询问参与者是否可以完成指令,协调者只需要回答是还是不是,而不需要做真正的操作,这个阶段参与者在等待超时后会自动中止。

  • 准备阶段:如果在询问阶段所有的参与者都返回可以执行操作,协调者向参与者发送预执行请求,然后参与者写 redo 和 undo 日志,锁定资源,执行操作,但是不提交操作;如果在询问阶段任何参与者返回不能执行操作的结果,则协调者向参与者发送中止请求,这里的逻辑与两阶段提交协议的的准备阶段是相似的,这个阶段参与者在等待超时后会自动提交。

  • 提交阶段:如果每个参与者在准备阶段返回准备成功,也就是预留资源和执行操作成功,协调者向参与者发起提交指令,参与者提交资源变更的事务,释放锁定的资源;如果任何一个参与者返回准备失败,也就是预留资源或者执行操作失败,协调者向参与者发起中止指令,参与者取消已经变更的事务,执行 undo 日志,释放锁定的资源,这里的逻辑与两阶段提交协议的提交阶段一致。

 SpringCloud Alibaba系列——16Seata原理及应用_第2张图片

 

询问阶段:询问阶段可以确保尽可能早的发现无法执行操作而需要中止的行为,但是它并不能发现所有的这种行为,只会减少这种情况的发生。

等待超时:如果在询问阶段等待超时,则自动中止;如果在准备阶段之后等待超时,则自动提交。这也是根据概率统计上的正确性最大。

2.3 TCC

TCC 将事务的提交过程分为 try-confirm-cancel(实际上 TCC 就是 try、confirm、cancel 的简称) 三个阶段:

1、try:完成业务检查、预留业务资源

2、confirm:使用预留的资源执行业务操作(需要保证幂等性)

3、cancel:取消执行业务操作,释放预留的资源(需要保证幂等性)

①事务发起方向事务协调器发起事务请求,事务协调器调用所有事务参与者的 try 方法完成资源的预留,这时候并没有真正执行业务,而是为后面具体要执行的业务预留资源,这里完成了一阶段。

②如果事务协调器发现有参与者的 try 方法预留资源时候发现资源不够,则调用参与方的 cancel 方法回滚预留的资源,需要注意 cancel 方法需要实现业务幂等,因为有可能调用失败(比如网络原因参与者接受到了请求,但是由于网络原因事务协调器没有接受到回执)会重试。

③如果事务协调器发现所有参与者的 try 方法返回都 OK,则事务协调器调用所有参与者的 confirm 方法,不做资源检查,直接进行具体的业务操作。

④如果协调器发现所有参与者的 confirm 方法都 OK 了,则分布式事务结束。

⑤如果协调器发现有些参与者的 confirm 方法失败了,或者由于网络原因没有收到回执,则协调器会进行重试。这里如果重试一定次数后还是失败,会做事务补偿。

SpringCloud Alibaba系列——16Seata原理及应用_第3张图片

TCC与2PC的区别:

如果拿TCC事务的处理流程与2PC两阶段提交做比较,2PC通常都是在跨库的DB层面,而TCC则在应用层面的处理,需要通过业务逻辑来实现。这种分布式事务的实现方式的优势在于,可以让应用自己定义数据操作的粒度,使得降低锁冲突、提高吞吐量成为可能。 TCC的不足之处则在于对应用的侵入性非常强,业务逻辑的每个分支都需要实现try、confirm、cancel三个操作。此外,其实现难度也比较大,需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。

2.4 MQ(非事务消息)

通常情况下,在使用非事务消息支持的MQ产品时,我们很难将业务操作与对MQ的操作放在一个本地事务域中管理。通俗点描述,还是以“跨行转账”为例,我们很难保证在扣款完成之后对MQ投递消息的操作就一定能成功。这样一致性似乎很难保证。

先从消息生产者这端来分析,请看伪代码:

public void trans(){
    try{
        //1.操作数据库
        boolean result = dao.update(model);//操作数据库失败,会抛异常
        //2.如果第一步成功,则操作消息队列(投递消息)
        if(result){
            mq.append(model);//如果mq.append方法执行失败(投递消息失败),方法内部会抛异常
        }
    }catch(Exception e){
        rollback();//如果发生异常,则回滚
    }
}

根据上述代码及注释,我们来分析下可能的情况:

  1. 操作数据库成功,向MQ中投递消息也成功,皆大欢喜

  2. 操作数据库失败,不会向MQ中投递消息了

  3. 操作数据库成功,但是向MQ中投递消息时失败,向外抛出了异常,刚刚执行的更新数据库的操作将被回滚

从上面分析的几种情况来看,貌似问题都不大的。那么我们来分析下消费者端面临的问题:

  1. 消息出列后,消费者对应的业务操作要执行成功。如果业务执行失败,消息不能失效或者丢失。需要保证消息与业务操作一致

  2. 尽量避免消息重复消费。如果重复消费,也不能因此影响业务结果

如何保证消息与业务操作一致,不丢失?

主流的MQ产品都具有持久化消息的功能。如果消费者宕机或者消费失败,都可以执行重试机制的(有些MQ可以自定义重试次数)。

如何避免消息被重复消费造成的问题?

  1. 保证消费者调用业务的服务接口的幂等性 。

  2. 通过消费日志或者类似状态表来记录消费状态,便于判断(建议在业务上自行实现,而不依赖MQ产品提供该特性)。

总结:这种方式比较常见,性能和吞吐量是优于使用关系型数据库消息表的方案。如果MQ自身和业务都具有高可用性,理论上是可以满足大部分的业务场景的。不过在没有充分测试的情况下,不建议在交易业务中直接使用。

2.5 MQ(事务消息)

举个例子,Bob向Smith转账,那我们到底是先发送消息,还是先执行扣款操作? ​ 好像都可能会出问题。如果先发消息,扣款操作失败,那么Smith的账户里面会多出一笔钱。反过来,如果先执行扣款操作,后发送消息,那有可能扣款成功了但是消息没发出去,Smith收不到钱。除了上面介绍的通过异常捕获和回滚的方式外,还有没有其他的思路呢?

下面以阿里巴巴的RocketMQ中间件为例,分析下其设计和实现思路。

RocketMQ第一阶段发送Prepared消息时,会拿到消息的地址,第二阶段执行本地事物,第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。细心的读者可能又发现问题了,如果确认消息发送失败了怎么办?

RocketMQ会定期扫描消息集群中的事物消息,这时候发现了Prepared消息,它会向消息发送者确认,Bob的钱到底是减了还是没减呢?如果减了是回滚还是继续发送确认消息呢?RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。如下图:

SpringCloud Alibaba系列——16Seata原理及应用_第4张图片

 总结:各大知名的电商平台和互联网公司,几乎都是采用类似的设计思路来实现“最终一致性”的。这种方式适合的业务场景广泛,而且比较可靠。不过这种方式技术实现的难度比较大。目前主流的开源MQ(ActiveMQ、RabbitMQ、Kafka)均未实现对事务消息的支持,所以需二次开发或者新造轮子。

2.6 如何保证幂等性

  • 衰减重试

  • 消息重发

2.7 幂等的实现

判断重复请求。

  • 状态机

  • 生成token (唯一标记)

  • 数据库的唯一约束(Unique Key) -> ( MD5 )

  • redis 。 setNX

第3章 Seata

Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。在 Seata 开源之前,Seata 对应的内部版本在阿里经济体内部一直扮演着分布式一致性中间件的角色,帮助经济体平稳的度过历年的双11,对各BU业务进行了有力的支撑。经过多年沉淀与积累,商业化产品先后在阿里云、金融云进行售卖。2019.1 为了打造更加完善的技术生态和普惠技术成果,Seata 正式宣布对外开源,未来 Seata 将以社区共建的形式帮助其技术更加可靠与完备。

3.1 组成模块

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

1、Transaction Manager(TM)

  • 事务管理器 - 第一个开始执行的业务系统

  • 全局事务的发起员,能够根据全局事务的成功与否来对全局提交或回滚进行决策

2、Transaction Coordinator(TC)

  • 事务协调器 - 第三方视角,Seata-server端

  • 维护全局事务的运行状态,负责协调并驱动全局事务的提交或者回滚

3、Resource Manager(RM)

  • 资源管理器 - 执行后面业务流程的业务系统

  • 负责对分支事务进行控制,包括分支注册、状态汇报,接收协调器发来的指令,以及对本地事务的开启、提交或回滚  

3.2 事务模式

1、全局事务的定义

若干分支事务的整体协调

SpringCloud Alibaba系列——16Seata原理及应用_第5张图片 

  • TM向TC申请开启一个全局事务,TC创建后,返回全局唯一XID,XID会在全局上下文中传播。

  • RM向TC注册分支事务,该分支事务归属于拥有相同XID的全局事务。

  • TM向TC发起全局提交或回滚

  • TC调度XID的分支事务完成提交或者回滚。

理解:一个全局事务只有一个TM,但是每一个需要调用别的服务的服务也就是全局事务的发起者都应该持有或者说是具备成为一个TM的能力。

从这里我们可以看到,全局事务的发起者通过rpc调用开启远程的分支事务,并且将全局事务id通过调用链传播下去,很明显,这一条调用链只有起点能够对整条调用链的成功与否进行感知,rpc调用链让起点天然具备了决定全局提交或是回滚的能力。

2、全局事务处理过程:

  • 执行阶段:执行分支事务,并保证执行结果满足是课回滚的和持久化的。

  • 完成阶段:根据执行阶段结果形成的决议,应用通过TM发出的全局提交或回滚的请求给TC,TC命令RM驱动分支事务进行Commit或Rollback

3、事务模式的定义

指的是运行在Seata全局事务框架下的分支事务的行为模式,准确的说应该叫分支事务模式。

不同的事务模式区别在于分支事务使用不同的方式达到全局事务两个阶段的目标,即,回答以下两个问题

  • 执行阶段:如何执行并保证执行结果满足是可回滚的和持久化的

  • 完成阶段:收到TC的命令后,如何做到分支的提交或回滚

4、事务模式能力边界

  • 数据库一定要支持本地事务——几乎是所有事务模式的基础

  • 数据表一定要定义主键——考虑到事务的隔离性,AT里面很有用。

3.3 AT模式

回答事务模式定义的两个问题:

执行阶段:

可回滚:根据SQL解析结果,记录回滚日志

持久化:回滚日志和业务SQL在同一个本地事务中提交到数据库

完成阶段:

分支提交:异步删除回滚日志记录

分支回滚:依据回滚日志进行反向补偿更新

Seata AT模式实际上是2PC协议的一种演变方式,也是通过两个阶段的提交或者回滚来保证 多节点事务的一致性。

1、第一个阶段, 应用系统会把一个业务数据的事务操作和回滚日志记录在同一个本地事务中提交,在提交之前,会向TC(seata server)注册事务分支,并申请针对本次事务操作的表的全局锁。接着提交本地事务,本地事务会提交业务数据的事务操作以及UNDO LOG,放在一个事务中提交。

  • 拦截sql并解析

  • 解析sql语义

  • 提取原数据 并将查询结果记录为前镜像

  • 执行sql

  • 保存后镜像

  • 记录undolog(类似于mysql的undolog,这是事务回滚的依据)

    • 其实undolog存在的形式是由前镜像与后镜像组成的json

SpringCloud Alibaba系列——16Seata原理及应用_第6张图片

2、第二个阶段,这一个阶段会根据参与到同一个XID下所有事务分支在第一个阶段的执行结果来决定事务的提交或者回滚,这个回滚或者提交是TC来决定的,它会告诉当前XID下的所有事务分支,提交或者回滚。

  • 如果是提交, 则把提交请求放入到一个异步任务队列,并且马上返回提交成功给到TC,这样可以避免阻塞问题。而这个异步任务,只需要删除UNDO LOG就行,因为原本的事务已经提交了。

  • 如果是回滚,则开启一个本地事务,执行以下操作

    • 通过XID和Branch ID查找到响应的UNDO LOG记录

    • 数据校验,拿到UNDO LOG中after image(修改之后的数据)和当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改,这种情况需要根据配置策略来做处理。

    • 根据UNDO LOG中的before image和业务SQL的相关信息生成并执行回滚语句提交本地事务,并把本地事务的执行结果上报给TC

SpringCloud Alibaba系列——16Seata原理及应用_第7张图片

 

3.3.1 事务隔离级别

那这种事务的隔离级别是什么样的呢?我们在学习数据库的事务特性时,必须会涉及到的就是事务的隔离级别,不同的隔离级别,会产生一些并发性的问题,比如

  • 脏读

  • 不可重复读

  • 幻读

我们知道mysql的数据库隔离级别有4种,具体哪四种我们就不在这里说明。

1、写隔离

所谓的写隔离,就是多个事务对同一个表的同一条数据做修改的时候,需要保证对于这个数据更新操作的隔离性,在传统事务模型中,我们一般是采用锁的方式来实现。

那么在分布式事务中,如果存在多个全局事务对于同一个数据进行修改,为了保证写操作的隔离,也需要通过一种方式来实现隔离性,自然也是用到锁的方法,具体来说。

  • 在第一阶段本地事务提交之前,需要确保先拿到全局锁,如果拿不到全局锁,则不能提交本地事务

  • 拿到全局锁的尝试会被限制在一定范围内,超出范围会被放弃并回滚本地事务并释放本地锁。

举一个具体的例子,假设有两个全局事务tx1和tx2,分别对a表的m字段进行数据更新操作,m的初始值是1000。

  • tx1先开始执行,按照AT模式的流程,先开启本地事务,然后更新m=1000-100=900。在本地事务更新之前,需要拿到这个记录的全局锁。

  • 如果tx1拿到了全局锁,则提交本地事务并释放本地锁。

  • 接着tx2后开始执行,同样先开启本地事务拿到本地锁,并执行m=900-100的更新操作。在本地事务提交之前,先尝试去获取这个记录的全局锁。而此时tx1全局事务还没提交之前,全局锁的持有者是tx1,所以tx2拿不到全局锁,需要等待

SpringCloud Alibaba系列——16Seata原理及应用_第8张图片

 

接着, tx1在第二阶段完成事务提交或者回滚,并释放全局锁。此时tx2就可以拿到全局锁来提交本地事务。当然这里需要注意的是,如果tx1的第二阶段是全局回滚,则tx1需要重新获取这个数据的本地锁,然后进行反向补偿更新实现事务分支的回滚。

此时,如果tx2仍然在等待这个数据的全局锁并且同时持有本地锁,那么tx1的分支事务回滚会失败,分支的回滚会一直重试直到tx2的全局锁等待超时,放弃全局锁并回滚本地事务并释放本地锁之后,tx1的分支事务才能最终回滚成功.

由于在整个过程中, 全局锁在tx1结束之前一直被tx1持有,所以并不会发生脏写问题。

SpringCloud Alibaba系列——16Seata原理及应用_第9张图片

2、读隔离

在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted) 。其实从前面的流程中就可以很显而易见的分析出来,因为本地事务提交之后,这个数据就对外可见,并不用等到tc触发全局事务的提交。

如果在特定场景下,必须要求全局的读已提交,目前Seata的方式只能通过SELECT FOR UPDATE语句来实现。

SELECT FOR UPDATE 语句的执行会申请 全局锁 ,如果 全局锁 被其他事务持有,则释放本地锁(回滚SELECT FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被 block 住的,直到 全局锁 拿到,即读取的相关数据是 已提交 的,才返回。

出于总体性能上的考虑,Seata 目前的方案并没有对所有 SELECT 语句都进行代理,仅针对 FORUPDATE 的 SELECT 语句。

SpringCloud Alibaba系列——16Seata原理及应用_第10张图片

3.4 TCC模式

回答事务模式定义的两个问题:

执行阶段:

调用业务定义的Try方法(完全由业务层面保证可回滚和持久化)

完成阶段:

分支提交:调用各事务分支定义的Confirm方法

分支回滚:调用各事务分支定义的Cancel方法

TCC模式在上面已经讲过了,就不重复讲了,实现原理是一样的。

  • 一阶段 prepare 行为

  • 二阶段 commit 或 rollback 行为

SpringCloud Alibaba系列——16Seata原理及应用_第11张图片

 

根据两阶段行为模式的不同,我们将分支事务划分为 Automatic (Branch) Transaction Mode 和TCC (Branch) Transaction Mode.

AT 模式基于 支持本地 ACID 事务 的 关系型数据库:

  • 一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录。

  • 二阶段 commit 行为:马上成功结束,自动 异步批量清理回滚日志。

  • 二阶段 rollback 行为:通过回滚日志,自动 生成补偿操作,完成数据回滚。

相应的,TCC 模式,不依赖于底层数据资源的事务支持:

  • 一阶段 prepare 行为:调用 自定义 的 prepare 逻辑。

  • 二阶段 commit 行为:调用 自定义 的 commit 逻辑。

  • 二阶段 rollback 行为:调用 自定义 的 rollback 逻辑。

所谓 TCC 模式,是指支持把 自定义 的分支事务纳入到全局事务的管理中

3.5 Saga模式

Saga模式是SEATA提供的长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。这个其实就是一个最终一致性的实现。

SpringCloud Alibaba系列——16Seata原理及应用_第12张图片

3.5.1 恢复策略

下面以下单流程为例,整个操作包括:创建订单、扣减库存、支付、增加积分。

Saga 的执行顺序有两种,如上图:

  • 事务正常执行完成:T1, T2, T3, ..., Tn,例如:扣减库存(T1),创建订单(T2),支付(T3),依次有序完成整个事务。

  • 事务回滚:T1, T2, ..., Tj, Cj,..., C2, C1,其中 0 < j < n,例如:扣减库存(T1),创建订单(T2),支付(T3,支付失败),支付回滚(C3),订单回滚(C2),恢复库存(C1)。

Saga定义了两种恢复策略

1、向前恢复

所谓向前恢复,就是指一些必须要成功的场景,如果某个子事务出现异常,不能回滚,而是不断触发重试来确保事务的成功。

SpringCloud Alibaba系列——16Seata原理及应用_第13张图片

 2、向后恢复

所谓向后恢复,就是指事务的回滚。也就是要往后逐项会撤销之前所有成功的子事务。

SpringCloud Alibaba系列——16Seata原理及应用_第14张图片

3.5.2 Saga的特点

1、适用场景

  • 业务流程长、业务流程多

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

2、优势

  • 一阶段提交本地事务,无锁,高性能

  • 事件驱动架构,参与者可异步执行,高吞吐

  • 补偿服务易于实现

3、缺点

  • 不保证隔离性

3.6 XA模式

回答事务模式定义的两个问题:

执行阶段:

可回滚:业务SQL操作放在XA分支中进行,由资源对XA协议的支持来保证可回滚

持久化:XA分支完成后,执行XA prepare,同样,由资源对XA协议的支持来保证持久化(即,之后任何意外都不会造成无法回滚的情况)

完成阶段:

分支提交:执行XA分支的commit

分支回滚:执行XA分支的rollback

 实际上前面的三种事务方式,AT、TCC、Saga都是属于补偿性事务,补偿性事务有一个特点就是无法做到真正的全局一致性,也就是无法保证从事务框架之外的全局视角的数据一致性,所以Seata引入了XA模式的支持。

补偿型事务处理机制构建在事务资源之上(要么在中间件层面,要么在应用层面),事务资源本身对分布式事务是无感知的。(说白了就是有一个上帝视角的人在控制你的事务,你自己本身是没有感觉的,因为你只看到了你自己的本地事务,但是对于全局事务压根跟你没关系,你也感知不到

XA模式,上一节课也讲过,它是X/Open组织定义的分布式事务处理标准(DTP,Distributed Transaction Processing)

XA 规范描述了全局的事务管理器与局部的资源管理器之间的接口。XA规范的目的是允许的多个资源(如数据库,应用服务器,消息队列等)在同一事务中访问,这样可以使 ACID 属性跨越应用程序而保持有效。

XA和补偿性不一样的点在于,XA协议要求事务资源(RM)本身提供对于XA协议的实现,这样可以使得事务资源(RM)感知并参与分布式事务的处理过程,所以事务资源(RM)可以保障从任意视角对数据的访问有效隔离并满足全局数据一致性。(说白了就是XA模式的时候,RM也参与到了全局事务的控制中了,不再是被动的被别人控制,而是自己也参与了控制

我们先来看一下它的整体运行机制.

3.6.1 整体运行机制

SpringCloud Alibaba系列——16Seata原理及应用_第15张图片

XA模式实际上就是一个2pc提交,所以它的事务分为两个阶段,然后我们先看一下图中所列出来的角色,TM、RM、AP,它的关系如下图所示。XA的分布式事务是由一个或者多个RM,一个事务管理器TM以及一个应用程序AP组成。

TM: 主要负责协调参与全局事务中的各个事务,它会分配一个事务唯一标识,并监控事务的执行进度,并负责事务的执行、回滚等

AP: 应用程序,定义事务的边界(事务开始和结束),并访问事务边界内的资源

RM: 资源管理器,常见的就是数据库.

SpringCloud Alibaba系列——16Seata原理及应用_第16张图片

 在Seata的XA事务模型中,原理是一样,只是把TM的角色绑定到了TC(AT模式是任何第一个发起事务请求的RM就是TM,可以理解成TM跟RM进行绑定了),从而实现XA的事务模型,具体的实现逻辑是。

SpringCloud Alibaba系列——16Seata原理及应用_第17张图片

1、执行阶段

  • 先向TC(Seata server)注册全局事务,注册之后会分配一个xid,XA start的时候需要xid参数,这个参数可以和Seata全局事务的XID和BranchId关联,以便由TC来驱动XA分支的提交和回滚

  • 向TC注册事务分支,目前Seata的BranchId是在分支注册的过程中由TC统一生成的,所以XA模式分支注册的时机需要在XA start之前

  • 通过XA Start开启XID事务,{XA START xid}

  • 执行事务SQL,预提交xid事务,也就是先执行事务操作,但是这个事务并没有提交,只是像本地事务一样写入事务日志。再调用XA END xid结束xid事务

  • XA Prepare XID 表示准备就绪,等待提交。然后向TC上报事务分支的执行结果

2、完成阶段

  • TC根据第一阶段所有事务分支的执行结果来决定事务的提交或者回滚,XA commit / XA rollback

执行阶段

  • 可回滚:业务 SQL 操作放在 XA 分支中进行,由资源对 XA 协议的支持来保证 可回滚

  • XA 分支完成后,执行 XA prepare,同样,由资源对 XA 协议的支持来保证 持久化(即,之后任何意外都不会造成无法回滚的情况)

3.6.2 XA的价值

XA和补偿性事务不同,XA协议要求事务资源本身提供对XA协议规范和协议的支持。

因为 事务资源 感知并参与分布式事务处理过程,所以 事务资源(如数据库)可以保障从任意视角对数据的访问有效隔离,满足全局数据一致性。

而这些数据的隔离性,就会依赖于数据库本身的隔离级别,如果是在读已提交之上,则不会出现脏读的情况。

除了 全局一致性 这个根本性的价值外,支持 XA 还有如下几个方面的好处:

  • 业务无侵入:和 AT 一样,XA 模式将是业务无侵入的,不给应用设计和开发带来额外负担。

  • 数据库的支持广泛:XA 协议被主流关系型数据库广泛支持,不需要额外的适配即可使用。

  • 多语言支持容易:因为不涉及 SQL 解析,XA 模式对 Seata 的 RM 的要求比较少,为不同语言开发SDK 较之 AT 模式将更 薄,更容易。

  • 传统基于 XA 应用的迁移:传统的,基于 XA 协议的应用,迁移到 Seata 平台,使用 XA 模式将更平滑。

简单的理解XA和AT模式的区别:就是AT模式在执行阶段就已经提交了,XA模式在执行阶段没有提交

3.7 Seata事务的使用

接下来,我们通过集成seata来实现一个分布式事务的使用场景。

3.7.1 Centos7安装

3.7.1.1 seata server安装

下载地址:https://github.com/seata/seata/releases

SpringCloud Alibaba系列——16Seata原理及应用_第18张图片

上传到服务器/usr/local/eclipse2019

解压: tar -zxvf seata-server-1.0.0.tar.gz

进入/usr/local/eclipse2019/seata/conf目录进行配置

  • 见4.3.7.1.1.2

在对应数据库中创建seata数据库,并创建global_table,branch_table和lock_table三张表,ddl语句如下  

CREATE TABLE IF NOT EXISTS `global_table`
(
    `xid`                       VARCHAR(128) NOT NULL,
    `transaction_id`            BIGINT,
    `status`                    TINYINT      NOT NULL,
    `application_id`            VARCHAR(32),
    `transaction_service_group` VARCHAR(32),
    `transaction_name`          VARCHAR(128),
    `timeout`                   INT,
    `begin_time`                BIGINT,
    `application_data`          VARCHAR(2000),
    `gmt_create`                DATETIME,
    `gmt_modified`              DATETIME,
    PRIMARY KEY (`xid`),
    KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
    KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
    `branch_id`         BIGINT       NOT NULL,
    `xid`               VARCHAR(128) NOT NULL,
    `transaction_id`    BIGINT,
    `resource_group_id` VARCHAR(32),
    `resource_id`       VARCHAR(256),
    `branch_type`       VARCHAR(8),
    `status`            TINYINT,
    `client_id`         VARCHAR(64),
    `application_data`  VARCHAR(2000),
    `gmt_create`        DATETIME(6),
    `gmt_modified`      DATETIME(6),
    PRIMARY KEY (`branch_id`),
    KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
    `row_key`        VARCHAR(128) NOT NULL,
    `xid`            VARCHAR(128),
    `transaction_id` BIGINT,
    `branch_id`      BIGINT       NOT NULL,
    `resource_id`    VARCHAR(256),
    `table_name`     VARCHAR(32),
    `pk`             VARCHAR(36),
    `gmt_create`     DATETIME,
    `gmt_modified`   DATETIME,
    PRIMARY KEY (`row_key`),
    KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

CREATE TABLE IF NOT EXISTS `distributed_lock`
(
    `lock_key`       CHAR(20) NOT NULL,
    `lock_value`     VARCHAR(20) NOT NULL,
    `expire`         BIGINT,
    primary key (`lock_key`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);

启动服务

控制台启动
[root@jt2 bin]# sh seata-server.sh -p 8091 -h 192.168.8.74 -m db

后台启动
[root@jt2 bin]# nohup sh seata-server.sh -p 8091 -h 192.168.8.74 -m db > /usr/local/eclipse2019/seata/log.log 2>&1 &
&:让命令在后台执行,终端退出后命令仍旧执行。
2>&1 解释:
将标准错误 2 重定向到标准输出 &1 ,标准输出 &1 再被重定向输入到 log.log 文件中。
0 – stdin (standard input,标准输入)
1 – stdout (standard output,标准输出)
2 – stderr (standard error,标准错误输出)

3.7.1.1.1 存储模式

事务日志的存储方式可以支持file、 db、 redis,默认情况下采用的是file,file存储是单机模式,全局事务会话信息会持久化到${SEATA_HOME}\bin\sessionStore\root.data中。

db和redis可以支持HA,file不行,但是性能比较好。

3.7.1.1.2 服务端配置说明

Seata-Server包含两个核心配置文件,其中registry.conf表示配置Seata服务注册的地址,它目前支持所有主流的注册中心。默认是file,表示不依赖于注册中心以及配置中心。

 registry.conf

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "nacos"

  nacos {
    application = "seata-server"
    serverAddr = "localhost:8848"
    group = "SEATA_GROUP"
    namespace = ""
    cluster = "default"
    username = "nacos"
    password = "nacos"
  }
}


config {
  # file、nacos 、apollo、zk、consul、etcd3
  type = "file"

  file {
    name = "file.conf"
  }
}

file.conf

file.conf存储的是Seata服务端的配置信息,完整的配置包含transport、Server、Metrics,分别表示协议配置,服务端配置,监控等。

service {
  #transaction service group mapping
  vgroupMapping.my_test_tx_group = "default"
  #only support when registry.type=file, please don't set multiple addresses
  default.grouplist = "127.0.0.1:8091"
  #disable seata
  disableGlobalTransaction = false
}

## transaction log store, only used in seata-server
store {
  ## store mode: file、db
  mode = "db"

  ## file store property
  file {
    ## store location dir
    dir = "sessionStore"
    # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
    maxBranchSessionSize = 16384
    # globe session size , if exceeded throws exceptions
    maxGlobalSessionSize = 512
    # file buffer size , if exceeded allocate new buffer
    fileWriteBufferCacheSize = 16384
    # when recover batch read size
    sessionReloadReadSize = 100
    # async, sync
    flushDiskMode = async
  }

  ## database store property
  db {
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
    datasource = "druid"
    ## mysql/oracle/postgresql/h2/oceanbase etc.
    dbType = "mysql"
    driverClassName = "com.mysql.jdbc.Driver"
    url = "jdbc:mysql://192.168.8.137:3306/seata?useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false&serverTimezone=Asia/Shanghai"
    user = "root"
    password = "123456"
    minConn = 5
    maxConn = 30
    globalTable = "global_table"
    branchTable = "branch_table"
    lockTable = "lock_table"
    queryLimit = 100
    maxWait = 5000
  }
}

3.7.1.2 nacos安装

下载地址:Releases · alibaba/nacos · GitHub

SpringCloud Alibaba系列——16Seata原理及应用_第19张图片

上传到服务器/usr/local/eclipse2019

解压:tar -zxvf nacos-server-1.1.0.tar.gz

进入/usr/local/eclipse2019/nacos/bin目录下执行

./startup.sh -m standalone或sh startup.sh -m standalone 命令启动单机模式nacos

关闭firewall:systemctl stop firewalld.service

禁止firewall开机启动:systemctl disable firewalld.service

 nacos设置开机自启动

1.添加nacos.service文件
vim /lib/systemd/system/nacos.service

[Unit]
Description=nacos
After=network.target

[Service]
Type=forking
ExecStart=/usr/local/eclipse2019/nacos/bin/startup.sh -m standalone
ExecReload=/usr/local/eclipse2019/nacos/bin/shutdown.sh
ExecStop=/usr/local/eclipse2019/nacos/bin/shutdown.sh
PrivateTmp=true

[Install]  
WantedBy=multi-user.target
 

2.加入nacos服务

systemctl daemon-reload
3.设置开机自启

systemctl enable nacos.service
4.启动nacos服务和关闭服务

systemctl start nacos.service

systemctl stop nacos.service

3.7.2 docker安装

3.7.2.1 seata server安装

1、运行镜像

docker run --name seata-server -p 8091:8091 -d seataio/seata-server:1.0.0

2、复制配置文件到主机

docker cp seata-server:/seata-server  /home/dockerdata/seata

3、停止服务

docker stop seata-server

4、删除服务

docker rm seata-server

5、设置开启自启和关键配置挂载到本地目录

docker run -d --restart always  --name  seata-server -p 8091:8091  -v /home/dockerdata/seata/seata-server:/seata-server -v /etc/localtime:/etc/localtime:ro -e SEATA_IP=192.168.50.128 -e SEATA_PORT=8091 --network=host seataio/seata-server:1.0.0

6、切换到配置目录

cd /home/dockerdata/seata/seata-server/resources

7、修改registry.conf文件,修为nacos启用方式,绿色字体为修改的关键地方

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "nacos"

  nacos {
    application = "seata-server"
    serverAddr = "192.168.8.137:8848"
    group = "SEATA_GROUP"
    namespace = ""
    cluster = "default"
    username = "nacos"
    password = "nacos"
  }
  eureka {
    serviceUrl = "http://localhost:8761/eureka"
    application = "default"
    weight = "1"
  }
  redis {
    serverAddr = "localhost:6379"
    db = 0
    password = ""
    cluster = "default"
    timeout = 0
  }
  zk {
    cluster = "default"
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
  }
  consul {
    cluster = "default"
    serverAddr = "127.0.0.1:8500"
  }
  etcd3 {
    cluster = "default"
    serverAddr = "http://localhost:2379"
  }
  sofa {
    serverAddr = "127.0.0.1:9603"
    application = "default"
    region = "DEFAULT_ZONE"
    datacenter = "DefaultDataCenter"
    cluster = "default"
    group = "SEATA_GROUP"
    addressWaitTime = "3000"
  }
  file {
    name = "file.conf"
  }
}

config {
  # file、nacos 、apollo、zk、consul、etcd3
  type = "file"

  nacos {
    serverAddr = "192.168.93.129:8848"
    namespace = "89f54c6f-3b21-46a4-bd1b-242ae159c12e"
    group = "DEFAULT_GROUP"
    username = ""
    password = ""
  }
  consul {
    serverAddr = "127.0.0.1:8500"
  }
  apollo {
    appId = "seata-server"
    apolloMeta = "http://192.168.1.204:8801"
    namespace = "application"
  }
  zk {
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
  }
  etcd3 {
    serverAddr = "http://localhost:2379"
  }
  file {
    name = "file.conf"
  }
}

项目中的库需要执行,sql文件路径:seata/script/client at develop · seata/seata · GitHub

seata服务 需要sql seata/script/server at develop · seata/seata · GitHub

不用的模式执行不同的脚本

具体的注意点查看/home/dockerdata/seata/resources/README-zh.md

8、修改存储方式为mysql

store {
  ## store mode: file、db
  mode = "db"

  ## file store property
  file {
    ## store location dir
    dir = "sessionStore"
    # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
    maxBranchSessionSize = 16384
    # globe session size , if exceeded throws exceptions
    maxGlobalSessionSize = 512
    # file buffer size , if exceeded allocate new buffer
    fileWriteBufferCacheSize = 16384
    # when recover batch read size
    sessionReloadReadSize = 100
    # async, sync
    flushDiskMode = async
  }

  ## database store property
  db {
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
    datasource = "druid"
    ## mysql/oracle/postgresql/h2/oceanbase etc.
    dbType = "mysql"
    driverClassName = "com.mysql.jdbc.Driver"
    url = "jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false&serverTimezone=Asia/Shanghai"
    user = "root"
    password = "root"
    minConn = 5
    maxConn = 30
    globalTable = "global_table"
    branchTable = "branch_table"
    lockTable = "lock_table"
    queryLimit = 100
    maxWait = 5000
  }
}

9、重启服务

docker restart seata-server
docker logs seata-server #查看启动日志

10、可以登录nacos查看服务列表,就会发现seata-server已经注册上来了

3.7.2.2 nacos安装

docker run -d -p 8848:8848 -e MODE=standalone --network=host -v /home/dockerdata/nacos/logs:/home/nacos/logs --restart always --name nacos nacos/nacos-server:1.1.0

开放8848端口

firewall-cmd --zone=public --add-port=80/tcp --permanent
firewall-cmd --reload #重启firewall

3.7.3 AT模式使用

代码见:seata-samples/springboot-dubbo-seata at master · seata/seata-samples · GitHub

详细使用文档以及数据库文件见该地址下的README.md和sql

下面事该代码的业务调用逻辑图

SpringCloud Alibaba系列——16Seata原理及应用_第20张图片 

 

3.8 Seata集群部署

官网地址:Seata 高可用部署

Seata的数据一般会持久化到MySQL,所以不存在数据一致性的问题,所以部署集群就比较简单

下文预告 

  1. Seata的AT模式流程推导

  2. Seata的AT模式核心源码分析

你可能感兴趣的:(SpringCloud,alibaba系列,spring,cloud,spring,boot,java,分布式,架构)