• SpringCloud Alibaba - Seata 四种分布式事务解决方案(XA、AT)+ 实践部署(上)


    目录

    一、Seata 分布式事务解决方案

    1.1、XA 模式

    1.1.1、XA模式理论

    第一阶段:

    第二阶段:

    1.1.2、Seata 框架中的 XA 模式

    第一阶段:

    第二阶段:

    1.1.3、XA 模式的优缺点

    1.2.4、实现Seata 的 XA 模式

    a)修改 application 文件(每一个参与事务的微服务)

    b)给发起全局事务中的入口方法添加 @GlobalTransactional 注解

    c)重启服务并测试

    1.2、AT 模式

    1.2.1、AT 模式理论

    第一阶段:

    第二阶段:

    问题:XA模式 和 AT模式 的区别?

    1.2.2、AT 模式脏写问题

    1.2.3、解决办法:写隔离

    1.2.4、AT 模式的优缺点

    1.2.5、实现 AT 模式

    a)建表

    b)修改 application.yml 文件

    c)重启服务并测试


    一、Seata 分布式事务解决方案


    1.1、XA 模式

    1.1.1、XA模式理论

    XA 模式是 X/Open 组织定义的一个分布式事务处理的标准,可以认为是分布式事务领域最早的标准了,所以几乎所有主流的数据库(mysql、oracle......)都是实现了这种标准. 换言之,这些数据库内部已经能够基于 XA 模式实现分布式事务了.

    这种模式把分布式定义成了以下两个阶段

    第一阶段:

    这一阶段也叫做准备阶段,事务协调者 会向 资源管理器 发起一个准备的请求,也就是告诉他:“你可以去执行业务 sql 了啊,但是执行完了以后不可以提交!”. 随后,资源管理器 就执行数据库业务,然后将执行结果告诉 事务协调者(执行成功了,就是就绪状态;失败了,就是 fail 状态),这样 事务协调者 就可以根据结果判断下一个阶段要干什么.

    第二阶段:

    事务协调者收到 RM 的反馈之后,就会进行判断.  如果都执行成功了,就通知这些 RM:“你们可以提交了啊”,然后 RM 收到请求,就把事务提交了,整个事务也就结束了;如果有任意一个服务执行失败了,那么事务协调者就会通知这些RM,让他们全部回滚.

    可以看出 XA 模式是一种强一致性的事务,软可用性,因为事务没有提交的时候,拿不到数据库锁,是不能进行其他操作的.

    1.1.2、Seata 框架中的 XA 模式

    Seata 的 XA 模式和上述讲的 XA 模式大体相似,只是多了一个 TM 的概念. 如下图

    第一阶段:

    Seata 实际上就是多了一个 TM 的概念, 也就是事务管理者. TM 一上来就是要去注册全局事务,作为分布式事务的入口,接下来就会去调用各个分支事务.  每个分支事务中又有一个 RM,RM 就回去 TC 里面注册分支事务,然后执行业务 sql,但是执行完后不能提交,只是去报告一下事务的状态.  到此,第一阶段结束.

    第二阶段:

    TM 这里的入口方法执行完了以后,就要去告诉 TC 说:“我这边执行完了,接下来就看你那边的情况了~”. 随后 TC 就会去检查各个分支事务报告的状态,然后向 RM 发送一个信号——如果第一阶段都执行成功了,那么就提交,反之,则进行回滚.   RM 收到信号之后就可以进行 提交/回滚 事务了.

    1.1.3、XA 模式的优缺点

    优点:

    1. 支持强一致:执行完业务 sql 以后,不进行提交事务,而是等到 TC 协调完后,在进行 提交/回滚. 

    2. 实现起来简单:因为数据库本身就支持 XA 模式,Seata 只是在数据库的 XA 模式上做了一层简单的封装.  如果只看核心部分,就没什么差别了.

    缺点:

    1. 弱可用性,性能差:第一阶段不提交,等待第二阶段才提交,这个过程中会占用数据库锁(相当于对系统资源的一种浪费).  一旦分支事务特别多的情况下,业务耗时就会很久.

    2. 依赖数据库底层实现:虽然用起来简单了,但是如果我用的不是 mysql 这种关系型数据库,而是 redis 这种非关系型数据库就不行了.

    1.2.4、实现Seata 的 XA 模式

    a)修改 application 文件(每一个参与事务的微服务

    开启 seata 的 XA 模式

    1. seata:
    2. data-source-proxy-mode: XA # 开启数据源代理的XA模式

    b)给发起全局事务中的入口方法添加 @GlobalTransactional 注解

    当前案例的全局事务入口是 订单服务中的 创建订单的 create 方法(案例架构如下图).

    这里只需要在 create 方法上添加 @GlobalTransactional 注解即可

    1. @Override
    2. @GlobalTransactional
    3. public Long create(Order order) {
    4. // 创建订单
    5. orderMapper.insert(order);
    6. try {
    7. // 扣用户余额
    8. accountClient.deduct(order.getUserId(), order.getMoney());
    9. // 扣库存
    10. storageClient.deduct(order.getCommodityCode(), order.getCount());
    11. } catch (FeignException e) {
    12. log.error("下单失败,原因:{}", e.contentUTF8(), e);
    13. throw new RuntimeException(e.contentUTF8(), e);
    14. }
    15. return order.getId();
    16. }

    c)重启服务并测试

    Ps:这里不要忘了先启动 Nacos 和 TC 服务.

    以下分别为用户余额和库存数量.

    1. 这里使用 Postman 发送以下请求(减少 9 个库存数量,用户余额减少 200)

    http://localhost:8082/order?userId=user202103032042012&commodityCode=100202003032041&count=9&money=200

    2. 随后可以看到订单微服务返回 500 错误.

    在扣款微服务中,先进行了扣款业务,但是随后 rm 又执行了回滚

    在库存微服务中,可以看到扣减失败.

    最后观察数据库中数据,可以看到事务已经回滚如初.

    1.2、AT 模式

    1.2.1、AT 模式理论

    AT 模模式也是分阶段性提交的事务模型. 他的出现正式为了解决 XA 模式中资源锁定周期过长的一个缺陷.

    AT 模式也分为两个阶段

    第一阶段:

    TM 去开启全局事务,作为分布式事务的入口,接着又会调用每一个分支事务 ,然后每个分支事务里的 rm 都会去注册分支事务,并执行本地的业务 sql,然后直接提交(这里就和 XA 不一样了). 因此 AT 模式就不需要像 XA 那样锁定资源,性能上要优于 XA 模式.

    这里直接提交,万一有人失败了呢,没办法回滚不就导致状态不一致了吗?

    实际上 AT 模式在执行 sql 之前,rm 会给当前数据生成一个快照,这个快照的名字叫 “undo log”,这就像是 redis 中 RDB 持久化时也会生成快照,那么即使服务重启,也可以根据快照恢复数据.  那么这样就即使有分支事务执行 sql 的时候失败了,也可以根据快照恢复如初,大胆提交就对了.

    第二阶段:

    TM 看到业务结束了,就会去通知 TC,那么 TC 就会判断是提交还是回滚.  如果分支事务的状态都是成功的,那就可以把第一阶段准备的快照给删了(删快照这个动作是异步的,因为第一阶段都成功了,也提交了,后面的事情就可以用一个线程独立去做,提高了效率).  如果第一阶段有人失败了,就要基于 undo log 恢复数据,恢复以后这个 log 也就没用了,最后也会删除.

    问题:XA模式 和 AT模式 的区别?

    1. XA模式在第一阶段的时候不会进行事务的提交,而 AT 模式执行完业务 sql 之后会立即提交事务,不会锁定资源,因此性能会好一些.

    2. XA 依赖于数据库的机制来做回滚,而 AT 模式因为已经提交了,就不能回滚,他是通过给自己生成快照的方式来实现数据的恢复.

    3. XA 模式是强一致,而 AT 是最终一致,因为 AT 在业务 sql 执行之后直接提交了,有人失败,有人成功,那么这个时候状态肯定是不一致的,也就是一个软状态,只有在第二阶段,基于快照恢复了数据,才能到达一个最终一致的效果.

    1.2.2、AT 模式脏写问题

    AT 模式在第一阶段执行完业务 sql 以后会直接提交,那么资源锁定的周期就比较短,效率高,但也正因为他提前释放了锁,就导致在高并发的场景下,会出现安全问题~

    例如我这里有一个用户余额表,里面记录了 money 字段,表示余额. 

    1. 现在有一个线程开启了事务1,首先他会去获取数据库锁,接着生成快照(money = 100),然后执行业务 sql(把 money 值改成 90),最后提交事务,锁也就释放了. 
    2. 此时,另一个线程开启了 事务2 ,拿到锁以后保存快照(money = 90),执行业务 sql(把 money 值改成 80),最后提交释放锁.

    到了第二阶段,假如说 事务1 要进行回滚(通过快照恢复数据),但是 事务1 的快照数据 money = 100!这个时候就出问题了,恢复了以后相当于 事务2 这哥们啥也没做. 这就是所谓的脏写问题.

    1.2.3、解决办法:写隔离

    上述过程出现的问题,归根结底还是隔离性的原因,如果第一阶段和第二阶段整体都是一个锁定状态,别人根本无法插入进来,因此 AT 模式就引入了一个东西叫 “全局锁”.

    Ps:全局锁由 TC 来控制,用来记录当前哪个事务在操作哪种表的哪一行数据. 这张表了主要有 事务id、表名字、pk主键(记录这张表的哪一行数据).

    1. 有了全局锁,那么在刚刚上述栗子的 事务1中,执行完业务 sql 就会去获取全局锁,就表明这张表的这行数据是我 事务1 才能拥有的,此时就可以大胆的提交事务. 
    2. 当 事务2 执行完业务 sql 之后,也尝试去获取全局锁,但是由于 事务1 已经获取了,因此 事务2 就只能阻塞等待 事务1 释放锁.  事务1 想要释放全局锁得等到第二阶段结束,但是现在 事务2 又持有 DB 锁,事务1 也拿不到 DB 锁,因此事务就互相等待,直到 事务2 等待超时,就会进行回滚并释放 DB 锁.

    这样也就实现了 事务1 在执行第一阶段和第二阶段的时候,任何人都不能来执行,起到了很好的 隔离效果.

    问题一:有人可能就会说,那这不跟 XA  模式一样了?也锁定资源,这样别人也无法访问,效率不久下降了么?

    这里实际上是有一个 锁粒度 上的差别.  XA 模式中是数据库锁不释放,意味着任何人都不能访问你这数据库的数据,而 AT 中的全局锁只是不能任何人操作这个表中的某一行数据,也就是说,同样是 account 表,你可以修改这张表其他所有不是余额的字段.  因此效率上还是高很多的.

    问题二:有人可能就会说,隔离不彻底啊,可能有一种极端情况,就是在修改 money 的过程中,有一个其他 非Seata 管理的事务 也来修改 money 字段,这时候人家又不用获取全局锁,也可能出现脏写的问题

    确实有可能,但是可能性非常低,有以下两个原因:

    1. 全局事务大多数情况下都是成功的,比如我这边转账,大多数情况下我不能明知道钱不够还去转超额的钱吧,那么第二阶段几乎就不会回滚. 
    2. 分布式事务耗时本来就长,或者说并发本来就比较低,像这种见缝插针的事务就更少见了.

    但即使概论低,Seata 也还是考虑这种情况的. Seata 在管理的事务的时候,是保存了两个快照的,第一个是更新前的快照(money = 100),另一个是更新后的快照(money = 90). 在 Seata 管理的 事务1 执行到第二阶段的时候,他就会去对比数据库中的字段值 和 after-image(更新后的快照)是否一致,如果不一致,就知道其中有人动了手脚,此时他可能就会发一个短信、邮件、电话告诉你,需要人工介入了.

    1.2.4、AT 模式的优缺点

    优点:

    高性能:第一阶段完成后直接提交,释放资源比较早,数据库的锁定时间短.

    写隔离:利用全局锁机制,不仅实现了隔离,性能也依旧维持.

    使用简单,无代码侵入:Seata 框架自动完成回滚和提交.

    缺点:

    软状态:由于第一阶段执行业务 sql 可能有人成功有人失败,并且已经提交,这种情况下,并没有达成一致,只有第二阶段快照恢复才最终一致.

    影响性能:虽然相比 XA模式,性能要好很多,但是框架的快照功能多多少少都会影响一点性能.

    1.2.5、实现 AT 模式

    a)建表

    AT 模式下会引入全局锁,因此需要 lock_table 表来记录(在 TC 服务关联的数据库中创建).

    1. DROP TABLE IF EXISTS `lock_table`;
    2. CREATE TABLE `lock_table` (
    3. `row_key` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
    4. `xid` varchar(96) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
    5. `transaction_id` bigint(20) NULL DEFAULT NULL,
    6. `branch_id` bigint(20) NOT NULL,
    7. `resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
    8. `table_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
    9. `pk` varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
    10. `gmt_create` datetime NULL DEFAULT NULL,
    11. `gmt_modified` datetime NULL DEFAULT NULL,
    12. PRIMARY KEY (`row_key`) USING BTREE,
    13. INDEX `idx_branch_id`(`branch_id`) USING BTREE
    14. ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

    AT 模式还需要保存快照,因此需要 undo_log 表来记录(在 微服务 关联的数据库中导入).

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

    b)修改 application.yml 文件

    将事务模式修改为 AT 模式即可.

    1. seata:
    2. data-source-proxy-mode: AT # 开启数据源代理的AT模式

    c)重启服务并测试

     以下分别为用户余额和库存数量.

    1. 这里使用 Postman 发送以下请求(减少 9 个库存数量,用户余额减少 200)

    2. 随后可以看到订单微服务返回 500 错误.

    3. 扣款服务中进行扣款业务,最后回滚

    4. 检查数据库数据回滚成功~

     

  • 相关阅读:
    ASP.NET数据库连接(运行失败,不掌握)
    uniapp app tabbar 页面默认隐藏
    异常与错误处理高级用法
    Ubuntu20.04安装ROS-noetic
    热门好用的API分享,含免费次数
    1分钟掌握 Python 函数参数
    LeetCode 热题 HOT 100 第八十天 494. 目标和 中等题 用python3求解
    项目开展CICD的实践探路
    爆火的正规号卡推广分销 流量卡分销代理平台
    Docker--harbor私有仓库部署与管理
  • 原文地址:https://blog.csdn.net/CYK_byte/article/details/133549637