在上文中,笔者通过下单扣减库存和扣款余额为业务场景,给大家展示了Seata的强大之处,一个简单的注解做到了真的无业务入侵,那么Seata底层是怎么实现分布式事务的呢?本文基于Seata AT带领大家了解Seata的工作原理。
1. 回顾AT模式
在前面的文章中,笔者已经给大家介绍了分布式事务的6中解决方案,其中很多模式都是两阶段提交的改进或者优化,Seata的AT模式也是如此。
AT模式将整个分布式事务分为三个角色:
- 事务的协调者(TC):协调全局事务的运行状态,负责驱动和协调全局事务的执行。
- 事务的管理这(TM):它是全局事务的发起者,比如下单服务中的订单服务就是TM,它用来决议全局事务的提交和回滚。
- 资源管理器(RM):用以管理每个分支事务的回滚和提交,并向TC注册分支事务
AT模式将整个分布式事务分成了两个步骤:
- 第一阶段:业务数据和回滚日志在同一本地事务中进行提交,释放本地所和所需资源
- 第二阶段:如果TC决议提交:通过将提交任务放入队列中,异步删除回滚日志 如果TC决议回滚:通过一阶段的回滚日志进行反向补偿,将业务数据恢复到初始状态
1.1 如何将分支事务统一关联起来
在下单业务场景中,创建定单、扣减余额、扣减库存这是由三个分支事务组成的分布式事务,理想情况下需要满足原子性,保证数据的的最终一致性。那么如何将分支事务统一管理起来呢?Seata通过一个全局事务Id即XID来标识一个分布式事务,每个分支事务都会分配一个branchId,这样通过XID和branchId即可将多个分支事务统一管理起来。
- XID:全局事务的唯一id
- branchId:每个分支事务的唯一id
1.2 如何进行提交和回滚
我们知道Innodb存储引擎支持事务的原理是通过undo log来实现的,Seata的回滚和提交也是通过undo log来实现的,这两种undolog不是同一种,注意区分。
- undo_log的表结构
| Field | Type | desc |
|---|---|---|
| branch_id | bigint PK | 分支事务id |
| xid | varchar(100) | 全局事务id |
| context | varchar(128) | 事务上下文 |
| rollback_info | longblob | 回滚信息 |
| log_status | tinyint | 状态 |
| log_created | datetime | 创建日期 |
| log_modified | datetime | 修改时间 |
rollback_info记录的就是业务数据的逆向过程,比如删除一条记录,里面记录的是对应的新增逻辑,如果新增一条记录,里面记录的就是对应的删除逻辑。
- 如果是提交操作,把回滚日志删除即可
- 如果是回滚操作,根据回滚日志将数据恢复到事务前即可,进行补偿
1.3 事务协调者如何协调分支事务
每个分支事务的提交回滚,事务协调者TC是如何感知的呢?Seata采用代理数据源的方式,业务逻辑通过JDBC接口访问数据库时,Seata通过拦截的方式对其进行拦截,进行一些非业务操作。每个分支事务在提交完本地事务时,都会向TC注册一个分支事务,在进入第二阶段时,再通过各个分支事务的执行状态决议全局事务的提交和回滚,通过回调接口进行回滚日志的删除或者补偿操作。
简单的说Seata通过代理数据源,生成每个事务数据的补偿逻辑,来协调整个全局事务。
1.4 如何保证事物的隔离性
假如全局事务A正在扣减库存,另外来了一个全局事务B也想扣减库存,那它们之间的隔离关系是如何解决的呢?Seata通过全局锁的方式来进行事务的隔离性。
- 全局锁是由修改的表和修改行对应的主键组成,分支事务在进行本地事务前,都会向TC进行分支事务注册,此时TC会校验全局锁是否被其它事务持有,如果被其它事务持有全局锁,那么当前事务需要循环等待。
- 在本地事务执行完第一阶段后,如果是决议提交,那么在第二阶段开始全局锁会释放,那么其它全局事务可以尝试获取全局锁,因此相比于传统的两阶段提交,AT模式提高了并发度,缩短了全局锁的持有范围。
1.5 提交阶段和回滚阶段失败了怎么办
- 假如所有的分支事务一阶段都提交成功,那么第二阶段事务协调者会向所有的参与者发送commit请求,参与者在收到commit请求,会将任务加入到一个内存队列中,并且会有个定时任务不断从队列中取出提交任务,通过XID和branchId找到对应的undo log记录,生成删除sql,删除对应的undo log记录。 如果此步骤失败,则会由另一个线程池去执行,不断重试直到成功。
- 假如某个分支事务本地提交失败,则会抛出异常,那么事务协调者TC感知到后,会向其他分支事务发送回滚请求。当分支事务收到回滚请求, 在本地进行补偿操作失败后,通过异步操作不断重试。
2. undo log浅析
undo log工作原理是怎么样的呢,这里以官方给出的例子结合笔者在上篇文章给出的例子进行介绍。
上篇指路:Spring Cloud Alibaba系列(四):Seata+Nacos+MyBatis Plus+Open Feign实现分布式事务(实践篇)
-- seata 框架必备undo_log表
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
-- 库存表
DROP TABLE IF EXISTS `storage_tbl`;
CREATE TABLE `storage_tbl` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`commodity_code` varchar(255) DEFAULT NULL,
`count` int(11) DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY (`commodity_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
在扣减库存的时候,会执行以下业务sql
update storage_tbl set count = count - 1 where commodity_code = 'product_001';
- 一阶段
-
通过解析业务sql,得到操作表为storage_tbl,操作类型为update,
where commodity_code = 'product_001' -
通过sql解析查询业务sql执行前的快照,并获取到对应的主键
id = 1
select * from storage_tbl where commodity_code = 'product_001';
- 执行业务sql,count值会被修改成8
- 通过主键查询,业务sql提交后的快照
select * from storage_tbl where id = 1 ;
- 插入回滚日志:把前后镜像数据以及业务 SQL 相关的信息组成一条回滚日志记录,插入到
UNDO_LOG表中。 - 提交前,向TC注册分支事务,获取
storage_tbl和id = 1组成的全局锁 - 本地事务提交前,将业务数据和undo log日志生成放在同一个事务中执行
- 将本地事务执行的结果上报给TC
- 二阶段提交
- 分支事务收到TC的提交请求,将请求放到一个任务队列中,立即返回成功响应给TC,并释放全局锁
- 后台有个定时任务,不断地从队列中取出任务,进行相应的undo log记录删除
- 二阶段回滚
- 分支事务收到TC的回滚请求,开启一个本地事务
- 通过XId和BranchId找到对应的undo log记录
- 通过业务sql执行前的镜像和业务sql生成补偿sql
- 提交回滚事务,并将执行结果上报给TC
3. GlobalTransactional注解
至此我们对Seata的工作流程有了大致的了解了,那么GlobalTransactional注解的工作流程是怎么样的呢。我们先简单列举相关的类,
-
GlobalTransactional : 被修饰的方法会被拦截
-
GlobalTransactionScanner:通过类名可知,扫描那些被GlobalTransactional修饰的bean,通过实现了InitializingBean, ApplicationContextAware, DisposableBean,便可获取Spring上下文。并且继承AbstractAutoProxyCreator来生成代理对象。
-
GlobalTransactionalInterceptor:通过AOP的方式,拦截需要全局事务管理的方法。
4. 优缺点比较
- AT模式相比于传统的XA两阶段提交,一方面是在第一阶段提交后就释放了资源和锁,因此将独占资源的范围缩小。另一方面,二阶段的提交和回滚都是异步执行的,因此性能会更好。
- AT模式相比于TCC模式,降低了对业务的侵入和代码的耦合,通过GlobalTransactional注解即可实现分布式事务管理,底层的细节都由Seata帮我们考虑和实现了。但是TCC需要业务将整个流程分成三个步骤,并且还需要实现相关接口,因此严重依赖于业务。