AT模式同样是分阶段提交的事务模型,不过缺弥补了XA模型中资源锁定周期过长的缺陷。

Seata的AT模型

image-20210724175327511.png

流程梳理

我们用一个真实的业务来梳理下AT模式的原理。
比如,现在有一个数据库表,记录用户余额。

id money
1 100

其中一个分支业务要执行的SQL为

  1. update tb_account set money = money - 10 where id = 1

AT模式下,当前分支事务执行流程如下:
一阶段:

  1. TM发起并注册全局事务到TC
  2. TM调用分支事务
  3. 分支事务准备执行业务SQL
  4. RM拦截业务SQL,根据where条件查询原始数据,形成快照。
  5. RM执行业务SQL,提交本地事务,释放数据库锁。此时 money = 90
  6. RM报告本地事务状态给TC

二阶段:

  1. TM通知TC事务结束
  2. TC检查分支事务状态
    1. 如果都成功,则立即删除快照
    2. 如果有分支事务失败,需要回滚。读取快照数据,将快照恢复到数据库。此时数据库再次恢复为100

流程图:
image-20210724180722921.png

脏写问题

在多线程并发访问AT模式的分布式事务时,有可能出现脏写问题,如图:
image-20210724181541234.png
解决思路就是引入了全局锁的概念。在释放DB锁之前,先拿到全局锁。避免同一时刻有另外一个事务来操作当前数据。
image-20210724181843029.png

AT与XA的区别

  • XA模式一阶段不提交事务,锁定资源;AT模式一阶段直接提交,不锁定资源。
  • XA模式依赖数据库机制实现回滚;AT模式利用数据快照实现数据回滚。
  • XA模式强一致;AT模式最终一致

    AT与XA的优缺点

    AT模式的优点:

  • 一阶段完成直接提交事务,释放数据库资源,性能比较好

  • 利用全局锁实现读写隔离
  • 没有代码侵入,框架自动完成回滚和提交

AT模式的缺点:

  • 两阶段之间属于软状态,属于最终一致
  • 框架的快照功能会影响性能,但比XA模式要好很多

    实现AT模式

    AT模式中的快照生成、回滚等动作都是由框架自动完成,没有任何代码侵入,因此实现非常简单。
    只不过,AT模式需要一个表来记录全局锁、另一张表来记录数据快照undo_log。
  1. 创建数据库表记录快照和全局锁 ```sql SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0;

— 数据快照表,导入到我们写的微服务相关的数据库里面,如果微服务用了多个数据库,每个数据库都创一个这个undo_log表


DROP TABLE IF EXISTS undo_log; CREATE TABLE undo_log ( branch_id bigint(20) NOT NULL COMMENT ‘branch transaction id’, xid varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT ‘global transaction id’, context varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT ‘undo_log context,such as serialization’, rollback_info longblob NOT NULL COMMENT ‘rollback info’, log_status int(11) NOT NULL COMMENT ‘0:normal status,1:defense status’, log_created datetime(6) NOT NULL COMMENT ‘create datetime’, log_modified datetime(6) NOT NULL COMMENT ‘modify datetime’, UNIQUE INDEX ux_undo_log(xid, branch_id) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = ‘AT transaction mode undo table’ ROW_FORMAT = Compact;


— 全局锁表,lock_table导入到TC服务关联的数据库,tc服务启动之前的时候创建了一个数据库


DROP TABLE IF EXISTS lock_table; CREATE TABLE lock_table ( row_key varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, xid varchar(96) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, transaction_id bigint(20) NULL DEFAULT NULL, branch_id bigint(20) NOT NULL, resource_id varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, table_name varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, pk varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, gmt_create datetime NULL DEFAULT NULL, gmt_modified datetime NULL DEFAULT NULL, PRIMARY KEY (row_key) USING BTREE, INDEX idx_branch_id(branch_id) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

SET FOREIGN_KEY_CHECKS = 1;

  1. 2. 修改配置文件
  2. ```yaml
  3. seata:
  4. data-source-proxy-mode: AT # 默认就是AT
  1. 给发起全局事务的入口方法添加@GlobalTransactional注解,看 实现XA模式
  2. 重启服务测试,也是看 实现XA模式