使用 seata 来处理分布式事务

1,703 阅读6分钟

前言

不同的版本可能导致一些 BUG,为了能快速运行项目,建议如下版本保持一致

配置 nacos

github.com/alibaba/nac…

无需配置其它参数,点击 bin 目录下 startup.cmd 启动就行

配置 seata

github.com/seata/seata…

进入到 conf 目录下,首先修改配置中心 registry.conf,目前使用的是 nacos

然后修改 file.conf,这一步是将 seata 的数据保存在 MySQL 数据库中

然后创建数据库 seata,后执行当前文件下的 db_store.sql 和 db_undo_log.sql,注意 db_undo_log.sql 会报错,把 drop table 去掉就是

seata 的几种工作模式

以下几种模式中,除了 XA 模式外都是补偿模式,其中 AT 模式自动帮我们生成了回滚代码,当业务发生异常的时候通过执行回滚语句进行回滚

这里先简单介绍以下几种工作模式,后续会详细来分析下 AT 模式的工作原理

**1. XA 模式 **

XA 它是一种强一致性模式,采用 2PC 的方式来保证所有资源同时提交或者回滚,它会锁定资源直到整个全局事务发出 commit/rollback 为止,无疑大幅度的降低了应用的吞吐量,同时如果全局事务“失联”各个处于 prepare 状态的分支事务就会一直被锁定,最终可能产生死锁

2. AT 模式

针对于 XA 模式可能存在的长时间锁定资源问题演变出了 Seata AT 模式

  • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。

  • 二阶段:

    • 提交异步化,非常快速地完成。
    • 回滚通过一阶段的回滚日志进行反向补偿。

在 AT 模式下我们无需编写回滚代码,通过注解 @GlobalTransactional 即可完成

3. TCC 模式

TCC 模式和 AT 模式的主要区别就是,我们需要编写自定义的 prepare、commit、rollback 逻辑

4. Saga 模式

Saga 模式是 SEATA 提供的长事务解决方案,在 Saga 模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现

AT 模式示例

git clone github.com/seata/seata…

启动顺序如下,注意修改他们的 application.properties 中的 dubbo.registry.address=nacos://127.0.0.1:8848

  1. AccountExampleApplication
  2. StorageExampleApplication
  3. OrderExampleApplication
  4. BusinessExampleApplication

在 BusinessExampleApplication 中

可以看到这里调动的是 2 个远程方法,然后通过调用 /buy 方法,通过 flag 来测试全局事务是否生效

最后发现 @GlobalTransactional 满足了我们对应事务的需求,flag 为 true 订单生成成功,为 false 数据回滚成功

Seata AT 模式原理

  1. tx1 开启本地事务,拿到本地锁,将 m - 100 = 900
  2. 然后在提交本地事务之前需要先获取全局锁
  3. 获取全局锁成功,提交本地事务
  4. tx2 获取本地锁,将 m -100 = 800
  5. tx2 要提交本地事务之前需要先获取全局锁,由于全局锁被 tx1 持有,所以此处不间断的去重复获取全局锁
  6. tx1 后续逻辑执行完毕后释放全局锁
  7. tx2 获取全局锁成功提交本地事务

Seata AT 模式下全局事务隔离级别是 Read Uncommitted 所以 tx1 的全局事务没有提交,tx2 也可能看到它做出的修改

针对上面的内容可能会有如下 2 个问题

  • 如果说 tx1/tx2 获取全局事务失败怎么办
  • 如果说 tx1 的后续操作导致全局事务需要进行回滚,那么 tx2 得到的 m = 900 就是一个错误的数据该如何处理
  1. tx1 获取本地锁,执行 m - 100 = 900
  2. tx1 获取全局锁
  3. tx1 获取全局锁成功后提交本地事务
  4. tx2 获取本地锁
  5. tx2 执行 m - 100 = 800
  6. tx2 获取全局锁,由于锁被 tx1 持有所以它会不间断重复获取
  7. tx1 继续后续分支事务,里面发生异常导致全局事务需要进行回滚
  8. tx1 会重新获取本地锁便于进行数据回滚,但是此时锁被 tx2 持有,当前的本地锁锁住的是主键 id = 1 这一行数据
  9. tx1 获取本地锁失败,不断的重试,直到 tx2 获取全局锁超时
  10. tx2 获取全局锁超时,回滚本地事务 m 恢复为 900,释放本地锁
  11. tx1 获取本地锁成功,检查 undo log 数据发现 m 值为 900 表示可以恢复,执行回滚逻辑 m 值恢复到 1000

在这种机制的保护下,就不会存在脏写的问题,但是会存在脏读的问题,比如中间时刻有人查询数据的话,会得到 m 为 900 的错误数据

AT 模式下示例代码执行原理浅析

看示例代码,我们发现通过一个 @GlobalTransactional(timeoutMills = 300000, name = "dubbo-gts-seata-example") 注解就完成了,里面调用的远程方法同时远程方法中也无需添加 @Transaction 注解,是如何做到的呢,看下图

  • TM:Transaction Manager,事务管理器负责开启全局事务和提交或者回滚
  • TC:Transaction Coordinator,事务协调者,协调处理分支事务
  • RM:Resource Manager,资源管理器
  • undo log:Seata 自动生成的回滚日志,记录了变更数据前后,数据的状态
  1. 当我们使用 @GlobalTransactional 开启全局事务,TM 向 TC 发起 BEGIN 请求开启全局事务,通过 RootContext.getXID() 就能获取到全局事务 XID
  2. 当发起远程调用逻辑的时候通过代理调用 storageDubboService.decreaseStorage
    • RM 向 TC 注册分支事务
    • XID 绑定分支事务,通过 RootContext.getXID() 就能拿到当前分支事务的 XID 值发现保持一致
  3. 在执行 update SQL 语句的时候会生成 undo log,undo log 中保存了修改之前的数据和修改过后的数据
  4. 假设 flag = false,最终回滚执行 undo log 中的数据
  5. RM 收到 TC 的回滚请求,通过 XID 和 Branch ID (分支事务 ID) 在 undo log 找到对应的数据,将 undo log 中后镜像数据与当前数据做对比,如果一样就执行前镜像数据进行恢复
  6. 假设 flag = true,RM 最终会收到提交请求,会将请求放入一个异步队列中,然后立刻成功,最终队列里面的任务将异步或者批量的删除 undo log

参考: