Spring Cloud Alibaba 微服务框架 Seata:分布式事务
2022-05-21 12:11:28 4 举报
AI智能生成
Spring Cloud Alibaba 微服务框架 Seata 分布式事务
作者其他创作
大纲/内容
数据库事务:是指作为单个逻辑工作单元执行的多个数据库操作,要么同时成功,要么同时失败,它必须满足ACID特性
原子性(Atomicity):事务必须是原子工作单元,不可继续分割,要么全部成功,要么全部失败。
一致性(Consistency):事务完成时,所有的数据都必须保持一致。
隔离性(Isolation):由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。
持久性(Durability):事务执行完成之后,它对于系统的影响是永久性的。
ACID特性
分布式事务:是指事务的参与者、支持事务的服务器、资源服务器及事务管理器分别位于分布式系统的不同节点上
分布式事务概要
分布式事务问题也叫分布式数据一致性问题,简单来说就是如何在分布式场景中保证多个节点数据的一致性
分布式事务产生的核心原因在于存储资源的分布性,比如多个数据库,或者MySQL和Redis两种不同存储设备的数据一致性等
在实际应用中,我们应该尽可能地从设计层面去避免分布式事务的问题,因为任何一种解决方案都会增加系统的复杂度
概要
X/Open这个组织定义的一套分布式事务的标准
这个标准提出了使用两阶段提交(2PC,Two-Phase-Commit)来保证分布式事务的完整性
AP:Application,表示应用程序
RM:Resource Manager,表示资源管理器,比如数据库
TM:Transaction Manager,表示事务管理器(全局事务),一般指事务协调者,负责协调和管理事务,提供AP编程接口或管理RM
包含三种角色
图片
执行流程图
配置TM,把多个RM注册到TM,相当于TM注册RM作为数据源。
AP从TM管理的RM中获取连接,如果RM是数据库则获取JDBC连接。
AP向TM发起一个全局事务,生成全局事务ID(XID),XID会通知各个RM。
AP通过第二步获得的连接直接操作RM完成数据操作。这时,AP在每次操作时会把XID传递给RM。
AP结束全局事务,TM会通知各个RM全局事务结束。
根据各个RM的事务执行结果,执行提交或者回滚操作。
执行步骤
分布式事务处理(DTP)
流程图
TM就是一个全局事务管理器,它可以管理多个资源管理器的事务
TM最终会根据各个分支事务的执行结果进行提交或者回滚,如果注册的所有分支事务中任何一个节点事务执行失败, 为了保证数据的一致性,TM会触发各个RM的事务回滚操作
重点
TM和多个RM之间的事务控制,是基于XA协议(XA Specification)来完成的
XA协议是X/Open提出的分布式事务处理规范,也是分布式事务处理的工业标准,它定义了xa_和ax_系列的函数原型及功能描述、约束等
前Oracle、MySQL、DB2都实现了XA接口,所以它们都可以作为RM
注意点
分布式事务执行流程
X/Open分布式事务模型
事务管理器(TM)通知资源管理器(RM)准备分支事务
记录事务日志,并告知事务管理器的准备结果
PreCommit(准备阶段)
如果所有的资源管理器(RM)在准备阶段都明确返回成功,则事务管理器(TM)向所有的资源管理器(RM)发起事务提交指令完成数据的变更
反之,如果任何一个资源管理器(RM)明确返回失败,则事务管理器(TM)会向所有资源管理器(RM)发送事务回滚指令
DoCommit(提交或回滚阶段)
两阶段提交
两阶段提交执行流程图
优点:充分考虑到了分布式系统的不可靠因素,并且采用非常简单的方式(两阶段提交)就把由于系统不可靠而导致事务提交失败的概率降到最小
同步阻塞:从执行流程图来看,所有参与者(RM)都是事务阻塞型的,对于任何一次指令都必须要有明确的响应才能继续进行下一步,否则会处于阻塞状态,占用的资源一直被锁定。
过于保守:任何一个节点失败都会导致数据回滚。
事务协调者的单点故障:如果协调者在第二阶段出现了故障,那么其他的参与者(RM)会一直处于锁定状态。
脑裂导致数据不一致问题:在第二阶段中,事务协调者向所有参与者(RM)发送commit请求后,发生局部网络异常导致只有一部分参与者(RM)接收到了commit请求, 这部分参与者(RM)收到请求后会执行commit操作,但是未收到commit请求的节点由于事务无法提交,导致数据出现不一致问题。
缺点
zookeeper集群中的数据一致性:它不需要所有参与者在第一阶段返回成功才能提交事务,而是利用少数服从多数的投票机制来完成数据的提交或者回滚
优化版的两阶段提交协议
两阶段提交协议
概要:三阶段提交协议是两阶段提交协议的改进版本,它利用超时机制解决了同步阻塞的问题
事务协调者向参与者发送事务执行请求,询问是否可以完成指令
参与者只需要回答是或者不是即可,不需要做真正的事务操作,这个阶段会有超时中止机制
CanCommit(询问阶段)
事务协调者会根据参与者的反馈结果决定是否继续执行
如果在询问阶段所有参与者都返回可以执行操作,则事务协调者会向所有参与者发送PreCommit请求, 参与者收到请求后写redo和undo日志,执行事务操作但是不提交事务,然后返回ACK响应等待事务协调者的下一步通知
如果在询问阶段任意参与者返回不能执行操作的结果,那么事务协调者会向所有参与者发送事务中断请求
这个阶段也会存在两种结果,仍然根据上一步骤的执行结果来决定DoCommit的执行方式
如果每个参与者在PreCommit阶段都返回成功,那么事务协调者会向所有参与者发起事务提交指令
反之,如果参与者中的任一参与者返回失败,那么事务协调者就会发起中止指令来回滚事务
三阶段提交
三阶段提交执行流程图
增加了CanCommit阶段,用于询问所有参与者是否可以执行事务操作并且响应,它的好处是,可以尽早发现无法执行操作而中止后续的行为
在PreCommit阶段之后,事务协调者和参与者都引入了超时机制,一旦超时,事务协调者和参与者会继续提交事务,并且认为处于成功状态, 因为在这种情况下事务默认为成功的可能性比较大
三阶段提交协议最大的好处就是基于超时机制来避免资源的永久锁定
三阶段提交协议和两阶段提交协议不同点
在三阶段提交协议下一旦超时,仍然可能出现数据不一致的情况,当然概率是比较小
两阶段提交和三阶段提交是XA协议解决分布式数据一致性问题的基本原理,但是这两种方案为了保证数据的强一致性,降低了可用性
三阶段提交协议
概念:在分布式系统中不可能同时满足一致性(C:Consistency)、可用性(A:Availability)、分区容错性(P:Partition Tolerance)这三个基本需求,最多同时满足两个
C(Consistency:一致性):数据在多个副本中要保持强一致,比如前面说的分布式数据一致性问题。
A(Availability:可用性):系统对外提供的服务必须一直处于可用状态,在任何故障下,客户端都能在合理的时间内获得服务端的非错误响应。
P(Partition Tolerance:分区容错性):在分布式系统中遇到任何网络分区故障,系统仍然能够正常对外提供服务。
CAP
网络分区:不同节点分布在不同的子网络中时,在内部子网络正常的情况下,由于某些原因导致这些子节点之间出现网络不通的情况,导致整个系统环境被切分成若干独立的区域
CAP定理证明:在分布式系统中,要么满足CP,要么满足AP,不可能实现CAP或者CA
网络通信并不是绝对可靠的,比如网络延时、网络异常等都会导致系统故障
在分布式系统中,即便出现网络故障也需要保证系统仍然能够正常对外提供服务
所以在分布式系统中Partition Tolerance是必然存在的,也就是需要满足分区容错性
不能实现CA或者CAP原因
相当于网络百分之百可靠,否则当出现网络分区的情况时,为了保证数据的一致性,必须拒绝客户端的请求
但是如果拒绝了请求,就无法满足A,所以在分布式系统中不可能选择CA,因此只能有AP或者CP两种选择
假如CA或者CAP情况存在
放弃了强一致性,实现最终的一致和高可用
这是很多互联网公司解决分布式数据一致性问题的主要选择
AP模式
放弃了高可用性,实现强一致性
前面提到的两阶段提交和三阶段提交都采用这种方案
可能导致的问题是用户完成一个操作会等待较长的时间
在数据库性能和系统处理能力上会存在一定的瓶颈
CP模式
CAP定理
由于CAP中一致性和可用性不可兼得而衍生出来的一种新的思想
核心思想是通过牺牲数据的强一致性来获得高可用性
概念
Basically Available(基本可用):分布式系统在出现故障时,允许损失一部分功能的可用性,保证核心功能的可用。
Soft State(软状态):允许系统中的数据存在中间状态,这个状态不影响系统的可用性,也就是允许系统中不同节点的数据副本之间的同步存在延时。
Eventually Consistent(最终一致性):中间状态的数据在经过一段时间之后,会达到一个最终的数据一致性。
三个特性
BASE理论并没有要求数据的强一致,而是允许数据在一段时间内是不一致的,但是数据最终会在某个时间点实现一致
在互联网产品中,大部分都会采用BASE理论来实现数据的一致,因为产品的可用性对于用户来说更加重要
BASE理论
柔性事务是遵循BASE理论来实现的事务模型
两个特性:基本可用、柔性状态
柔性事务
分布式事务问题的理论模型
Try:这个阶段主要是对数据的校验或者资源的预留。
Confirm:确认真正执行的任务,只操作Try阶段预留的资源。
Cancel:取消执行,释放Try阶段预留的资源。
TCC(Try-Confirm-Cancel)
第一阶段通过Try进行准备工作
第二阶段Confirm/Cancel表示Try阶段操作的确认和回滚
TCC是一种两阶段提交的思想
TCC执行流程图
在分布式事务场景中,每个服务实现TCC之后,就作为其中的一个资源,参与到整个分布式事务中
然后主业务服务在第一阶段中分别调用所有TCC服务的Try方法
最后根据第一个阶段的执行情况来决定对第二阶段的Confirm或者Cancel
TCC执行步骤
TCC事务框架会记录一些分布式事务的操作日志,保存分布式事务运行的各个阶段和状态
TCC事务协调器会根据操作日志来进行重试,以达到数据的最终一致性
TCC服务支持接口调用失败发起重试,所以TCC暴露的接口都需要满足幂等性
部分服务没有收到TCC事务协调器的Cancel或者Confirm请求的异常处理
TCC补偿型方案
是互联网公司比较常用的分布式数据一致性解决方案
主要利用消息中间件(Kafka、RocketMQ或RabbitMQ)的可靠性机制来实现数据一致性的投递
支付流程图
先发送消息(发送卡券消息到MQ),再执行数据库事务,在这种情况下可能会出现消息发送成功但是本地事务更新失败的情况,仍然会导致数据不一致的问题
先执行数据库事务操作,再发送消息,在这种情况下可能会出现MQ响应超时导致异常,从而将本地事务回滚,但消息可能已经发送成功了,也会存在数据不一致的问题
存在的问题:支付服务的本地事务与发送消息这个操作的原子性问题
通过RocketMQ提供的事务消息模型
RocketMQ事务消息方案流程图
生产者发送一个事务消息(Half消息)到消息队列上,消息队列只记录这条消息的数据,此时消费者无法消费这条消息。
生产者执行具体的业务逻辑,完成本地事务的操作。
接着生产者根据本地事务的执行结果发送一条确认消息给消息队列服务器,如果本地事务执行成功,则发送一个Commit消息,表示在第一步中发送的消息可以被消费, 否则,消息队列服务器会把第一步存储的消息删除。
如果生产者在执行本地事务的过程中因为某些情况一直未给消息队列服务器发送确认,那么消息队列服务器会定时主动回查生产者获取本地事务的执行结果, 然后根据回查结果来决定这条消息是否需要投递给消费者。
消息队列服务器上存储的消息被生产者确认之后,消费者就可以消费这条消息,消息消费完成之后发送一个确认标识给消息队列服务器,表示该消息投递成功
RocketMQ事务消息方案步骤
RocketMQ的事务消息模型中最核心的机制应该是事务回查
如果消费者没有签收该消息,那么消息队列服务器会重复投递,从而实现生产者的本地数据和消费者的本地数据在消息队列的机制下达到最终一致
解决方案
电商平台的支付场景示例
基于可靠性消息的最终一致性方案
最大努力通知型和基于可靠性消息的最终一致性方案的实现是类似的,它是一种比较简单的柔性事务解决方案
比较适用于对数据一致性要求不高的场景
最典型的使用场景是支付宝支付结果通知
最大努力通知型执行流程图
支付完成后触发一个回调通知给商户,商户收到该通知后,根据结果修改本地支付订单的状态,并且返回一个处理状态给支付宝
由于网络的不确定性,支付结果通知可能会失败或者丢失,导致商户端的支付订单的状态是未知的
支付结果回调请求会以衰减重试机制(逐步拉大通知的间隔)继续触发,比如1min、5min……直到达到最大通知次数
如果达到指定次数后商户还没有返回确认状态
支付宝提供一个交易结果查询接口,可以根据这个支付订单号去支付宝查询支付状态, 然后根据返回的结果来更新商户的支付订单状态,这个过程可以通过定时器来触发,也可以通过人工对账来触发
流程中的重点说明
所谓的最大努力通知,就是在商户端如果没有返回一个消息确认时,支付宝会不断地进行重试,直到收到一个消息确认或者达到最大重试次数。
最大努力通知型方案
分布式事务问题的解决方案
Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。
它提供了AT、TCC、Saga和XA事务模式,为开发者提供了一站式的分布式事务解决方案。
AT模式是Seata最主推的分布式事务解决方案,它是基于XA演进而来的一种分布式事务模式
TM:Transaction Manager,事务管理器
RM:Resource Manager,资源管理器
TC:Transaction Coordinator,事务协调器
同样分为三大模块
TM和RM作为Seata的客户端与业务系统集成
TC作为Seata的服务器独立部署
TM负责向TC注册一个全局事务,并生成一个全局唯一的XID
每个数据库资源被当作一个RM,在业务层面通过JDBC标准的接口访问RM时,Seata会对所有请求进行拦截
每个本地事务进行提交时,RM都会向TC注册一个分支事务
是一个两阶提交事务模型,不过和XA相比,做了很多优化
简介
AT事务模式流程图
TM向TC注册全局事务,并生成全局唯一的XID。
RM向TC注册分支事务,并将其纳入该XID对应的全局事务范围。
RM向TC汇报资源的准备状态。
TC汇总所有事务参与者的执行状态,决定分布式事务是全部回滚还是提交。
TC通知所有RM提交/回滚事务。
AT事务模式流程步骤
AT模式
Saga模式又称为长事务解决方案
主要描述的是在没有两阶段提交的情况下如何解决分布式事务问题
业务流程长、业务流程多
参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口
适用场景
把一个业务流程中的长事务拆分为多个本地短事务
业务流程中的每个参与者都真实的提交给该本地短事务
当其中一个参与者事务执行失败,则通过补偿机制补偿前面已经成功的参与者
核心思想
Saga事务模型
Saga由一系列sub-transaction T i 组成,每个T i 都有对应的补偿动作C i ,补偿动作用于撤销T i 造成的数据变更结果
它和TCC相比,少了Try这个预留动作,每一个T i 操作都真实地影响到数据库
Saga事务模型说明
T1,T2,T3,...,T i :这种方式表示所有事务都正常执行
T1,T2,...,T j ,C j ,...,C2,C1(其中0< j < i ):这种方式表示执行到T j 事务时出现异常,通过补充操作撤销之前所有成功的sub-transaction
Saga的两种执行方式
向后恢复,也就是上面提到的第二种工作模式,如果任一子事务执行失败,则把之前执行的结果逐一撤销
向前恢复,也就是不进行补偿,而是对失败的事务进行重试,这种方式比较适合于事务必须要执行成功的场景
注意点:不管是向后恢复还是向前恢复,都可能出现失败的情况,在最坏的情况下只能人工干预处理
Saga的两种补偿恢复方式
一阶段直接提交本地事务,没有锁等待,性能较高
在事件驱动的模式下,短事务可以异步执行,高吞吐
补偿机制的实现比较简单
Saga 实现是有侵入性的
优势
不提供原子性和隔离性支持,隔离性的影响是比较大的
例如:用户购买一个商品后系统赠送一张优惠券,如果用户已经把优惠券使用了,那么事务如果出现异常要回滚时就会出现问题
缺乏隔离性的应对方案
劣势
Saga的优劣势(和XA或者TCC相比)
优点:业务耦合度低
缺点:业务分散到各个点,后期很难把握整体执行流程
事件/编排式:把Saga的决策和执行顺序逻辑分布在Saga的每一个参与者中,它们通过交换事件的方式来进行沟通。(常用的解决方案)
优点:通过Saga控制类就能了解整体执行流程
缺点:业务控制都集中到了Saga控制类,耦合度高
命令/协同式:把Saga的决策和执行顺序逻辑集中在一个Saga控制类中,它以命令/回复的方式与每项服务进行通信,告诉它们应该执行哪些操作。
Saga两种协调模式
下单时序图
根据Saga模式的定义,先将长事务拆分成多个本地短事务
每个服务的本地事务按照执行顺序逐一提交
一旦其中一个服务的事务出现异常,则采用补偿的方式逐一撤回
实现逻辑概要
第一个服务执行完一个本地事务之后,发送一个事件
这个事件会被一个或者多个服务监听,监听到事件的服务再执行本地事务并发布新的事件
此后一直延续这种事件触发模式,直到该业务流程中最后一个服务的本地事务执行结束,才意味着整个分布式长事务也执行结束
通用流程描述
实现流程图
订单服务创建新的订单,把订单状态设置为待支付,并发布一个ORDER_CREATE_EVENT事件。
库存服务监听到ORDER_CREATE_EVENT事件后,执行本地的库存冻结方法,如果执行成功,则发布一个ORDER_PREPARED_EVENT事件。
支付服务监听ORDER_PREPARED_EVENT事件后,执行账户扣款方法,并发布PAY_ORDER_EVENT事件。
最后,积分服务监听PAY_ORDER_EVENT事件,增加账户积分,并更新订单状态为成功。
上述任一步骤执行失败,都会发送一个失败的事件,每个服务需要监听失败的情况根据实际需求进行逐一回滚。
流程步骤
事件/编排式 方案
订单服务首先创建一个订单,然后创建一个订单Saga协调器,启动订单事务。
Saga协调器向库存服务发送冻结库存命令,库存服务通过Order Saga Reply Queue回复执行结果。
接着,Saga协调器继续向支付服务发起账户扣款命令,支付服务通过Order Saga Reply Queue回复执行结果。
最后,Saga协调器向积分服务发起增加积分命令,积分服务回复执行结果。
订单Saga协调器必须提前知道“创建订单事务”的所有流程(Seata是通过基于JSON的状态机引擎来实现的)
并且在整个流程中任何一个环节执行失败,它都需要向每个参与者发送命令撤销之前的事务操作。
命令/协同式 方案
电商平台下单的长事务场景
Saga的实现方式
空补偿:原服务未执行,补偿服务执行了
原服务超时(丢包)
Saga 事务触发回滚
未收到原服务请求,先收到补偿请求
出现原因
允许空补偿
悬挂:补偿服务比原服务先执行
原服务超时(拥堵)
Saga 事务回滚,触发回滚
拥堵的原服务到达
所以要检查当前业务主键是否已经在空补偿记录下来的业务主键中存在,如果存在则要拒绝服务的执行
防悬挂控制
幂等控制
服务设计
配置客户端参数client.rm.report.success.enable=false,可以在当分支事务执行成功时不上报分支状态到server,从而提升性能。
原因:当上一个分支事务的状态还没有上报的时候,下一个分支事务已注册,可以认为上一个实际已成功
性能优化
Saga的最佳实践
目前seata提供的Saga模式是基于状态机引擎来实现的
通过状态图来定义服务调用的流程并生成 json状态语言定义文件
状态图中一个节点可以是调用一个服务,节点可以配置它的补偿节点
状态图json由状态机引擎驱动执行,当出现异常时状态引擎反向执行已成功节点对应的补偿节点将事务回滚
注意: 异常发生时是否进行补偿也可由用户自定义决定
可以实现服务编排需求,支持单项选择、并发、子流程、参数转换、参数映射、服务执行状态判断、异常捕获等功能
实现机制
示例状态图
Seata Saga 提供了一个可视化的状态机设计器方便用户使用
代码和运行指南请参考:https://github.com/seata/seata/tree/develop/saga/seata-saga-statemachine-designer
状态机设计器
Name:表示状态机的名称,必须唯一
Comment:状态机的描述
Version:状态机定义版本
StartState:启动时运行的第一个"状态"
States:状态列表,是一个map结构,key是"状态"的名称,在状态机内必须唯一
"状态机" 属性简介
ServiceTask:执行调用服务任务
Choice:单条件选择路由
CompensationTrigger:触发补偿流程
Succeed:状态机正常结束
Fail:状态机异常结束
SubStateMachine:调用子状态机
CompensateSubMachine:用于补偿一个子状态机
Type:"状态" 的类型
ServiceName:服务名称,通常是服务的beanId
ServiceMethod:服务方法名称
CompensateState:该"状态"的补偿"状态"
Catch:捕获到异常后的路由
Next:服务执行完成后下一个执行的"状态"
ErrorCode:Fail类型"状态"的错误码
Message:Fail类型"状态"的错误信息
"状态" 属性简介
状态机属性
Saga的Seata实现
Saga模式
分布式事务框架Seata
直接部署
Docker
Kubernetes
Helm
部署方式
在Seata官网下载最新安装包。
进入${SEATA_HOME}\\bin目录,根据系统类型执行相应的启动脚本,在Linux下的执行命令:sh seata-server.sh
seata-server.sh支持设置启动参数
直接安装步骤
File存储模式为单机模式
全局事务会话信息持久化在本地文件${SEATA_HOME}\\bin\\sessionStore\oot.data中,性能较高
启动命令:sh seata-server.sh -p 8091 -h 127.8.0.1 -m file
File存储模式
DB存储模式为高可用模式,全局事务会话信息通过db共享,性能相对差一些
全局事务:global_table
分支事务:branch_table
全局锁构:lock_table
创建表结构
设置事务日志存储方式,进入${SEATA_HOME}\\conf\\file.conf,修改store.mode="db"
修改数据库连接
-h :注册到注册中心的IP地址,Seata-Server可以把自己注册到注册中心,支持Nacos、Eureka、Redis、ZooKeeper、Consul、Etcd3、Sofa。
-p :Server RPC监听端口。
-m:全局事务会话信息存储模式,包括file、db,优先读取启动参数。
-n :Server node,有多个Server时,需区分各自节点,用于生成不同区间的transactionId,以免冲突。
启动seata-server:sh seata-server.sh -h 127.8.0.1 -p 8091 -m db -n 1
操作步骤
DB存储模式
在${SEATA_HOME}\\conf目录下有两个配置文件,分别是registry.conf和file.conf
registry.conf中包含两项配置:registry、config
通过type指定注册中心的类型(支持目前市面上所有主流的注册中心组件)
然后根据指定的类型配置对应的服务地址信息
当type=nacos时,则匹配到Nacos的配置项
file类型可以不依赖第三方注册中心快速集成Seata
file类型不具备注册中心的动态发现和动态配置功能
type默认为file,它表示不依赖于配置中心
registry表示配置Seata服务端的注册中心地址
本地文件读取配置(默认情况下type=file,它会加载file.conf文件中的配置信息)
远程配置中心读取配置(type=nacos)
config表示配置Seata服务端的配置文件地址
registry.conf 配置说明
transport:协议配置
server:服务端配置
metrics:监控配置
file.conf存储的是Seata服务端的配置信息
完整配置示例
file.conf 配置说明
client:存放客户端的SQL脚本,参数配置。
server:服务端数据库脚本及各个容器配置。
config-center:各个配置中心参数导入脚本,config.txt(包含server和client)为通用参数文件。
下载seata源码,目录结构
在config-center\acos目录下,执行命令:sh nacos-config.sh -h 192.168.216.128 -p 8848 -g SEATE_GROUP
nacos上的seata本配置信息
将配置上传到nacos
进入${SEATA_HOME}\\conf目录,修改registry.conf文件中的config段
seata服务端修改配置加载位置
从nacos配置中心加载配置
Seata服务端配置中心说明
Seata的安装
项目整体架构图
基于Spring Boot+Nacos+Dubbo构建以下项目
sample-order-service:订单服务
sample-repo-service:库存服务
sample-account-service:账户服务
sample-seata-common:公共服务组件
sample-rest-web:提供统一业务的REST接口服务
项目准备
创建三个数据库:seata_order、seata_repo、seata_account
分别创建业务表
数据库准备
添加seata jar包依赖,分别在4个项目中添加seata的starter组件依赖
seata.support.spring.datasource-autoproxy:true属性表示数据源自动代理开关, 在sample-order-service、sample-account-service、sample-repo-service中设置为true, 在sample-rest-web中设置为false,因为该项目并没有访问数据源,不需要代理
如果注册中心为file,seata.service.grouplist需要填写Seata服务端连接地址。在默认情况下,注册中心配置为file
如果需要从注册中心上进行服务发现,可以增加配置
tx-service-group表示指定服务所属的事务分组,如果没有指定,默认使用spring.application.name加上字符串-seata-service-group。 需要注意这两项配置必须要配置一项,否则会报错
添加seata配置项目,分别在4个项目中的application.yml文件中添加Seata的配置项
分别在3个数据库seata-account、seata-repo、seata-order中添加一张回滚日志表
用于记录每个数据库表操作的回滚日志
当某个服务的事务出现异常时会根据该日志进行回滚
添加回滚日志表
增加@GlobalTransactional全局事务注解
模拟一个异常处理,当商品编号等于某个指定的值时抛出异常,触发整个事务的回滚
sample-rest-web增加全局事务控制
正常情况下,每个服务都会向TC(Seata-Server)注册一个事务分支,出现如图日志说明事务分支注册成功
在引入Seata之后,在异常出现后会触发各个事务分支的数据回滚,出现如图日志说明完成事务回滚操作
启动服务进行测试
整合Seata实现分布式事务
AT模式Dubbo集成Seata
使用Dubbo集成Seata的项目
Spring Cloud项目准备
添加依赖包
spring-cloud-alibaba-seata不支持yml形式,所以只能使用file.conf和registry.conf文件来描述客户端的配置信息(可以从nacos加载)
直接将${SEATA_HOME}\\conf目录下的这两个文件复制到项目的resource目录中。
如果希望从配置中心加载这些配置项,在registry.conf中指定配置中心地址即可(可以在application.yml中指定从nacos加载)
file.conf完整配置项
配置中,vgroup_mapping.${txServiceGroup}="default"表示事务群组,其中${txServiceGroup}表示事务服务分组,它的值要设置为spring.cloud.alibaba.seata.tx-service-group或者spring.application.name+"seata.tx-service-group"
添加配置文件
配置数据源代理DataSourceProxy
初始化GlobalTransactionScanner,装载到Spring IoC容器
在事务服务中添加一个配置类SeataAutoConfig(手动配置)
seata-spring-boot-starter默认支持自动实现数据源的代理
配置Seata代理数据源
集成Spring Cloud Alibaba Seata
在seata-client端的file.conf配置中有一个属性vgroup_mapping,它表示事务分组映射,是Seata的资源逻辑,类似于服务实例
它的主要作用是根据分组来获取seata-server的服务实例
事务分组映射:vgroup_mapping
默认情况下,获取spring.application.name的值+"-seata-service-group"
在spring-cloud-alibaba-seata中,可以使用spring.cloud.alibaba.seata.tx-service-group赋值
在seata-spring-boot-starter中,可以使用seata.tx-service-group赋值
txServiceGroup参数赋值方式
首先,在应用程序中需要配置事务分组,也就是使用GlobalTransactionScanner构造方法中的txServiceGroup参数
然后,seata客户端会根据应用程序的txServiceGroup去指定位置(file.conf或者远程配置中心)查找service.vgroup_mapping.${txServiceGroup}对应的配置值, 该值代表TC集群(seata-server)的名称
拿到TC集群名称后seata客户端通过一定的前后缀+集群名称去构造服务名,各配置中心的服务名实现不同
最后,程序会根据集群名称去配置中心或者file.conf中获得对应的服务列表,也就是clusterName.grouplist对应的TC集群真实的服务列表
事务分组工作机制
流程图示例
获取事务分组spring-cloud-seata-repo配置的值Agroup
拿到事务分组的值Agroup,拼接成service.vgroup_mapping.Agroup,去配置中心查找集群名,得到default
拼接service.default.grouplist,查找集群名对应的Seata Server服务地址:192.168.1.1:8091
事务分组示例流程
在客户端获取服务器地址并没有直接采用服务名称,而是增加了一层事务分组映射到集群的配置
优点:事务分组可以作为资源的逻辑隔离单位,当某个集群出现故障时,可以把故障缩减到服务级别,实现快速故障转移, 只需要切换对应的分组即可,前提也是你有足够server集群
事务分组设计优点
关于事务分组的说明
Spring Cloud Alibaba Seata
AT模式是基于XA事务模型演进而来的
第一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源
第二阶段:提交异步化,非常快速地完成。回滚通过第一阶段的回滚日志进行反向补偿
整体机制是一个改进版的两阶段提交协议
Seata会基于数据源代理对原执行的SQL进行解析,代理的配置代码在备注中(Seata在0.9.0版本之后支持自动代理)
然后将业务数据在更新前后保存到undo_log日志表中,利用本地事务的ACID特性,把业务数据的更新和回滚日志写入同一个本地事务中进行提交
假设AT分支事务的业务逻辑是:update tbl_repo set count=count-1 where product_code = 'GP20200202001'
通过DataSourceProxy对业务SQL进行解析,得到SQL的类型(UPDATE)、表(tbl_repo)、条件(where product_code="GP20200202001")等相关的信息
得到该产品代码对应的库存数量为1000
执行业务SQL:更新这条记录的count=count-1(此时还未提交事务)
得到修改之后的镜像数据,此时count=999
基本数据
字段rollback_info表示回滚的数据包含beforeImage和afterImage
插入回滚日志:把前、后镜像数据及业务SQL相关的信息组成一条回滚日志记录,插入undo_log表中
提交前,向TC注册分支事务:申请tbl_repo表中主键值等于1的记录的全局锁
本地事务提交:业务数据的更新和前面步骤中生成的undo_log一并提交
将本地事务提交的结果上报给TC
第一阶段的执行逻辑为
示例流程分析
AT模式分支的本地事务可以在第一阶段提交完成后马上释放本地事务锁定的资源(最大的不同点)
XA事务的两阶段提交,一般锁定资源后持续到第二阶段的提交或者回滚后才释放资源
AT模式降低了锁的范围,从而提升了分布式事务的处理效率
AT模式记录了回滚日志,即便第二阶段发生异常,只需要根据undo_log中记录的数据进行回滚即可
AT模式和XA的不同点
AT模式第一阶段的原理分析
TC接收到所有事务分支的事务状态汇报之后,决定对全局事务进行提交或者回滚
全局事务提交流程
TC并不需要同步知道分支事务的处理结果,所以分支事务才会采用异步的方式来执行
分支事务只需要清除UNDO_LOG日志即可,而即便日志清除失败,也不会对整个分布式事务产生任何影响
分支事务收到TC的提交请求后把请求放入一个异步任务队列中,并马上返回提交成功的结果给TC
从异步队列中执行分支,提交请求,批量删除相应UNDO_LOG日志
全局事务提交流程步骤
AT模式第一阶段各个分支事务的本地事务已经提交了
AT模式第二阶段并不需要TC来触发所有分支事务的提交
AT模式第二阶段只需要清理UNDO_LOG日志即可
事务提交
全局事务回滚流程
所有分支事务接收到TC的事务回滚请求后,分支事务参与者开启一个本地事务
通过XID和branch ID查找到相应的UNDO_LOG记录
数据校验:拿UNDO_LOG中的afterImage镜像数据与当前业务表中的数据进行比较,如果不同,说明数据被当前全局事务之外的动作做了修改,那么事务将不会回滚(隔离性问题?)
如果afterImage中的数据和当前业务表中对应的数据相同,则根据UNDO_LOG中的beforeImage镜像数据和业务SQL的相关信息生成回滚语句并执行
提交本地事务,并把本地事务的执行结果(即分支事务回滚的结果)上报给TC
全局事务回滚流程步骤
事务回滚
AT模式第二阶段的原理分析
隔离性是指多个用户并发访问数据库时,数据库为每个用户开启的事务不能被其他事务的操作所干扰,多个并发事务之间要相互隔离
在AT模式中,当多个全局事务操作同一张表时,它的事务隔离性保证是基于全局锁来实现的
写隔离是为了在多个全局事务针对同一张表的同一个字段进行更新操作时,避免全局事务在没有被提交之前被其他全局事务修改
写隔离的主要实现是:在第一阶段本地事务提交之前,确保拿到全局锁。如果拿不到全局锁,则不能提交本地事务
获取全局锁的尝试会有一个时间范围限制,如果超出时间范围将会放弃全局锁的获取,并且回滚事务,释放本地锁
场景:假设两个全局事务 tx1 和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000
全局事务提交时序图
tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900
tx1 本地事务提交前,先拿到该记录的全局锁,本地提交释放本地锁
tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800
tx2 本地事务提交前,尝试拿该记录的全局锁,tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试等待全局锁
tx1 二阶段全局提交,释放 全局锁 。tx2 拿到 全局锁 提交本地事务
全局事务回滚时序图
如果 tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚
此时,如果 tx2 仍在等待该数据的 全局锁,同时持有本地锁,则 tx1 的分支回滚会失败。
tx1的分支的回滚会一直重试,直到 tx2 的 全局锁 等锁超时,放弃 全局锁 并回滚本地事务释放本地锁,tx1 的分支回滚最终成功
整个过程 全局锁 在 tx1 结束前一直是被 tx1 持有的,所以不会发生脏写的问题
示例
写隔离
Read Uncommitted:读未提交
Read Committed:读已提交
Repeatable Read:可重复读
Serializable:可串行化
数据库的4种隔离级别
在数据库本地事务隔离级别为读已提交(Read Committed)或者以上时,AT事务模式的默认全局事务隔离级别是读未提交(Read Uncommitted)
在该隔离级别,所有事务都可以看到其他未提交事务的执行结果,产生脏读
在最终一致性事务模型中是允许存在的,并且在大部分分布式事务场景中都可以接受脏读
AT事务模式隔离级别
目前Seata是通过 SelectForUpdateExecutor 执行器对 SELECT FOR UPDATE 语句进行代理的
SELECT FOR UPDATE 语句在执行时会申请全局锁
实现方式
时序图
SELECT FOR UPDATE 语句的执行会申请 全局锁
如果 全局锁 被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试
这个过程中,查询是被 block 住的,直到 全局锁 拿到,即读取的相关数据是 已提交 的,才返回(查询性能受影响)
出于总体性能上的考虑,Seata 目前的方案并没有对所有 SELECT 语句都进行代理,仅针对 FOR UPDATE 的 SELECT 语句
AT事务的全局的读已提交级别
读隔离
关于事务的隔离性保证
Seata AT模式的实现原理
如果导图对您有用,请在右上角给点个赞吧
Spring Cloud Alibaba 微服务框架 Seata:分布式事务
0 条评论
回复 删除
下一页