目录
背景
由于业务发展,在Place Order时接入了Promotion模块,要进行核销Coupon的动作,由于动作在不同的服务上,同时也要保证事务一致性,@Transactional注解已经不能满足需求,所以考虑引入Seata
基础概念
TC (Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM (Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
tx-service-group - 事务分组
事务分组是seata的资源逻辑概念。即可以按微服务的需要对事务进行逻辑上分组,每组取一个名字
在客户端(微服务)配置。
在Spring的配置参数形式如下: seata.tx-service-group=mytest-tx-group 或者用spring.cloud.alibaba.seata.tx-service-group配置
如以上不指定,则逻辑事务分组名默认规则为:spring.application.name值+"-seata-service-group"
vgroup-mapping - 服务端节点seata-server组成的集群cluster名
需要在客户端(微服务)中,对每个逻辑事务分组指定映射使用的vgroup-mapping(即服务端的cluster)。
通过此映射关系调整,即可切换到不同服务端cluster(异地多机房容灾,cluster="shanghai",cluster="suzhou"...)。
在客户端(微服务)配置。
指定逻辑事务分组映射到vgroup-mapping(vgroup-mapping的值需要与seata-server中registry.conf中的cluster保持一致)。 在Spring的配置参数形式如下: seata.service.vgroup-mapping.default-tx-group=default
其中:default-tx-group为事务分组名 等号的右侧 default 与seata-server的cluster名相同
@GlobalTransactional
注解选项:
timeoutMills:超时时间
name:事务的名字
rollbackFor:指定函数抛出哪些异类class会进行全局回滚(不指定默认为RuntimeException)。
noRollbackFor:指定函数抛出哪些异类class不会进行全局回滚。
Client环境
<spring.boot.version>2.6.4</spring.boot.version>
<spring.cloud.version>2021.0.1</spring.cloud.version>
<spring.cloud.alibaba.version>2021.1</spring.cloud.alibaba.version>
<nacos.version>2.0.3</nacos.version>
<seata.version>1.4.2</seata.version>
<dubbo.version>3.0.12</dubbo.version>
AT模式
引用Seata文档描述
- 在一阶段,Seata 会拦截“业务 SQL”,首先解析 SQL 语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,然后执行“业务 SQL”更新业务数据,在业务数据更新之后,再将其保存成“after image”,最后生成行锁。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
- 两阶段提交
二阶段如果是提交的话,因为“业务 SQL”在一阶段已经提交至数据库, 所以 Seata 框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
- 两阶段回滚
二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。
AT 模式的一阶段、二阶段提交和回滚均由 Seata 框架自动生成,用户只需编写“业务 SQL”,便能轻松接入分布式事务,AT 模式是一种对业务无任何侵入的分布式事务解决方案。
业务场景:扣减库存
-
一阶段
- 解析SQL:可认为 RM 封装了 DataSource,这里会得到SQL类型(UPDATE),表(goods),条件(where gid=100)等信息
- 生成前镜像:比如SQL是 update * where,那记录时把 UPDATE 改成 SELECT 就得到了前镜像的结果集(相当于备份数据)
- 执行业务SQL
- 生成后镜像:同样,再 SELECT 一下记录起来,此时count=9(也是在备份数据,因为分支事务SQL执行完是会本地提交的)
- 记录undolog:undolog 在 mysql 里面是用来做回滚的,这里实际就是将前后镜像组合,然后用 json 存到 UNDO_LOG 表中
- 提交前,向TC注册分支:这里会申请 goods 表中主键值等于 100 的记录的全局锁
- 提交本地事务:业务数据的更新和前面生成的 undolog 一并提交
- 将本地事务提交的结果上报给 TC
-
二阶段(提交)
- TM 向 TC 发起全局提交
- TC 向 RM 发起分支提交(传递XID)
- RM 将请求放入一个异步任务的队列中,并马上返回提交成功的结果给 TC
- 异步任务阶段的分支提交请求将异步和批量地删除相应 undolog
-
二阶段(回滚)
- 收到 TC 的分支回滚请求,开启一个本地事务
- 通过 XID 和 Branch ID 找到相应的 undolog
- 校验脏写:后镜像与当前数据库数据比较,如有不同则说明已被当前全局事务外的动作做了修改,此时需根据配置策略来处理
- 还原数据:根据前后镜像,生成逆向SQL并“回滚”(相当于重新写入一次)
- 删除undolog
- 提交本地事务,并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC
注意:分支事务的回滚由TC控制,所以RM一定要把异常抛出,如果自己处理了,则无法回滚
问题踩坑
日期序列化问题 (Seata 1.4.2)
问题描述:当Seata版本为1.4.x时,如果业务表中有datetime
类型,就会出现如下错误
com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Type id handling not implemented for type java.lang.Object (by serializer of type com.fasterxml.jackson.databind.ser.impl.UnsupportedTypeSerializer)
(through reference chain: io.seata.rm.datasource.undo.BranchUndoLog["sqlUndoLogs"]
->java.util.ArrayList[0]
->io.seata.rm.datasource.undo.SQLUndoLog["beforeImage"]
->io.seata.rm.datasource.sql.struct.TableRecords["rows"]->java.util.ArrayList[0]
->io.seata.rm.datasource.sql.struct.Row["fields"]->java.util.ArrayList[2]
->io.seata.rm.datasource.sql.struct.Field["value"])
解决方案:
1. 将Seata升级至1.5.x版本,即可解决该问题,但要注意项目中使用的dubbo版本,否则很有可能出现下述Dubbo版问题
2. 修改undo_log默认序列化方式,修改为kryo
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>4.0.2</version>
</dependency>
<dependency>
<groupId>de.javakaffee</groupId>
<artifactId>kryo-serializers</artifactId>
<version>0.42</version>
</dependency>
Dubbo版本问题(Seata 1.5.2 | Dubbo 3.0.x 启动报错)
解决方案:
1. Seata降版本至1.4.2 | Dubbo 3.0.x ☑️
2. Dubbo降版本至2.x | Seata 1.5.2
JDK版本问题(Seata 1.4.2 | JDK 17 启动报错)
解决方案:切换成11可以解决