1:什么是Seata
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
Seata有三个重要的术语
-
TC:维护全局和分支事务的状态,驱动全局事务提交或回滚。,也就是我们的Seata-server服务
-
TM:定义全局事务的范围:开始全局事务、提交或回滚全局事务。通常来说就是我们添加了@GloableTrancation注解的那个服务
-
RM:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
2:AT模式整体机制
整体机制是基于两阶段提交协议的演变:
-
一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
-
二阶段:
- 提交异步化,非常快速地完成。
- 回滚通过一阶段的回滚日志进行反向补偿。
3:AT模式整体流程
-
开启全局事务
- 首先TM会向TC注册一个全局事务,注册成功之后会返回一个全局的事务ID,也就是XID,并且会生成一个全局的事务会话GlobalSession,并且会在seata数据库的global_table中保存这个全局事务的相关信息
-
执行自己的业务逻辑
-
事务开启之后RM就执行自己的业务逻辑,执行完自己的业务之后就需要注册自己的分支事务,并生成一个分支事务会话BranchSession,该会话会与全局事务会话绑定在一起,其实就是全局会话GlobalSession里面有个List的变量,将这个BranchSession放到这个List中,也会将这个分支会话保存到branch_table中
-
在生成分支会话之前还要判断这个全局事务的状体是否属于Begin的状态
-
生成分支会话之后,就要获取该记录的全局锁了,举个例子,比如现在操作的是订单表,那么这时候订单ID=1的这条记录只能是一个事务去操作,其它事务是不能去操作的,只有当这个事务释放了这把锁,其它事务才能去操作这条记录,生成的全局锁是在seata的lock_table表中,操作的是那个数据,那张表,以及主键是多少都在row_key中保存起来了
-
注册成功之后就会返回一个BranchId,RM收到这个BranchId之后,就会在自己本地数据库的undo_log表中保存一条回滚记录,用于后续的事务回滚,保存完之后本地事务就要提交,注意:这里是本地事务提交不是全局事务
-
lock_table:
-
branch_table
-
-
全局事务提交
-
通过XID全局事务ID去找到这个事务的全局会话,找到该全局事务的所有的分支会话,期间会判断该全局事务的状态
-
拿到所有的分支会话之后,通知各个的分支会话去删除本地的undo_log对应的记录
-
-
全局事务回滚
-
因为在执行本地业务的时候,本地事务就已经提交了,如果这时候回滚该怎么办?还记得都在本地保存了一条undo_log的记录嘛,该记录有个很重要的字段 rollback_info,这个字段记录了记录操作前后的二个镜像,也就是beforeImage和afterImage。本地事务就可以通过回放将数据重新修改成事务之前的状态
修改操作:记录了记录操作前的状态和操作后的状态
-
新增操作:记录了记录操作前的状态和操作后的状态,但是操作前的状态只是为空,因为是插入,之前是没有这条记录的
4:关键步骤源码
在底层只要有 @GlobalTransactional注解的方法都会经过GlobalTransactionalInterceptor这个拦截器的 invoke() 处理
TransactionalTemplate.class
public Object execute(TransactionalExecutor business) throws Throwable {
// 1 get transactionInfo
TransactionInfo txInfo = business.getTransactionInfo();
if (txInfo == null) {
throw new ShouldNeverHappenException("transactionInfo does not exist");
}
// 1.1 get or create a transaction
GlobalTransaction tx = GlobalTransactionContext.getCurrentOrCreate();
// 1.2 Handle the Transaction propatation and the branchType
Propagation propagation = txInfo.getPropagation();
SuspendedResourcesHolder suspendedResourcesHolder = null;
try {
switch (propagation) {
case NOT_SUPPORTED:
suspendedResourcesHolder = tx.suspend(true);
return business.execute();
case REQUIRES_NEW:
suspendedResourcesHolder = tx.suspend(true);
break;
case SUPPORTS:
if (!existingTransaction()) {
return business.execute();
}
break;
case REQUIRED:
break;
case NEVER:
if (existingTransaction()) {
throw new TransactionException(
String.format("Existing transaction found for transaction marked with propagation 'never',xid = %s"
,RootContext.getXID()));
} else {
return business.execute();
}
case MANDATORY:
if (!existingTransaction()) {
throw new TransactionException("No existing transaction found for transaction marked with propagation 'mandatory'");
}
break;
default:
throw new TransactionException("Not Supported Propagation:" + propagation);
}
try {
// 2. 开启全局事务
beginTransaction(txInfo, tx);
Object rs = null;
try {
// 执行自己的业务逻辑
rs = business.execute();
} catch (Throwable ex) {
// 3.全局事务回滚
completeTransactionAfterThrowing(txInfo, tx, ex);
throw ex;
}
// 4. 全局事务提交
commitTransaction(tx);
return rs;
} finally {
//5. clear
triggerAfterCompletion();
cleanUp();
}
} finally {
tx.resume(suspendedResourcesHolder);
}
}
最后附上整体的源码流程图
5:结尾
在通过debug方式去调试Seata源码的时候会特别麻烦,因为Seata底层是基于Netty进行通信的,所以首先你对Netty得熟悉,其次,就是因为基于Netty通信,所以源码的跳跃很容易让人头晕,不知道该走哪一步
其次就是Seata底层初始化了很多的定时线程池,当发现你有事务没有提交,或者没有回滚就会尝试去提交或者回滚之类的操作,此时可能你debug到后面突然断点又到之前的代码去了
事务都是有超时时间的,你debug时间太长根本就无法正常走完整个流程