跳至主要內容

AT vs XA

gqzclGolang后端事务后端分布式大约 10 分钟

AT vs XA

AT 这种事务模式是阿里开源的seata主推的事务模式,本文先给出了XA 和 AT之间的特性比较,然后详解AT的原理,并对其中的问题进行深入探讨

XAAT
脏回滚open in new window存在
SQL支持度open in new window全部支持部分支持
脏读open in new window
应用侵入性open in new window无侵入无侵入
性能open in new window较低较低
数据库支持open in new window主流数据库都支持理论上可扩展至NoSQL

原理

AT 从原理上面看,与 XA 的设计有很多相近之处。XA 是数据库层面实现的二阶段提交, AT 则是应用/驱动层实现的二阶段提交。建议您了解了XAopen in new window相关的知识后,来阅读这篇文章,这样能够更快更好的掌握 AT 的原理与设计。

AT的角色和XA一样分为3个,但是起了不一样的名称,大家注意分辨:

  • RM 资源管理器,是业务服务,负责本地数据库的管理,与XA中的RM一致
  • TC 事务协调器,是Seata服务器,负责全局事务的状态管理,负责协调各个事务分支的执行,相当于XA中的TM
  • TM 事务管理器,是业务服务,负责全局事务的发起,相当于XA中的APP

AT 的第一阶段为prepare,它在这一阶段会完成以下事情:

  1. RM 侧,用户开启本地事务

  2. RM 侧,用户每进行一次业务数据修改,假设是一个update语句,那么 AT 会做以下内容:

    1. 根据update的条件,查询出修改前的数据,该数据称为BeforeImage
    2. 执行update语句,根据BeforeImage中的主键,查询出修改后的数据,该数据称为AfterImage
    3. 将BeforeImage和AfterImage保存到一张undolog表
    4. 将BeforeImage中的主键以及表名,该数据称为lockKey,记录下来,留待后续使用
  3. RM 侧,用户提交本地事务时,AT 会做以下内容:

    1. 将2.4中记录的所有的lockKey,注册到 TC(即事务管理器seata)上
    2. 3.1中的注册处理会检查 TC 中,是否已存在冲突的主键+表名,如果有冲突,那么AT会睡眠等待后重试,没有冲突则保存
    3. 3.1成功完成后,提交本地事务

如果 AT 的第一阶段所有分支都没有错误,那么会进行第二阶段的commit,AT 会做以下内容:

  1. TC 会将当前这个全局事务所有相关的lockKey删除
  2. TC 通知与当前这个全局事务相关的所有业务服务,告知全局事务已成功,可以删除undolog中保存的数据
  3. RM 收到通知后,删除undolog中的数据

如果 AT 的第一阶段有分支出错,那么会进行第二阶段的rollback,AT 会做以下内容:

  1. TC 通知与当前这个全局事务相关的所有业务服务,告知全局事务失败,执行回滚

  2. RM 收到通知后,对本地数据的修改进行回滚,回滚原理如下:

    1. 从undolog中取出修改前后的BeforeImage和AfterImage
    2. 如果AfterImage与数据库中的当前记录校验一致,那么使用BeforeImage中的数据覆盖当前记录
    3. 如果AfterImage与数据库中的当前记录不一致,那么这个时候发生了 脏回滚 ,此时需要人工介入解决
  3. TC 待全局事务所有的分支,都完成了回滚,TC 将此全局事务所有的lockKey删除

脏回滚

AT 模式的一个突出问题是rollback中2.3的脏回滚难以避免。以下步骤能够触发该脏回滚:

  1. 全局事务g1对数据行A1进行修改 v1 -> v2
  2. 另一个服务将对数据行A1进行修改 v2 -> v3
  3. 全局事务g1回滚,发现数据行A1的当前数据为v3,不等于AfterImage中的v2,回滚失败

这个脏回滚一旦发生,那么分布式事务框架没有办法保证数据的一致性了,必须要人工介入处理。想要避免脏回滚,需要把所有对这个表的写访问,都加上特殊处理(在Seata的Java客户端中,需要加上GlobalLock注解)。这种约束对于一个上了一定规模的复杂系统,是非常难以保证的。

XA 在数据库系统层面实现了行锁,原理与普通事务相同,因此一旦出现两个事务访问同一行数据,那么后一个事务会阻塞,完全不会有脏回滚的问题

SQL支持度

AT 模式并未支持所有的SQL,它的原理是在应用层解析SQL,然后根据不同的SQL生成BeforeImage和AfterImage,一方面不同的SQL可能需要采用不同的逻辑来生成这些Image,另一方面不同的数据库语法不同,因此不常见的SQL,AT可能不支持。

XA 是数据库层面支持的,因此对所有的DML SQL都支持,不会出现问题

脏读

AT 模式会发生脏读,在 AT 模式下发生如下的执行序列:

  1. 全局事务g1对数据行A1进行修改 v1 -> v2
  2. 另一个服务将读取数据行A1,获得数据 v2
  3. 全局事务g1回滚,将数据行A1改回 v2 -> v1

