AT 这种事务模式是阿里开源的seata主推的事务模式,本文会详解AT的原理,并将它与XA模式进行比较
原理
AT 从原理上面看,与 XA 的设计有很多相近之处。XA 是数据库层面实现的二阶段提交, AT 则是应用/驱动层实现的二阶段提交。建议您了解了XA相关的知识后,来阅读这篇文章,这样能够更快更好的掌握 AT 的原理与设计。
AT的角色和XA一样分为3个,但是起了不一样的名称,大家注意分辨:
- RM 资源管理器,是业务服务,负责本地数据库的管理,与XA中的RM一致
- TC 事务协调器,是Seata服务器,负责全局事务的状态管理,负责协调各个事务分支的执行,相当于XA中的TM
- TM 事务管理器,是业务服务,负责全局事务的发起,相当于XA中的APP
AT 的第一阶段为prepare,它在这一阶段会完成以下事情:
- RM 侧,用户开启本地事务
RM 侧,用户每进行一次业务数据修改,假设是一个update语句,那么 AT 会做以下内容:
- 根据update的条件,查询出修改前的数据,该数据称为BeforeImage
- 执行update语句,根据BeforeImage中的主键,查询出修改后的数据,该数据称为AfterImage
- 将BeforeImage和AfterImage保存到一张undolog表
- 将BeforeImage中的主键以及表名,该数据称为lockKey,记录下来,留待后续使用
RM 侧,用户提交本地事务时,AT 会做以下内容:
- 将2.4中记录的所有的lockKey,注册到 TC(即事务管理器seata)上
- 3.1中的注册处理会检查 TC 中,是否已存在冲突的主键+表名,如果有冲突,那么AT会睡眠等待后重试,没有冲突则保存
- 3.1成功完成后,提交本地事务
如果 AT 的第一阶段所有分支都没有错误,那么会进行第二阶段的commit,AT 会做以下内容:
- TC 会将当前这个全局事务所有相关的lockKey删除
- TC 通知与当前这个全局事务相关的所有业务服务,告知全局事务已成功,可以删除undolog中保存的数据
- RM 收到通知后,删除undolog中的数据
如果 AT 的第一阶段有分支出错,那么会进行第二阶段的rollback,AT 会做以下内容:
- TC 通知与当前这个全局事务相关的所有业务服务,告知全局事务失败,执行回滚
RM 收到通知后,对本地数据的修改进行回滚,回滚原理如下:
- 从undolog中取出修改前后的BeforeImage和AfterImage
- 如果AfterImage与数据库中的当前记录校验一致,那么使用BeforeImage中的数据覆盖当前记录
- 如果AfterImage与数据库中的当前记录不一致,那么这个时候发生了脏回滚,此时需要人工介入解决
- TC 待全局事务所有的分支,都完成了回滚,TC 将此全局事务所有的lockKey删除
问题分析
AT 模式的一个突出问题是rollback中2.3的脏回滚难以避免。以下步骤能够触发该脏回滚:
- 全局事务g1对数据行A1进行修改 v1 -> v2
- 另一个服务将对数据行A1进行修改 v2 -> v3
- 全局事务g1回滚,发现数据行A1的当前数据为v3,不等于AfterImage中的v2,回滚失败
这个脏回滚一旦发生,那么分布式事务框架没有办法保证数据的一致性了,必须要人工介入处理。想要避免脏回滚,需要把所有对这个表的写访问,都加上特殊处理(在Seata的Java客户端中,需要加上GlobalLock注解)。这种约束对于一个上了一定规模的复杂系统,是非常难以保证的。
AT vs XA
上述脏回滚问题,在 XA 事务中不会出现,因为 XA 事务是在数据库层面实现的,当另一个服务对为数据行A1进行修改时,会因为行锁被阻塞,与普通事务的表现完全一样,不会产生问题。
另外 XA 不会发生脏读,而 AT 会发生脏读,考虑AT下的如下执行步骤:
- 全局事务g1对数据行A1进行修改 v1 -> v2
- 另一个服务将读取数据行A1,获得数据 v2
- 全局事务g1回滚,将数据行A1改回 v2 -> v1
这里面步骤2读取的数据是v2,是一个中间态数据。在Seata的手册中,虽然也有一些方法能够避免AT模式下,但是涉及到注解和sql改写,并不优雅。而在XA模式下,由于还没有进行xa commit,那么步骤2根据MVCC
读取到的数据依然是v1,没有AT模式中的脏读的困扰。
性能分析
从原理的详细步骤看,XA事务的性能高于AT,分析如下:
AT 模式下,RM侧,上述原理过程中,执行的SQL如下:
- 开启事务
- 查询BeforeImage数据
- 执行update
- 查询AfterImage数据
- 将BeforeImage,AfterImage插入到undolog中
- 提交事务
- 事务完成后,删除BeforeImage和AfterImage
而 XA 模式下,RM侧,执行的SQL如下:
- xa begin
- 执行update
- xa end
- xa prepare
- xa commit
两者对比,相关的开启/提交事务是两个模式都需要的,性能差异不大。但是从执行的DML操作来看,AT 下的 SQL 数量为:3 writes,2 read,比 XA 下仅一个update多出许多,因此在性能上会有较大的差距
从上述理论分析,XA 事务性能会大幅高于AT,应当可以在postgres数据库上验证出来;而mysql数据库,在当前的5.8版本上,由于xa prepare后,需要将当前连接断开才能够在其他连接上xa commit,所以会有一个重新创建连接的开销,最终性能对比参考下一节。
性能实测
上述进行了理论上的性能分析,我同时也做了性能实测,详细的测试过程和结果数据,参考 xa-at bench
dtm实现的XA事务,为了在极端情况下,也能保证XA事务能够正确的被清理,会在业务事务中对子事务屏障表进行插入,因此会比上述理论分析中,多一个sql写入。
我们可以看到,最终的结果XA性能优于AT。如果未来Mysql完善了XA的实现,可以不用关闭当前连接也能够允许其他连接提交xa事务,那么XA的性能还能够提升一大截。
AT的意义
mysql在版本5.6中,xa相关API存在bug。如果当前连接在xa prepare之后,连接断开,那么这个连接未完成的事务会被自动回滚。这样的bug导致mysql的XA模式是无法保证正确性的,在各种应用crash中,可能导致数据不一致。因此AT在mysql的5.6版本及更低版本使用中,是具有很高应用价值的。
另外部分大厂的数据库是禁止使用XA事务的,这种特定场景下,选型AT模式,也是合理的。
对于其他场景,建议优先考虑 XA 事务。
小结
作者对AT模式的完整实现源码,并未完整阅读。上述的相关原理是根据自己阅读相关资料,并参考了seata-golang的源代码而写。文中如果不准确之处,希望各位读者帮忙指正
欢迎访问 https://github.com/dtm-labs/dtm 并star支持我们
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。