<SOFA:Channel/>,有趣实用的分布式架构频道。
本文根据 SOFAChannel#12 直播分享整理,主题:蚂蚁金服分布式事务实践解析。
回顾视频以及 PPT 查看地址见文末。欢迎加入直播互动钉钉群 : 30315793,不错过每场直播。
大家好,我是今天分享的讲师仁空,目前是蚂蚁金服分布式事务产品的研发。今天跟大家分享的是蚂蚁金服分布式事务实践解析,也就是分布式事务 Seata 在蚂蚁金服内部的实践。
今天我们将从以下 4 个主题进行详细介绍:
- 为什么会有分布式事务产品的需求;
- 理论界针对这个需求提出的一些理论和解决方案;
- 蚂蚁金服在工程上是如何解决这个问题的;
- 针对蚂蚁金服业务场景的性能优化;
分布式事务产生背景
首先是分布式事务产生的背景。
支付宝支付产品在 2003 年上线的时候,那时候的软件形态是单体应用,在一个应用内完成所有的业务逻辑操作。随着软件的工业化,场景越来越复杂,软件也越做越大,所有的业务在一个应用内去完成变的不可能,出现了软件模块化、服务化。
在从单体应用升级到分布式架构过程中,很自然得需要进行业务服务拆分,将原来糅在一个系统中的业务进行梳理,拆分出能独立成体系的各个子系统,例如交易系统、支付系统、账务系统等,这个过程就是服务化。业务服务拆分之后,原来一个服务就能完成的业务操作现在需要跨多个服务进行了。
另一个就是数据库拆分,分库分表。原来的单体数据库存不下的这么多信息,按服务维度拆库,比如把用户相关的存一起,形成用户库,订单放一块形成订单库,这个是拆库的过程;另一个是拆表,用户信息按照用户 ID 散列到不同的 DB 中,水平拆分,数据库的容量增大了。这样分库分表之后,写操作就会跨多个数据库了。
分布式事务理论基础
我们可以看到,在分布式架构中,跨数据库、跨服务的问题是天然存在的。一个业务操作的完成,需要经过多个服务配合完成,这些服务操作的数据可能在一个机房中,也可能跨机房存在,如果中间某一个服务因为网络或机房硬件的问题发生了抖动,怎么保证这笔业务最终的状态是正确的,比如支付场景,怎么防止我转钱给你的过程中,我的钱扣了,而对方的账户并没有收到钱。这个就是业务最终一致性的问题,是分布式事务需要解决的问题。
2PC 协议
针对这个问题,理论界也提出了解决方案,其中最为人熟知的就是二阶段协议了,简称2PC(Two Phase Commitment Protocol)两阶段提交协议。
两阶段提交协议,就是把整个过程分成了两个阶段,这其中,它把参与整个过程的实体分成了两类角色,一个叫事务管理器或事务协调者,一个叫资源管理器,事务管理器我们也把它叫做事务发起方,资源管理器称为事务参与者。
两个阶段,第一个阶段是资源准备阶段,比如我要转账,我要先查询下我的余额够不够,够的话我就把余额资源预留起来,然后告诉发起方“我准备好了”,第二个阶段,事务发起方根据各个参与者的反馈,决定事务的二阶段操作是提交还是取消。
TCC 协议
另一个协议是 TCC 协议,各个参与者需要实现3个操作:Try、Confirm 和 Cancel,3个操作对应2个阶段,Try 方法是一阶段的资源检测和预留阶段,Confirm 和 Cancel 对应二阶段的提交和回滚。
图中,事务开启的时候,由发起方去触发一阶段的方法,然后根据各个参与者的返回状态,决定二阶段是调 Confirm 还是 Cancel 方法。
蚂蚁金服分布式事务介绍
2019年,蚂蚁金服跟阿里巴巴共同开源了分布式事务 Seata ,目前 Seata 已经有 TCC、AT、Saga 模式,Seata 意为:Simple Extensible Autonomous Transaction Architecture,是一套一站式分布式事务解决方案。今天的分享也是 Seata 在蚂蚁金服内部的实践。
分布式事务在蚂蚁金服的发展
基于上述的理论,接下来我们详细看下蚂蚁金服的分布式事务实现。
经过多年的发展,蚂蚁金服内部针对不同的场景发展了几种不同的模式,最早的是 TCC 模式,也就是上面讲的 Try - confirm - Cancel,我们定义接口规范,业务自己实现这3个操作。这个模式提供了更多的灵活性,因为是业务自己实现的,用户可以介入两阶段提交过程,以达到特殊场景下的自定义优化及特殊功能的实现,这个模式能几乎满足任何我们想到的事务场景,比如自定义补偿型事务、自定义资源预留型事务、消息事务等场景。TCC 模式广泛用于蚂蚁金服内部各金融核心系统。
这里要强调一点的是,TCC 模式与底层数据库事务实现无关,是一个抽象的基于 Service 层的概念,也就是说,在 TCC 的范围内,无论是关系型数据库 MySQL,Oracle,还是 KV 存储 MemCache,或者列式存储数据库 HBase,只要将对它们的操作包装成 TCC 的参与者,就可以接入到 TCC 事务范围内。
TCC 模式的好处是灵活性,弊端是牺牲了易用性,接入难度比较大,所有参与者需要进行改造提供 Try - Confirm - Cancel 三个方法。为了解决 TCC 模式的易用性问题,蚂蚁金服分布式事务推出了框架管理事务模式(Framework - Managed Transactions,简称 FMT),也就是 Seata 中的 AT 模式。FMT 模式解决分布式事务的易用性问题,最大的特点是易于使用、快速接入、对业务代码无侵入。
XA 模式是依赖于底层数据库实现的。
Saga 模式是基于冲正模型实现的一个事务模式,现在的银行业金融机构普遍用的是冲正模型。
这期我们重点讲 TCC 和 FMT,关于 Saga 模式,之前 Saga 模式也有专场直播分享过,感兴趣的可以看一下之前的直播回顾:《Seata 长事务解决方案 Saga 模式 | SOFAChannel#10 回顾》。
TCC 模式在蚂蚁金服内的使用
首先看下 TCC 模式,主要包含一下几个模块:
- 参与者,它要实现全部的三个方法,prepare、commit 和 rollback;
- 发起方,主要是作为协调者的角色,编排各个参与者,比如调用参与者的一阶段方法,决策二阶段是执行提交还是回滚;
举个例子,比如在这个流程图中,存在1个发起方和2个参与者,两个参与者分别实现了 prepare,commit 和 rollback 接口,第一阶段被包含在发起方的本地事务模版中,也就是说,发起方负责调用各个参与者的一阶段方法,发起方的本地事务结束后,开始执行二阶段操作,二阶段结束,整个分布式事务结束。
二阶段是通过 spring 提供的事务同步器实现的,发起方在发起一个分布式事务的时候,会注册一个事务同步器,当发起方本地事务结束的时候,会进入事务同步器的回调方法中,如果发起方的本地事务失败,则在回调中自动回滚所有参与者,如果发起方的本地事务成功,则二阶段自动提交所有参与者。二阶段结束后,删除所有事务记录。
总结一下:
- 事务发起方是分布式事务的协调者;
- 分布式事务必须在本地事务模板中进行;发起方本地事务的最终状态(提交或回滚)决定整个分布式事务的最终状态;
- 发起方主职责:开启事务(start)+调用参与者一阶段方法,第二阶段由框架自动调用;
- 使用数据库持久化记录事务数据,也就是会跟踪发起方和各个参与者的状态,我们称为主事务状态和分支事务状态;
再回到刚才那张图,绿颜色的这两个模块:“start”和“二阶段”的调用,其实是框架提供的,发起方实现的时候,首先开启一个本地事务,调用 start 开启分布式事务,框架会自动注册一个 spring 事务同步器,然后发起方发起对参与者 prepare 方法的调用,当有一个 prepare 方法失败,则阻断发起方本地事务,状态置为回滚;否则,所有的参与者 prepare 成功,整个分布式事务的状态就是提交。框架会利用事务同步器自动去执行参与者的二阶段方法。
上面讲了整个流程以及发起方的实现内容,现在看下业务在实现参与者的时候,需要遵循以下规范:
- 业务模型分二阶段设计;
- 幂等控制;
- 并发控制;
- 允许空回滚;
- 防悬挂控制;
我们逐个了解一下:
- 二阶段设计
二阶段设计和幂等控制比较容易明白。二阶段设计就是一阶段的资源预留和二阶段的提交回滚。
比如以扣钱场景为例,账户 A 有 100 元,要扣除其中的 30 元。一阶段要先检查资源是否足够,账户余额是否大于等于 30 块,资源不足则需要立马返回失败;资源足够则把这部分资源预留起来,预留就是锁资源,锁的粒度可大可小,尽量是按照最小粒度、尽快释放的原则来,比如这里引入一个“冻结部分”的字段,“可用余额”在一阶段后就能立马得到释放,锁的是冻结字段。
二阶段,如果是提交则真正扣除冻结的 30 元;如果是回滚的话,则把冻结部分加回可用余额里。
我们看个具体的客户案例,网商银行在使用 TCC 时,划分了三层,最上一层是具体的业务平台,承接着外部不断变化的业务需求;中间是资产交换服务,是事务发起方层,由它来发起和编排各种不同的事务链路;最底下一层是事务参与者层,提供最基础的服务,比如存款核心提供的存入、支出、冻结、解冻服务,借记账务的各种原子服务等。
看下我们日常生活中常见的几个金融业务场景,支出、存入、冻结、解冻、提现、手续费和销户。提现场景,比如信用卡提现至银行卡,类似 A 到 B 的转账;手续费,跟转账类似;销户场景,主要操作是校验余额、账户结清、最后改状态销户,没有用到分布式事务。
下面重点介绍一下其他 4 个场景:支出(扣款)、存入(记入)、冻结和解冻四个 Case。
首先,看下账户表的设计,前面说过,在设计的时候,需要尽可能减少锁的时间和锁的粒度,这里账户表有这4个字段:当前余额、未达金额、业务冻结金额和预冻结金额。显示的可用余额 = 当前金额 - 预冻结 - 业务冻结金额。
支出(扣款)场景
先来看下支出(扣款)场景下,账户表里各字段的数额变化。初始状态下,显示的账户余额,和当前余额是一致的。TCC 的一阶段检查并预留资源,这里对应的资源是“预冻结金额”字段,预冻结金额设置为 100 元,当前余额不变。因为 100 块被预冻结了,显示给用户的可用余额现在是 900 元。如果二阶段是提交的话,就释放预冻结金额,扣除当前余额,账户的当前余额就是 900 元。如果二阶段不是提交,是回滚,这里就是把一阶段的资源释放,也就是把预冻结金额释放回去,显式的账户余额重新变成 1000 元。
存入场景
上面是支出(扣款)场景,再来看下存入的场景。初始状态还是当前余额和显式的可用余额都是1000元。因为是存入,一阶段的话就是“未达金额”加 100 元,显示的可用余额还是不变。二阶段如果是提交,就把未达金额清除,把这部分的钱加到当前余额,当前余额就是 1100 元了。如果二阶段是回滚,直接清除一阶段的未达金额即可。
冻结场景
冻结场景则是在一阶段是资源预留,就是预冻结,预冻结金额字段设置为 100 元,显示给用户的可用余额也要少 100 块。二阶段如果是提交,就是真正冻结,把预冻结金额释放,添加业务冻结金额。二阶段回滚的话,就是把一阶段的预冻结释放。
解冻场景
最后看下解冻场景,一阶段检查账户状态是不是可用,二阶段如果提交,就释放冻结金额,显示的可用余额就多了 100 元。二阶段如果是回滚状态,就什么都不用做。
以上分享了接入 TCC 如何进行二阶段设计以及如何进行资源预留,用实际的金融场景分析了下 TCC 一二阶段需要做的事情。因为二阶段设计是 TCC 接入的关键,所以进行了重点阐述。接下来我们继续看 TCC 设计的其他规范。
- 幂等控制
幂等控制,就是 Try-Confirm-Cancel 三个方法均需要保持幂等性。
- 并发控制
并发控制即当两个并发执行的分布式事务操作同一个账号时,冻结的部分是相互隔离的,也就是 T1 冻结金额只能被事务 1 使用,T2 冻结金额只能被事务 2 使用。冻结资源与事务 ID 之间建立关联关系。
- 允许空回滚
首先对空回滚的定义就是 Try 未执行,Cancel 先执行了。正常是一阶段的请求先执行,然后才是二阶段的请求。出现空回滚的原因,是网络丢包导致的,调用 Try 方法时 RPC timeout 了,分布式事务回滚,触发 Cancel 调用;参与者未收到 Try 请求而收到了 Cancel 请求,出现空回滚。
我们在设计参与者时,要支持这种空回滚。
- 防悬挂
悬挂的定义是 Cancel 比 Try 先执行。不同于空回滚,空回滚是 Try 方法的请求没有收到。悬挂是 Try 请求到达了,只不过由于网络拥堵,Try 的请求晚于二阶段的 Cancel 方法。
整个流程是这样的:
- 调用 TCC 服务 Try 方法,网络拥堵(未丢包),RPC超时;
- 分布式事务回滚;
- TCC 服务 Cancel 被调用,执行了空回滚;整个分布式事务结束;
- 被拥堵的 Try 请求到达 TCC 服务,并被执行;出现了二阶段 Cancel 请求比一阶段 Try 请求先执行的情况,TCC 参与者悬挂;
解决悬挂的问题,可以跟踪事务的执行,如果已经回滚过了,一阶段不应该正常执行,这时候要拒绝 Try 的执行。
FMT 模式在蚂蚁金服内的使用
接下来我们来看一下 FMT(Framework-Managerment-Transaction)框架管理事务模式。
之前介绍几个事务模式的时候,说过 TCC 模式虽然灵活,功能强大,能做很多定制和优化,但是使用难度上比较大,业务系统要进行二阶段改造,编码工作非常多。
针对那些对性能要求并不高,业务体量并不大的中小业务,我们推出了 FMT 模式——框架管理事务,从名字上看,就是大部分工作由框架自动完成,业务只需要关注实现自己的业务 SQL 即可。
FMT 还是基于二阶段的模型,业务只需要关注一阶段实现自己的业务 SQL,二阶段的自动提交回滚由框架来完成。
框架托管的二阶段,需要基于对一阶段的分析。在一阶段中,会执行下面几个步骤:对 SQL 进行解析,提取表的元数据,保存 SQL 执行前的值,执行 SQL,保存执行后的快照,保存行锁。
下面看下每个阶段具体做的事:
查询操作不涉及事务,我们这里以一个更新操作为例,首先要对操作的 SQL 进行语法语义分析,提取出关于这条记录的全部信息,包括是属于哪张表、查询条件是什么、有哪些字段、这些记录的主键等,这些信息可以通过 JDBC MetaData api 就能拿到。
然后我们开始保存执行前的快照数据,把目标记录的所有字段的当前值存到 undo log 里,存完后真正执行 SQL,SQL 执行后原来的一些字段值就已经产生变化了,我们把新的快照数据存到 redo log 里。最后把表名称和记录主键值存到行锁表,代表当前这个事务正在操作的是哪些记录。
有了这些信息后,框架就完全能自己去执行二阶段操作了。比如,当事务需要进行二阶段提交,因为在一阶段里业务SQL 已经执行了,二阶段只需要把产生的中间数据删掉即可。当二阶段回滚时,因为我们保存了 SQL 执行的快照数据,所以还原回执行前的快照数据即可,同时把中间数据删掉。
这里我们知道了 undo 和 redo log 的作用,接下来讲讲行锁。行锁是用来进行并发控制的。当一个事务在操作一条记录前,会先去行锁表里查下有没有这条记录的锁信息,如果有,说明当前已经有一个事务抢占了,需要等待那个事务把锁释放。图中,事务 1 在一阶段对记录上锁,这个时候事务 2 进来,只能等待,等事务 1 二阶段提交,把锁释放,事务 2 这时候才能加锁成功。
极致性能优化
最后,我们看看在蚂蚁金服内部,针对双十一、双十二这种大促,为了达到更好的性能状态,做的一些优化。
二阶段异步化
一个是二阶段异步化,因为一阶段的结果已经能决定整个事务的状态了,而且资源也都预留好了,剩下的二阶段可以等请求峰值过后再去执行。这样,分布式事务耗时由执行 prepare+commit 或者 prepare+Cancel 缩减成 prepare,提高了吞吐量。虽然结果有延迟的,但最终结果无任何影响。
异步的二阶段方法,在请求洪峰过后,会由事务恢复服务捞起执行。
同库模式
另一个优化,在事务记录上。分布式事务在推进过程中,会记录事务日志,如果这个事务日志是放到 Server 这边的,发起方更新事务状态时,需要跨 RPC 调用到业务方那边,每次记录状态的就是同库执行的,减少 RPC 调用次数。
总结
以上就是本期分享的全部内容,我们先从事务产生的背景入手,在现在分布式架构的体系结构下,跨服务协同调用是常态,而网络、数据库、机器等都具有不可靠性,如果保证这中间操作要么全部成功,要么全部失败,是大家面临的共同问题,特别是金融场景下,对解决这个问题更有迫切性,蚂蚁金服作为一家金融科技公司,在这方面也进行了探索,积累了很多经验。
在介绍蚂蚁金服的分布式事务中间件之前,先介绍了一些分布式事务的理论背景,包括两阶段协议和 TCC 协议。基于理论背景,重点介绍了蚂蚁金服在分布式事务上的 TCC、FMT 模式的应用,分享了实现原理和设计规范以及 TCC 二阶段设计等。最后介绍了针对双十一双十二这种大促活动,如何进行二阶段异步化和同库模式的优化,来支撑零点峰值时的洪峰请求。
以上就是本期分享的全部内容,如果大家对蚂蚁金服在分布式事务中的实践以及 Seata 有问题跟兴趣,也可以在群内与我们交流。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。