Seata 的事物注解是 GlobalTransactional , 可以通过调用标记这个注解的方法,开启一个分布式事务。
Seata 的声明方式与Spring事物声明方式类似,相比Spring定义的事物声明,加入了事物名称和事物超时时间,传播机制取消了嵌套类型(Propagation.NESTED)。
首先看全局事物的声明和调用方式
全局事物注解
public @interface GlobalTransactional {
/** 事物名称, 必须唯一 */
String name() default "";
// 默认 60 秒超时
int timeoutMills() default TransactionInfo.DEFAULT_TIME_OUT;
// 传播机制
Propagation propagation() default Propagation.REQUIRED;
// 其他和 spring 事物一样的属性就省略了
}
调用方式
@GlobalTransactional(timeoutMills = 300000, name = "seata-example")
public ObjectResponse handleBusiness(BusinessDTO businessDTO) {
//1、扣减库存
ObjectResponse storageResponse =
storageDubboService.decreaseStorage(commodityDTO);
// 2. 生成订单
ObjectResponse<OrderDTO> response =
orderDubboService.createOrder(orderDTO);
// 省略处理响应的内容
}
Seata 事物切面
全局事物切面的作用
事物切面在事物开始前向事务协调者(TC)发送全局事物开启请求, 获得XID。 把XID通过微服务调用其他的服务时透传过去。其他微服务通过XID处理分支事物。
微服务调用完成时, 由事务管理器(TM)向事物协调者(TC)发起全局事物提交请求, TC 会向每个注册了分支事物的微服务发起分支事物提交请求(AT模式就是删除回滚日志undo_log).
微服务调用失败时, 由事务管理器(TM)向事物协调者(TC)发起全局事物回滚请求, TC 会向每个注册了分支事物的微服务发起分支事物回滚请求(AT模式就是查询回滚日志, 对比当前数据和后置镜像是否一致的方式避免脏写, 如果没有脏写,就用前置镜像构建SQL语句,然后执行SQL语句回滚数据).
全局事物切面的实现方式
切面的扫描由 GlobalTransactionScanner 定义, GlobalTransactionScanner 扩展了 Spring 的 BeanPostProcessor 接口.
当GlobalTransactionScanner创建完成后,会调用初始化方法, 初始化资源管理器, 初始化事物管理器, 注册容器关闭的回调函数。
由于实现了 BeanPostProcessor, 所以对于所有Bean的创建,在bean初始化的时候都可以进行增强。
GlobalTransactionScanner 实现了很多接口,这里关注其中几个重要的接口
- BeanPostProcessor , Spring 对Bean增强的处理
- DisposableBean , 容器关闭时, 执行所有的回调函数
- InitializingBean 初始化接口, 初始化资源管理器, 事务管理器, 注册关闭容器的回调函数
- Ordered 排序, 默认值为 1024
- AopInfrastructureBean, 标记当前Bean是基础设施, 基础设施不会被增强
当 GlobalTransactionScanner 扫描当前类的所有方法声明,和所有接口的方法声明 找到有 @GlobalTransactional 或者 @GlobalLock 就对当前bean进行增强, 加入指定的增强器: GlobalTransactionalInterceptor
切面顺序
AOP 排序是一个值得讨论的问题
在这里举一个例子, 对于一个方法同时有三种注解, @Async,@GlobalTransactional,@Transactional 会发生什么情况?
这里要从BeanPostProcessor的角度来描述
这三个注解由不同的BeanPostProcessor处理
- 处理 @Async 的是 AsyncAnnotationBeanPostProcessor,顺序为 Ordered.LOWEST_PRECEDENCE(最低优先级)
- 处理 @Transactional的是标准AOP , 顺序为 Ordered.HIGHEST_PRECEDENCE(最高优先级)
- 处理 @GlobalTransactional 的是 GlobalTransactionScanner ,顺序为 1024
对于要增强的方法的增强顺序就是: 标准AOP增强 => 全局事物增强器 => 异步增强器。
同时对方法增强,那么方法有调用链如下:
名称 | 顺序 | 插入方式 | 第一次 | 第二次 | 第三次 |
---|---|---|---|---|---|
标准AOP | 2 | advised.add(interceptor) | 处理增强 | 未被增强 | 未被增强 |
全局事物增强器 | 1 | advised.add(0, interceptor) | 已被增强 | 处理增强 | 未被增强 |
异步增强器 | 0 | advised.add(0, interceptor) | 已被增强 | 已被增强 | 处理增强 |
最先由标准AOP增强Bean,bean有一个调用链, 然后由全局事物增强器增强曾经被增强的Bean, 这个添加一定会把调用链插入到头部, 最后由异步增强器增强。
所以Seata定义的BeanPostProcessor 不能是 Ordered.HIGHEST_PRECEDENCE 也不能是 Ordered.LOWEST_PRECEDENCE, 避免顺序混淆
方法拦截器
方法拦截器会判断当前方法是加了全局事务还是全局锁, 选择其中一个执行
// 获取全局事物注解
final GlobalTransactional globalTransactionalAnnotation =
getAnnotation(method, targetClass, GlobalTransactional.class);
// 获取全局锁注解
final GlobalLock globalLockAnnotation = getAnnotation(method,
targetClass, GlobalLock.class);
// 判断是否配置了禁用分布式事务,或者降级检查
boolean localDisable = disable
|| (degradeCheck && degradeNum >= degradeCheckAllowTimes);
if (!localDisable) {
// 如果有全局事物
if (globalTransactionalAnnotation != null) {
return handleGlobalTransaction(methodInvocation,
globalTransactionalAnnotation);
}
// 如果有全局锁
else if (globalLockAnnotation != null) {
return handleGlobalLock(methodInvocation, globalLockAnnotation);
}
}
全局事物拦截器:
在经过全局事物的方法拦截器时, 首先判断是否禁用全局事物, 然后判断是否开启降级检查。如果开启了降级检查,seata会开启一个调度,默认每2秒执行一次,默认情况下达到10次失败,就会进行服务降级, 不会开启分布式事务。
- 处理事务的传播机制
- 处理全局锁的挂起
- 向协调者发送全局事物开始请求,或的XID,绑定到当前线程, 执行RPC调用时,会将XID透传
- 执行业务逻辑
- 如果成功就向协调者发起事物提交请求, 失败就向协调者发起全局事物回滚请求
- 处理全局锁的释放与恢复, 处理全局事物的恢复
全局锁拦截器:
全局锁在Seata中用的范围很小, 在AT模式中,业务操作执行完成,并且回滚日志也插入完成,在提交之前需要获取全局锁。(只有加入了@GlobalLock才在提交之前获取全局锁, 不加的话,数据库连接执行提交之前是不需要全局锁的)
- 修改全局锁配置
- 执行业务方法, (数据库连接提交的时候, 分布式事务提交失败后重试的时候,会用到全局锁)
- 恢复全局锁配置