这里面步骤2读取的数据是v2,是一个中间态数据。在Seata的手册中,虽然也有一些方法能够避免AT模式下,但是涉及到注解和sql改写,并不优雅。

XA模式下,由于还没有进行xa commit,那么步骤2根据MVCC读取到的数据依然是v1,没有AT模式中的脏读的困扰。

应用侵入性

AT 在最简单的情况下,通过在代码中添加注解,就能够把分布式事务引入到应用中,因此很多人认为是无侵入的。但是前面给出的脏读,脏回滚,SQL支持等,是开发人员必须考虑的,并进行设计,否则引入了相关注解变成全局事务之后,发生这些问题,导致线上应用故障,产生的后果会更严重。因此AT的无侵入不是真正的无侵入,仅仅是表面上代码的“无侵入”,但是设计上“侵入”了(不可以用AT未支持的SQL),行为上“侵入”了(需要容忍脏回滚),还会“侵入”其他项目(如果其他项目也写了同一张表,也需要加GlobalLock注解)。

XA 没有前面的问题,它的侵入性很低,在Java语言中,也同样做到通过加注解,而不用修改Java代码就完成分布式事务的引入。

注解是Java中很有特色的语法,是面向切面编程的一个典范。注解也有一定的理解成本,在Go和其他语言领域,并未引入注解,一个重要的理由是,通过显式的代码调用,更容易让读者理解中间发生了什么,可读性更好。DTM 在各语言的SDK中保持了统一的接口,让多语言的分布式事务更加简单,因此各语言的SDK大多未采用注解的这种接口方式。

性能分析

从原理的详细步骤看,XA事务的性能应高于AT,分析如下:

AT 模式下,RM侧,上述原理过程中,执行的SQL如下:

  1. 开启事务
  2. 查询BeforeImage数据
  3. 执行update
  4. 查询AfterImage数据
  5. 将BeforeImage,AfterImage插入到undolog中
  6. 提交事务
  7. 事务完成后,删除BeforeImage和AfterImage

而 XA 模式下,RM侧,执行的SQL如下:

  1. xa begin
  2. 执行update
  3. xa end
  4. xa prepare
  5. xa commit

两者对比,相关的开启/提交事务是两个模式都需要的,性能差异不大。但是从执行的DML操作来看,AT 下的 SQL 数量为:3 writes,2 read,比 XA 下仅一个update多出许多,因此在性能上会有较大的差距

从上述理论分析,XA 事务性能会大幅高于AT,应当可以在postgres数据库上验证出来;而mysql数据库,在当前的5.8版本上,由于xa prepare后,需要将当前连接断开才能够在其他连接上xa commit,所以会有一个重新创建连接的开销。

我同时也做了性能实测,详细的测试过程和结果数据,参考 xa-at benchopen in new window

dtm实现的XA事务,为了在极端情况下,也能保证XA事务能够正确的被清理,会在业务事务中对子事务屏障表进行插入,因此会比上述理论分析中,多一个sql写入。

我们可以看到,最终的结果XA性能优于AT。如果未来Mysql完善了XA的实现,可以不用关闭当前连接也能够允许其他连接提交xa事务,那么XA的性能还能够提升一大截。

但AT和XA两种模式,由于数据锁在整个分布式事务期间的存在,降低了并发度,因此性能都低于其他模式。当您的并发度较高时,建议使用其他无全局锁的事务模式

数据库支持

AT 目前支持了多个主流数据库,而且从理论上看,也能够扩展到非SQL数据库,但目前暂未看到支持非SQL数据库的扩展。

  • AT与Redis:虽然Redis也支持事务,但Redis的事务支持主要是通过lua脚本来做的,与传统数据库的Begin Transaction/Commit不一样,因此上述生成前后镜像的原理并不适用Redis,因此AT想要支持Redis会非常困难,目前未看到有这样的尝试
  • AT与Mongo:Mongo的事务支持与Mysql类似,但Mongo的操作类型很多,而且在主键规范上面,与SQL数据库有很大不同,想要正确生成前后镜像的工作量庞大,目前未看到有这样的尝试

XA 模式则需要底层数据库支持,目前主流的数据库,Mysql,Postgres,Oracle等都已支持。如果分布式事务涉及mongo呢?这个时候需要考虑其他事务模式,DTM中有关于Redis,Mongo的事务例子

小结

mysql在版本5.6中,xa相关API存在bug。如果当前连接在xa prepare之后,连接断开,那么这个连接未完成的事务会被自动回滚。这样的bug导致mysql的XA模式是无法保证正确性的,在各种应用crash中,可能导致数据不一致。因此AT在mysql的5.6版本及更低版本使用中,是具有很高应用价值的。

另外部分大厂的数据库是禁止使用XA事务的,这种特定场景下,选型AT模式,也是合理的。

对于其他场景,建议优先考虑 XA 事务。