赞
踩
Seata 是一款开源的分布式事务解决方案,star 高达 18100+,社区活跃度极高,致力于在微服务架构下提供高性能和简单易用的分布式事务服务,本文将剖析 Seata-AT 的实现原理,让用户对 AT 模式有更深入的认识。
Seata 定义了全局事务的框架。全局事务定义为若干分支事务的整体协调:
Seata 的全局事务处理过程,分为两个阶段:
执行阶段 : 执行分支事务,并保证执行结果满足是可回滚的(Rollbackable)和持久化的(Durable)。
完成阶段: 根据执行阶段结果形成的决议,应用通过 TM 发出的全局提交或回滚的请求给 TC,TC 命令 RM 驱动 分支事务 进行 Commit 或 Rollback。
Seata 的所谓事务模式是指:运行在 Seata 全局事务框架下的分支事务的行为模式。准确地讲,应该叫作分支事务模式。
不同的事务模式区别在于分支事务使用不同的方式达到全局事务两个阶段的目标。即,回答以下两个问题:
执行阶段 : 如何执行并保证执行结果满足是可回滚的(Rollbackable)和持久化的(Durable)。
完成阶段: 收到 TC 的命令后,做到事务的回滚/提交
首先来看下 TCC 事务如何融合在 Seata 事务框架中:
可以发现,其实跟 Seata 的事务框架图长得非常像,而区别为 RM 负责管理就是一阶段的 try 执行和二阶段的 confirm/cancel,一样是由 TM 进行事务的 Begin(发起),RM 被 TM 调用后执行一阶段的 Try 方法,等待调用链路走完的时候,TM 向 TC 告知二阶段决议,此时 TC 对 RM 驱动二阶段执行(下发通知,RM 执行 confirm/cancel)。
如图所示,XA 模式其实就是 Seata 底层利用了 XA 接口,在一阶段二阶段时自动处理。如一阶段时,XA 的 RM 通过代理用户数据源,创建 XAConnection,进行开启 XA 事务(XA start)和 XA-prepare(此时 XA 的任何操作都会被持久化,即便宕机也能恢复),在二阶段时,TC 通知 RM 进行 XA 分支的 Commit/Rollback 操作。
首先来看一个例子。
业务 sql: update product set name = 'GTS' where name = 'TXC'
。
一阶段的执行过程对用户是无感知的,用户侧的业务 sql 保持不变,而 AT 模式下一阶段具体发生了什么?接下来,简单说下。
select id, name, since from product where name = 'TXC'
。select id, name, since from product where id = 1
。提交:仅需把事务相关信息删除即可(理论上不删除也没问题)。
回滚:取出前镜像进行回滚。
通过上述简单的例子,其实可以发现,AT 模式就是自动补偿式事务,那 AT 具体都做了哪些呢?下文将会讲述。
先来看这个图:
可能很多人刚看到上图会有疑问,其实这个就是无侵入式 AT 模式的做法示意图。首先用户还是从接口进入,到达事务发起方,此时对业务开发者来说,这个发起方入口就是一个业务接口罢了,一样地执行业务 sql,一样地 return 响应信息给客户端并没有什么改变。而背后就是用户的 sql 被 Seata 代理所托管,Seata-AT 模式能感知到用户的所有 sql,并对之进行操作,来保证一致性。
Seata-AT 是怎么做到无侵入的呢?
如图所示,应用启动时 Seata 会自动把用户的 DataSource 代理,对 JDBC 操作熟悉的用户其实对 DataSource 还是比较熟悉的,拿到了 DataSource,就等于掌握了数据源连接,也就能在背后做些“小动作”,此时对用户来讲也是无感知无入侵。
之后业务有请求进来,执行业务 sql 时,Seata 会解析用户的 sql,提取出表元数据,生成前镜像,再通过执行业务 sql,保存执行 sql 后的后镜像(至于后镜像的介绍之后会讲到),生成行锁之后在注册分支时携带到 Seata-Server,也就是 TC 端。
到此为止,在 Client 端的一阶段操作就已经完成了,无感知、无入侵。此时如果思考下,会发现这里其实有一个行锁,这个行锁是干什么用的呢?这就是要接着讲到 Seata-AT 是如何保证分布式下的事务隔离性,这里直接拿官网的示例来说。
以一个示例来说明:
两个全局事务 tx1 和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000。
tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的全局锁,本地提交释放本地锁。tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的全局锁,tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试等待全局锁 。
tx1 二阶段全局提交,释放全局锁 。tx2 拿到全局锁提交本地事务。
如果 tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。
此时如果 tx2 仍在等待该数据的全局锁,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试,直到 tx2 的全局锁等锁超时,放弃全局锁并回滚本地事务释放本地锁,tx1 的分支回滚最终成功。
因为整个过程全局锁在 tx1 结束前一直是被 tx1 持有的,所以不会发生脏写的问题。
这个时候隔离性想必大家已经比较明白了,此时一阶段的大部分操作相信大家也比较明白了,接下来我们继续往下一阶段解析。
由上图可见,在二阶段提交时,TC 仅是下发一个通知 :把之前一阶段做记录的 undoLog 删除,并把相关事务信息如:行锁删除,之后让因为在竞争锁被阻塞的事务顺利进行。
而二阶段是回滚时,则要多做一些处理。
首先在 Client 端收到 TC 告知的二阶段是回滚时,会去查到对应的事务的 undolog,取出后镜像,对比当前的数据(因为 SeataAT 是从业务应用层面进行保护分布式事务,如果此时在数据库层面直接修改了库内信息,这个时候 SeataAT 的行锁不起隔离性作用),如果出现了在全局事务以外的数据修改,此时判定为脏写,而 Seata 因为无法感知这个脏写如何发生,此时只能打印日志和触发异常通知,告知用户需要人工介入(规范修改数据入口可避免脏写)。
而如果没有发生脏写就比较简单了,拿出前镜像,众所皆知事务是需要有原子性的,要么一起发生,要么都不发生,此时前镜像记录了发生之前的数据,进行回滚后,就达到了类似本地事务那样的原子性效果。回滚后,再把事务相关信息,如 undolog,行锁进行删除。二阶段回滚算是告一段落了。
既然介绍完了 AT 模式的一阶段及二阶段的原理思想方式,那么 AT 在 Seata 的分布式事务框架下是怎么样的呢?
可以看到,AT 与其它事务模式在 Seata 事务框架中,会多出一个 undolog 的表(相对其它模式的入侵点),但是除此之外,对业务来说,几乎是零入侵性,这也就是为什么 AT 模式在 Seata 中受众广泛的原因。
首先应该明白,目前为止,不存在有任何一种分布式事务的可以满足所有场景。
无论 AT 模式、TCC 模式还是 Saga 模式,这些模式的提出,本质上都源自 XA 规范对某些场景需求的无法满足。
目前分为 3 点来做出对比:
AT 模式使用全局锁保障基本的写隔离,实际上也是锁定数据的,只不过锁在 TC 侧集中管理,解锁效率高且没有阻塞的问题。
TCC 模式无锁,利用本地事务排他锁特性,可预留资源,在全局事务决议后执行相应操作。
XA 模式在整个事务处理过程结束前,涉及数据都被锁定,读写都按隔离级别的定义约束起来。
XA 模式 prepare 后(老版本的数据库中,需要 XA END 后,再下发 prepare <三阶段由来>),分支事务进入阻塞阶段,收到 XA commit 或 XA rollback 前必须阻塞等待。
AT 可支持降级,因为锁存储在 TC 侧,如果 Seata 出现 bug 或者其它问题,可直接降级,对后续业务调用链无任何影响。
TCC 无此问题。
性能的损耗主要来自两个方面:一方面,事务相关处理和协调过程,增加单个事务的 RT;另一方面,并发事务数据的锁冲突,降低吞吐。其实主要原因就是上面的协议阻塞跟数据锁定造成。
XA 模式它的一阶段不提交,在大并发场景由于锁存储在多个资源方(数据库等),加剧了性能耗损。
AT 模式锁粒度细至行级(需要主键),且所有事务锁存储在 TC 侧,解锁高效迅速。
TCC 模式性能最优,仅需些许 RPC 开销,及 2 次本地事务的性能开销,但是需要符合资源预留场景,且是对业务侵入性较大(需要业务开发者每个接口分为 3 个,一个 try,2 个二阶段使用的 confirm 和 cancel )。
可能很多同学对 XA 和 AT 的锁 & 协议阻塞不是特别理解,那么直接来看下图:
可以试着猜一下是哪个是 XA?其实下图的是 XA,因为它带来的锁粒度更大,且锁定时间更久,导致了并发性能相对 AT 事务模型来说,差的比较多,所以至今XA模式的普及度都不很太高。
首先控制台是 Seata 用户暴露已久的一个问题,没有一个可视化界面,使得用户对 Seata 的可靠性出现了怀疑,更由于没有控制台,局限了很多在 Seata 上可人工介入分布式事务的可能性等问题,所以未来在 1.5.0 的版本会带来控制台的加入,也欢迎更多的同学加入进来一起共建!
Raft 集成的原因,可能大部分用户不是特别知晓,首先要知道目前 TC 端的事务信息都是存储在外部存储器,比如数据库、redis、mongodb(PR 阶段),这就造成了如果外部存储宕机,Seata-Server 集群的完全不可用。即便 Server 是集群部署,有 10 个甚至更多节点,都会因此而不可用,这是不可接受的。
所以引入 Raft 来让每个 Seata-Server 的事务信息达到一致,即便某个节点宕机,也不会破坏事务信息准确性,从而也让分布式事务的一致性得到了更好的保证。(关于 Seata-Server raft 的实现之后会以新篇章来分享。)
这个是 1.5.0 AT 模式比较大的性能优化,由于一阶段操作的数据多且大,因为 Seata 在背后为用户插入了 undolog 信息,由此可能也会变得大,有造成了入库缓慢的可能,所以要把 undolog 进行压缩,使 undolog 的插入不再成为 AT 事务在分支数据量大的时候成为一个大的心梗开销。
AT 说到底就是实现对资源操作的代理,并记录原先 & 变更后的状态,并用锁保证该数据的隔离性。在调用链中出现异常时,还原所有分支数据,达到分布式事务下的“原子性”。
未来呢?redis,mongodb,mq? 尽情期待。
Seata 项目的最核心的价值在于:构建一个全面解决分布式事务问题的标准化平台。
基于 Seata,上层应用架构可以根据实际场景的需求,灵活选择合适的分布式事务解决方案,非常欢迎大家参与到项目的建设中,共同打造一个标准化的分布式事务平台。
原文: https://mp.weixin.qq.com/s/WAaqOf9HBCf3CJs-S_KMUg
事务询问:协调者向所有的参与者发送事务内容,询问是否可以执行事务提交操作,并开始等待各参与者的响应
执行事务:各参与者节点执行事务操作,并将Undo和Redo信息记入事务日志中
如果参与者成功执事务操作,就反馈给协调者Yes响应,表示事物可以执行,如果没有成功执行事务,就反馈给协调者
No响应,表示事务不可以执行
二阶段提交一些的阶段一夜被称为投票阶段,即各参与者投票票表明是否可以继续执行接下去的事务提交操作
阶段二:执行事务提交
假如协调者从所有的参与者或得反馈都是Yes响应,那么就会执行事务提交。
发送提交请求:协调者向所有参与者节点发出Commit请求
事务提交:参与者接受到Commit请求后,会正式执行事务提交操作,并在完成提交之后放弃整个事务执行期间占用的
事务资源
反馈事务提交结果:参与者在完成事物提交之后,向协调者发送ACK消息
完成事务:协调者接收到所有参与者反馈的ACK消息后,完成事务
中断事务
假如任何一个参与者向协调者反馈了No响应,或者在等待超市之后,协调者尚无法接收到所有参与者的反馈响应,那么
就中断事务。
发送回滚请求:协调者向所有参与者节点发出Rollback请求
事务回滚:参与者接收到Rollback请求后,会利用其在阶段一种记录的Undo信息执行事物回滚操作,并在完成回滚之
后释放事务执行期间占用的资源。
反馈事务回滚结果:参与则在完成事务回滚之后,向协调者发送ACK消息
中断事务:协调者接收到所有参与者反馈的ACk消息后,完成事务中断、
优缺点
原理简单,实现方便
缺点是同步阻塞,单点问题,脑裂,保守
3PC提交
三阶段提,也叫三阶段提交协议,是二阶段提交(2PC)的改进版本。
与两阶段提交不同的是,三阶段提交有两个改动点。引入超时机制。同时在协调者和参与者中都引入超时机制。在第一
阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。
三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。
Seata分布式事务方案
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了
AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
Seata术语
TC:事务协调者。维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM:事务管理器。定义全局事务的范围:开始全局事务、提交或回滚全局事务
RM:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
Seata的2PC方案
一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
二阶段:提交异步化,非常快速地完成。回滚通过一阶段的回滚日志进行反向补偿。
一阶段本地事务提交前,需要确保先拿到 全局锁 。拿不到全局锁 ,不能提交本地事务。
拿全局锁的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。
在数据库本地事务隔离级别读已提交或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交
如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。
Seata执行流程分析
每个RM使用DataSourceProxy链接数据路,目的是使用ConnectionProxy,使用数据源和数据代理的目的是在第一阶段
将undo_log和业务数据放在一个本地事务提交,这样就保存了只要有业务操作就一定有undo_log
在第一阶段undo_log中存放了数据修改前后修改后的值,为事务回滚做好准别,所以第一阶段完成就已经将分支事务提
交了,也就释放了锁资源
TM开启全局事务开始,将XID全局事务ID放在事务上下文中,通过feign调用也将XID传入下游分支事务,每个分支事务
将自己的Branch ID 分支事务ID与XID关联
第二阶段全局事务提交,TC会通知各分支参与者提交分支事务,在第一阶段就已经提交了分支事务,这里各参与者只需
要删除undo_log即可,并且可以异步执行,第二阶段很快可以完成
如果某一个分支事务异常,第二阶段就全局事务回滚操作,TC会通知各分支参与者回滚分支事务,通过XID和
Branch-ID找到对应的回滚日志,通过回滚日志生成的反向SQL并执行,以完成分支事务回滚到之前
Seata的实战案列
github.com/seata/seata…
github.com/seata/seata…
TCC分布式事务
TCC是服务化的两阶段编程模型,其Try、Confirm、Cancel,3个方法均由业务编码实现
TCC要求每个分支事务实现三个操作:预处理Try,确认Confirm,撤销Cancel。
Try操作做业务检查及资源预留,
Confirm做业务确认操作
Cancel实现一个与Try相反的操作即回滚操作。
TM首先发起所有的分支事务Try操作,任何一个分支事务的Try操作执行失败,TM将会发起所有分支事务的Cancel操作,
若Try操作全部成功,TM将会发起所有分支事务的Confirm操作,其中Confirm/Cancel操作若执行失败,TM会进行重试。
TCC的三个阶段
Try阶段是做业务检查(一致性)及资源预留(隔离),此阶段仅是一个初步操作,它和后续的Confirmy一起才能构成一个完整
的业务逻辑
Confirm阶段是做确认提交,Try阶段所有分支事务执行成功后开始执行Confirm,通常情况下,采用TCC则认为
Confirm阶段是不会出错的,即:只要Try成功,Confirm一定成功,若Confirm阶段真的出错,需要引入重试机制或人工
处理
Cancel阶段是在业务执行错误需要回滚到状态下执行分支事务的取消,预留资源的释放,通常情况下,采用TCC则认为
Cancel阶段也一定是真功的,若Cance阶段真的出错,需要引入重试机制或人工处理
TM事务管理器:TM事务管理器可以实现为独立的服务,也可以让全局事务发起方充当TM的角色,TM独立出来是为了公
用组件,是为了考虑系统结构和软件的复用
TM在发起全局事务时生成全局事务记录,全局事务ID贯穿整个分布式事务调用链条,用来记录事务上下文,追踪和记录
状态,用于Confirm和cacel失败需要进行重试,因此需要实现幂等
TCC的三种异常处理情况
幂等处理
因为网络抖动等原因,分布式事务框架可能会重复调用同一个分布式事务中的一个分支事务的二阶段接口。所以分支事务
的二阶段接口Confirm/Cancel需要能够保证幂等性。如果二阶段接口不能保证幂等性,则会产生严重的问题,造成资源
的重复使用或者重复释放,进而导致业务故障。
对于幂等类型的问题,通常的手段是引入幂等字段进行防重放攻击。对于分布式事务框架中的幂等问题,同样可以祭出
这一利器。
幂等记录的插入时机是参与者的Try方法,此时的分支事务状态会被初始化为INIT。然后当二阶段的Confirm/Cancel执行
时会将其状态置为CONFIRMED/ROLLBACKED。
当TC重复调用二阶段接口时,参与者会先获取事务状态控制表的对应记录查看其事务状态。如果状态已经为
CONFIRMED/ROLLBACKED,那么表示参与者已经处理完其分内之事,不需要再次执行,可以直接返回幂等成功的结果
给TC,帮助其推进分布式事务。
空回滚
当没有调用参与方Try方法的情况下,就调用了二阶段的Cancel方法,Cancel方法需要有办法识别出此时Try有没有执行。如果Try还没执行,表示这个Cancel操作是无效的,即本次Cancel属于空回滚;如果Try已经执行,那么执行的是正常的回滚逻辑。
要应对空回滚的问题,就需要让参与者在二阶段的Cancel方法中有办法识别到一阶段的Try是否已经执行。很显然,可以
继续利用事务状态控制表来实现这个功能。
当Try方法被成功执行后,会插入一条记录,标识该分支事务处于INIT状态。所以后续当二阶段的Cancel方法被调用时,
可以通过查询控制表的对应记录进行判断。如果记录存在且状态为INIT,就表示一阶段已成功执行,可以正常执行回滚操
作,释放预留的资源;如果记录不存在则表示一阶段未执行,本次为空回滚,不释放任何资源。
资源悬挂
问题:TC回滚事务调用二阶段完成空回滚后,一阶段执行成功
解决:事务状态控制记录作为控制手段,二阶段发现无记录时插入记录,一阶段执行时检查记录是否存在
TCC和2PC比较
2PC通常都是在跨库的DB层面,而TCC则在应用层面处理,需要通过业务逻辑实现,这种分布式事务的实现方式优势在
于,可以让应用自己定义数据操作的粒度,使得降低锁冲突,提高吞吐量成为可能
而不足之处则在于对应用的侵入性非常强,业务逻辑的每个分支都需要实现Try,confirm,cancel三个操作。此外,其实
现难度也比较大,需要按照网络状态,系统故障的不同失败原因实现不同的回滚策略
Hmily框架实现TCC案列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
RocketMQ实现可靠消息最终一致性
可靠消息最终一致性就是保证消息从生产方经过消息中间件传递到消费方的一致性
RocketMQ主要解决了两个功能:本地事务与消息发送的原子性问题。事务参与方接收消息的可靠性
可靠消息最终一致性事务适合执行周期长且实时性要求不高的场景,引入消息机制后,同步的事务操作变为基于消息执行
的异步操作,避免分布式事务中的同步阻塞操作的影响,并实现了两个服务的解耦
最大努力通知
最大努力通知与可靠消息一致性有什么不同
可靠消息一致性,发起通知方需要保证将消息发出去,并且将消息发送到接收通知方,消息的可靠性由发起通知方保证
最大努力通知,发起通知方尽最大的努力将业务处理结果通知为接收通知方,但是消息可能接收不到,此时需要接收通知
方主动调用发起通知方的接口查询业务,通知可靠性关键在于接收通知方
两者的应用场景
可靠消息一致性关注的是交易过程的事务一致,以异步的方式完成交易
最大努力通知关注的是交易后的通知事务,即将交易结果可靠的通知出去
基于MQ的ack机制实现最大努力通知
利用MQ的ack机制由MQ向接收通知方发送消息通知,发起方将普通消息发送到MQ
接收通知监听MQ,接收消息,业务处理完成回应ACK
接收通知方如果没有回应ACK则MQ会重复通知,按照时间间隔的方式,逐步拉大通知间隔
此方案适用于内部微服务之间的通知,不适应与通知外部平台
方案二:增加一个通知服务区进行通知,提供外部第三方时适用
分布式事务方案对比分析
2PC 最大的一个诟病是一个阻塞协议。RM在执行分支事务后需要等待TM的决定,此时服务会阻塞锁定资源。由于其阻
塞机制和最差时间复杂度高,因此,这种设计不能适应随着事务涉及的服务数量增加而扩展的需要,很难用于并发较高以
及子事务生命周期较长的分布式服务中
如果拿TCC事务的处理流程与2PC两阶段提交做比较,2PC通常都是在跨库的DB层面,而TCC则在应用层面处理,需要通
过业务逻辑来实现。这种分布式事务的优势在于,可以让应用自定义数据操作的粒度,使得降低锁冲突,提高吞吐量成为
可能。而不足之处在于对应用的侵入性非常强,业务逻辑的每个分支都需要实现三个操作。此外,其实现难度也比较大,
需要按照网络状态,系统故障等不同失败原因实现不同的策略。
可靠消息最终一致性事务适合执行周期长且实时性要求不高的场景。引入消息机制后,同步的事务操作变为基于消息执行
的异步操作,避免了分布式事务中的同步阻塞操作的影响,并实现了两个服务的解耦,典型的场景:注册送积分,登陆送
优惠券等
最大努力通知是分布式事务中要求最低的一种,适用于一些最终一致性时间敏感度低的业务,允许发起通知方业务处理失
败,在接收通知方收到通知后积极进行失败处理,无论发起通知方如何处理结果都不会影响到接收通知方的后续处理,发
起通知方需提供查询执行情况接口,用于接收通知方校对结果,典型的应用场景:银行通知,支付结果通知等。
2PC TCC 可靠消息 最大努力通知
来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/69946034/viewspace-2671341/,如需转载,请注明出处,否则将追究法律责任。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。