Seata简介
Seata是Alibaba开源的一个分布式事务组件,可以实现分布式环境下跨服务的事务一致性。
Seata三种角色
Seata包含三种角色:TM、RM、TC。
- TM(Transaction Manager):事务管理器。
- RM(Resource Manager):资源管理器。
- TC(Transaction Coordinator):事务协调者。
他们的作用如下图:
这么说好像还是不太明白,我画个图就清晰了。
我们以XA事务模式为例。
- 在XA事务一阶段,TM请求TC开启全局事务。
- TM请求第一个RM执行业务逻辑,如果TM和TC是同一个服务,那么这个请求其实就是个本地调用,如果不是的话,那么就是RPC远程调用。
- RM先执行SQL,然后请求TC注册分支事务,然后本地执行prepare操作,然后上报分支事务状态
- 第一个RM请求第二个RM,第二个RM也是和第一个RM一样来一套。
如果一切顺利,TM在二阶段就会进行全局事务提交。
TC接收到TM的全局事务提交请求后,会通知各RM进行分支事务提交。各RM接收到TC的提交分支事务通知后,会提交本地事务。
RM执行时不管那一步出问题,都会报错,然后TM就会catch到报错信息,在二阶段进行全局事务回滚。
TC接收到TM的全局事务回滚请求后,会通知各RM进行分支事务回滚。各RM接收到TC的回滚分支事务通知后,会回滚本地事务。
Seata两个注解
Seata提供了两个注解@GlobalTransactional和@GlobalLock:
- @GlobalTransactional表示声明开启一个全局事务。
- @GlobalLock执行当前操作需要检查全局锁。
如果一个服务的某个方法被标注了@GlobalTransactional,并且该服务又是服务调用链的起点,那么当前服务就是TM,要负责进行全局事务的开启、提交、回滚等一系列操作。
而如果一个服务的某个方法被标注了@GlobalLock注解,则执行本地事务时,在获取到本地锁执行了本地事务的操作后,先不提交,会请求TC检查全局锁是否存在,如果存在,说明当前有全局事务正修改同一条记录,因此当前操作回滚,之后全局锁不存在,才提交本地事务,然后释放本地锁。
@GlobalLock注解不会开启全局事务,但是可以防止与全局事务的并发读写冲突。
Seata四种事务模式
AT
流程
AT模式一阶段流程:
- TM请求TC开启全局事务
- TM调用RM执行(第一个RM),如果是TM和RM是同一个服务则是本地调用,否则就是远程调用
- RM生成前置镜像(当前记录修改前的快照)、执行sql、生成后置镜像(当前记录修改后的快照)、根据前置镜像和后置镜像生成undolog并插入(注意:这个undolog不是数据库的undolog日志,而是seata搞的一个undolog表的记录,用于回滚已提交的本地事务)
- RM请求TC注册分支事务
- RM把执行的sql已经插入的undolog一并提交
- RM请求TC上报分支事务状态
- 如果调用链还有后续,则前一个RM调用后一个RM,后一个RM也是这样来一套
如果一切顺利,那么TM在二阶段就会提交全局事务。
AT模式二阶段(提交)流程:
- TM请求TC提交全局事务
- TC通知RM提交分支事务
- RM所谓的提交分支事务,就是异步删除一阶段生成的undolog,其他啥也没干
但是如果一阶段在调用链上有执行报错的话,就会被TM捕获到,然后TM在二阶段就会发起全局事务回滚。
AT模式二阶段(回滚)流程:
- TM请求TC回滚全局事务
- TC通知RM回滚分支事务
- RM开启本地事务,查询一阶段生成的undolog,根据undolog解析生成回滚语句,然后执行该回滚语句并提交
使用
Seata的AT模式使用非常简单,只需在需要开启全局事务的地方使用@GlobalTransactional注解。但是为了防止其他操作与当前全局事务出现并发读写冲突的情况,可以在@GlobalTransactional注解修饰的接口触发的调用链上的每个接口,使用@GlobalLock修饰。
TCC
流程
TCC模式一阶段:
- TM请求TC开启全局事务
- TM调用RM执行(第一个RM),如果是TM和RM是同一个服务则是本地调用,否则就是远程调用
- RM请求TC注册分支事务
- RM执行自定义的prepare操作
- RM请求TC上报分支事务状态
- 如果调用链还有后续,则前一个RM调用后一个RM,后一个RM也是这样来一套
如果一切顺利,那么TM在二阶段就会提交全局事务。
TCC二阶段(提交)流程:
- TM请求TC提交全局事务
- TC通知RM提交分支事务
- RM执行自定义的commit操作
但是如果一阶段在调用链上有执行报错的话,就会被TM捕获到,然后TM在二阶段就会发起全局事务回滚。
TCC二阶段(回滚)流程:
- TM请求TC回滚全局事务
- TC通知RM回滚分支事务
- RM执行自定义的rollback操作
使用
TCC的使用比AT模式复杂一点点。
@GlobalTransactional
public String doTransactionCommit(){
// rpc远程调用
tccActionOne.prepare(null,"one");
// 本地调用
tccActionTwo.prepare(null,"two");
}
比如上面的doTransactionCommit方法需要开启全局事务,那么还是需要@GlobalTransactional注解修饰。然后doTransactionCommit()里面调用了tccActionOne.prepare(null,“one”)和tccActionTwo.prepare(null,“two”)两个方法,假设tccActionOne.prepare(null,“one”)是远程调用,tccActionTwo.prepare(null,“two”)是本地调用。
public interface TccActionOne {
@TwoPhaseBusinessAction(name = "DubboTccActionOne", commitMethod = "commit", rollbackMethod = "rollback")
public boolean prepare(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "a") String a);
public boolean commit(BusinessActionContext actionContext);
public boolean rollback(BusinessActionContext actionContext);
}
然后两个接口的prepare方法上,需要添加@TwoPhaseBusinessAction。@TwoPhaseBusinessAction的name属性指定TCC的beanName,commitMethod属性指定commit操作执行的方法,rollback操作指定执行的rollback操作方法,使用@BusinessActionContextParameter可以把指定参数存入BusinessActionContext,那么二阶段就能从BusinessActionContext中取到。
@LocalTCC
public interface TccActionTwo {
@TwoPhaseBusinessAction(name = "TccActionTwo", commitMethod = "commit", rollbackMethod = "rollback")
public boolean prepare(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "a") String a);
public boolean commit(BusinessActionContext actionContext);
public boolean rollback(BusinessActionContext actionContext);
}
由于TccActionTwo是本地调用,因此在TccActionTwo上还要添加@LocalTCC注解。
SAGA
SAGA适合用于长事务,在SAGA事务模式中,每个事务参与者都提交本地事务,如果其中一个参与者提交失败,则会沿着调用链执行反向补偿。
每个正向服务和反向补偿都是一个本地事务,执行完会立刻提交。每个正向服务和反向服务都需要业务开发自己实现。
seata的saga事务模式是基于状态机引擎实现的。
使用saga模式,我们首先自己要画好一个状态流转图,然后根据状态流转图按照seata的规则定义描述状态图的json文件。json描述文件将交给状态机引擎执行,执行过程中的状态流转将按照json描述文件进行。
XA
XA模式一阶段:
XA模式二阶段(提交):
XA模式二阶段(回滚):
流程
XA的流程上面已经说了,这里不再重复描述。
使用
XA模式的使用方式是根据AT模式一样的,唯一区别就是代理数据源换成DataSourceProxyXA类型
public class DataSourceProxy {
@Bean("dataSourceProxy")
public DataSource dataSource(DruidDataSource druidDataSource) {
return new DataSourceProxyXA(druidDataSource);
}
}