目录
一、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)重启服务并测试
XA 模式是 X/Open 组织定义的一个分布式事务处理的标准,可以认为是分布式事务领域最早的标准了,所以几乎所有主流的数据库(mysql、oracle......)都是实现了这种标准. 换言之,这些数据库内部已经能够基于 XA 模式实现分布式事务了.
这种模式把分布式定义成了以下两个阶段
这一阶段也叫做准备阶段,事务协调者 会向 资源管理器 发起一个准备的请求,也就是告诉他:“你可以去执行业务 sql 了啊,但是执行完了以后不可以提交!”. 随后,资源管理器 就执行数据库业务,然后将执行结果告诉 事务协调者(执行成功了,就是就绪状态;失败了,就是 fail 状态),这样 事务协调者 就可以根据结果判断下一个阶段要干什么.
事务协调者收到 RM 的反馈之后,就会进行判断. 如果都执行成功了,就通知这些 RM:“你们可以提交了啊”,然后 RM 收到请求,就把事务提交了,整个事务也就结束了;如果有任意一个服务执行失败了,那么事务协调者就会通知这些RM,让他们全部回滚.
可以看出 XA 模式是一种强一致性的事务,软可用性,因为事务没有提交的时候,拿不到数据库锁,是不能进行其他操作的.
Seata 的 XA 模式和上述讲的 XA 模式大体相似,只是多了一个 TM 的概念. 如下图
Seata 实际上就是多了一个 TM 的概念, 也就是事务管理者. TM 一上来就是要去注册全局事务,作为分布式事务的入口,接下来就会去调用各个分支事务. 每个分支事务中又有一个 RM,RM 就回去 TC 里面注册分支事务,然后执行业务 sql,但是执行完后不能提交,只是去报告一下事务的状态. 到此,第一阶段结束.
TM 这里的入口方法执行完了以后,就要去告诉 TC 说:“我这边执行完了,接下来就看你那边的情况了~”. 随后 TC 就会去检查各个分支事务报告的状态,然后向 RM 发送一个信号——如果第一阶段都执行成功了,那么就提交,反之,则进行回滚. RM 收到信号之后就可以进行 提交/回滚 事务了.
优点:
1. 支持强一致:执行完业务 sql 以后,不进行提交事务,而是等到 TC 协调完后,在进行 提交/回滚.
2. 实现起来简单:因为数据库本身就支持 XA 模式,Seata 只是在数据库的 XA 模式上做了一层简单的封装. 如果只看核心部分,就没什么差别了.
缺点:
1. 弱可用性,性能差:第一阶段不提交,等待第二阶段才提交,这个过程中会占用数据库锁(相当于对系统资源的一种浪费). 一旦分支事务特别多的情况下,业务耗时就会很久.
2. 依赖数据库底层实现:虽然用起来简单了,但是如果我用的不是 mysql 这种关系型数据库,而是 redis 这种非关系型数据库就不行了.
开启 seata 的 XA 模式
seata:
data-source-proxy-mode: XA # 开启数据源代理的XA模式
当前案例的全局事务入口是 订单服务中的 创建订单的 create 方法(案例架构如下图).
这里只需要在 create 方法上添加 @GlobalTransactional 注解即可
@Override
@GlobalTransactional
public Long create(Order order) {
// 创建订单
orderMapper.insert(order);
try {
// 扣用户余额
accountClient.deduct(order.getUserId(), order.getMoney());
// 扣库存
storageClient.deduct(order.getCommodityCode(), order.getCount());
} catch (FeignException e) {
log.error("下单失败,原因:{}", e.contentUTF8(), e);
throw new RuntimeException(e.contentUTF8(), e);
}
return order.getId();
}
Ps:这里不要忘了先启动 Nacos 和 TC 服务.
以下分别为用户余额和库存数量.
1. 这里使用 Postman 发送以下请求(减少 9 个库存数量,用户余额减少 200)
http://localhost:8082/order?userId=user202103032042012&commodityCode=100202003032041&count=9&money=200
2. 随后可以看到订单微服务返回 500 错误.
在扣款微服务中,先进行了扣款业务,但是随后 rm 又执行了回滚
在库存微服务中,可以看到扣减失败.
最后观察数据库中数据,可以看到事务已经回滚如初.
AT 模模式也是分阶段性提交的事务模型. 他的出现正式为了解决 XA 模式中资源锁定周期过长的一个缺陷.
AT 模式也分为两个阶段
TM 去开启全局事务,作为分布式事务的入口,接着又会调用每一个分支事务 ,然后每个分支事务里的 rm 都会去注册分支事务,并执行本地的业务 sql,然后直接提交(这里就和 XA 不一样了). 因此 AT 模式就不需要像 XA 那样锁定资源,性能上要优于 XA 模式.
这里直接提交,万一有人失败了呢,没办法回滚不就导致状态不一致了吗?
实际上 AT 模式在执行 sql 之前,rm 会给当前数据生成一个快照,这个快照的名字叫 “undo log”,这就像是 redis 中 RDB 持久化时也会生成快照,那么即使服务重启,也可以根据快照恢复数据. 那么这样就即使有分支事务执行 sql 的时候失败了,也可以根据快照恢复如初,大胆提交就对了.
TM 看到业务结束了,就会去通知 TC,那么 TC 就会判断是提交还是回滚. 如果分支事务的状态都是成功的,那就可以把第一阶段准备的快照给删了(删快照这个动作是异步的,因为第一阶段都成功了,也提交了,后面的事情就可以用一个线程独立去做,提高了效率). 如果第一阶段有人失败了,就要基于 undo log 恢复数据,恢复以后这个 log 也就没用了,最后也会删除.
1. XA模式在第一阶段的时候不会进行事务的提交,而 AT 模式执行完业务 sql 之后会立即提交事务,不会锁定资源,因此性能会好一些.
2. XA 依赖于数据库的机制来做回滚,而 AT 模式因为已经提交了,就不能回滚,他是通过给自己生成快照的方式来实现数据的恢复.
3. XA 模式是强一致,而 AT 是最终一致,因为 AT 在业务 sql 执行之后直接提交了,有人失败,有人成功,那么这个时候状态肯定是不一致的,也就是一个软状态,只有在第二阶段,基于快照恢复了数据,才能到达一个最终一致的效果.
AT 模式在第一阶段执行完业务 sql 以后会直接提交,那么资源锁定的周期就比较短,效率高,但也正因为他提前释放了锁,就导致在高并发的场景下,会出现安全问题~
例如我这里有一个用户余额表,里面记录了 money 字段,表示余额.
到了第二阶段,假如说 事务1 要进行回滚(通过快照恢复数据),但是 事务1 的快照数据 money = 100!这个时候就出问题了,恢复了以后相当于 事务2 这哥们啥也没做. 这就是所谓的脏写问题.
上述过程出现的问题,归根结底还是隔离性的原因,如果第一阶段和第二阶段整体都是一个锁定状态,别人根本无法插入进来,因此 AT 模式就引入了一个东西叫 “全局锁”.
Ps:全局锁由 TC 来控制,用来记录当前哪个事务在操作哪种表的哪一行数据. 这张表了主要有 事务id、表名字、pk主键(记录这张表的哪一行数据).
这样也就实现了 事务1 在执行第一阶段和第二阶段的时候,任何人都不能来执行,起到了很好的 隔离效果.
问题一:有人可能就会说,那这不跟 XA 模式一样了?也锁定资源,这样别人也无法访问,效率不久下降了么?
这里实际上是有一个 锁粒度 上的差别. XA 模式中是数据库锁不释放,意味着任何人都不能访问你这数据库的数据,而 AT 中的全局锁只是不能任何人操作这个表中的某一行数据,也就是说,同样是 account 表,你可以修改这张表其他所有不是余额的字段. 因此效率上还是高很多的.
问题二:有人可能就会说,隔离不彻底啊,可能有一种极端情况,就是在修改 money 的过程中,有一个其他 非Seata 管理的事务 也来修改 money 字段,这时候人家又不用获取全局锁,也可能出现脏写的问题
确实有可能,但是可能性非常低,有以下两个原因:
但即使概论低,Seata 也还是考虑这种情况的. Seata 在管理的事务的时候,是保存了两个快照的,第一个是更新前的快照(money = 100),另一个是更新后的快照(money = 90). 在 Seata 管理的 事务1 执行到第二阶段的时候,他就会去对比数据库中的字段值 和 after-image(更新后的快照)是否一致,如果不一致,就知道其中有人动了手脚,此时他可能就会发一个短信、邮件、电话告诉你,需要人工介入了.
优点:
高性能:第一阶段完成后直接提交,释放资源比较早,数据库的锁定时间短.
写隔离:利用全局锁机制,不仅实现了隔离,性能也依旧维持.
使用简单,无代码侵入:Seata 框架自动完成回滚和提交.
缺点:
软状态:由于第一阶段执行业务 sql 可能有人成功有人失败,并且已经提交,这种情况下,并没有达成一致,只有第二阶段快照恢复才最终一致.
影响性能:虽然相比 XA模式,性能要好很多,但是框架的快照功能多多少少都会影响一点性能.
AT 模式下会引入全局锁,因此需要 lock_table 表来记录(在 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;
AT 模式还需要保存快照,因此需要 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;
将事务模式修改为 AT 模式即可.
seata:
data-source-proxy-mode: AT # 开启数据源代理的AT模式
以下分别为用户余额和库存数量.
1. 这里使用 Postman 发送以下请求(减少 9 个库存数量,用户余额减少 200)
2. 随后可以看到订单微服务返回 500 错误.
3. 扣款服务中进行扣款业务,最后回滚
4. 检查数据库数据回滚成功~