一、什么是Seata?
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
PS:各事务模式介绍可参考:http://seata.io/zh-cn/docs/overview/what-is-seata.html
二、AT
Seata的优势在于提供了多种事务模式,用户可以根据需要进行选择,Seata目前复合主键仅支持mysql,如果项目使用mysql数据库,AT模式基本能满足你的全部需求,seata配置好后,在接口方法入口处加上@GlobalTransactional注解,Seata会自动完成一阶段SQL解析、执行、将日志记录插入到UNDO_log表,二阶段提交/回滚等操作,无需自己代码实现,基本能实现对原本业务代码零侵入。
三、TCC
本次公司项目使用的是Oracle,Seata不支持Oracle多主键,所以采用的是TCC模式,接下来对TCC模式进行较为详细的介绍:
TCC模式和AT模式一样,也是采用两阶段提交协议:
- 一阶段 prepare 行为
- 二阶段 commit 或 rollback 行为
与AT模式不相同的是,TCC模式下的prepare 、commit和rollback 都需要自己代码实现,例如本次项目的签名接口,涉及七个服务的调用,其中有3个服务涉及对复合主键表的操作,这就导致对原有业务代码的侵入性较强,涉及到有多主键表的增删改操作都需进行TCC改造,需自己代码实现其prepare 、commit和rollback逻辑。
四、DEMO FOR TCC
以下是项目某接口TCC改造后的部分伪代码
package说明
com.demo.tcc -----用于放tcc接口定义
com.demo.tcc.impl -----用于放tcc接口实现
(1)定义TCC接口
package com.demo.tcc;
@LocalTCC
public interface DemoTccAction {
@TwoPhaseBusinessAction(name = "saveAll" , commitMethod = "saveAllCommit", rollbackMethod = "saveAllRollback")
List<User> saveAll(@BusinessActionContextParameter (paramName = "UserList") List<User> UserList);
boolean saveAllCommit(BusinessActionContext actionContext);
TwoPhaseResult saveAllRollback(BusinessActionContext actionContext);
}
- @LocalTCC 适用于SpringCloud+Feign模式下的TCC
- @TwoPhaseBusinessAction 注解try方法,其中name为当前tcc方法的bean名称,写方法名便可,commitMethod指向提交方法,rollbackMethod指向事务回滚方法。指定好三个方法之后,seata会根据事务的成功或失败,通过动态代理去帮我们自动调用提交或者回滚。
- @BusinessActionContextParameter 注解可以将参数传递到二阶段(commitMethod/rollbackMethod)的方法。
- BusinessActionContext 是指TCC事务上下文
(2)tcc接口两阶段实现
package com.demo.tcc.impl;
@Service
@Slf4j
public class DemoTccActionImpl extends BaseLogicImpl<UserDao, User> implements DemoTccAction {
@Override
public List<User> saveAll(List<User> UserList) {
List<User> users= dao.saveAll(UserList);
dao.flush();
return users;
}
@Override
public boolean saveAllCommit(BusinessActionContext actionContext) {
log.info("xid = {}, branchId = {},保存用户提交成功", actionContext.getXid(), actionContext.getBranchId());
return true;
}
@Override
public TwoPhaseResult saveAllRollback(BusinessActionContext actionContext) {
log.info("xid = {}, branchId = {},发生异常,保存用户开始回滚...", actionContext.getXid(), actionContext.getBranchId());
TwoPhaseResult result = new TwoPhaseResult(true, "");
// 空回滚
if (actionContext.getActionContext("UserList") == null) {
log.warn("BusinessActionContext 不存在记录", actionContext.getXid(), actionContext.getBranchId());
return result;
}
String json = actionContext.getActionContext("UserList").toString();
List<User> UserList= JSON.parseArray(json,User.class);
for (User user : UserList) {
dao.deleteByUserIdAndUserNo(user.getUserId(),user.getUserNo());
}
log.info("xid = {}, branchId = {},保存用户回滚成功", actionContext.getXid(), actionContext.getBranchId());
return result;
}
踩坑汇总:
1.Caused by: io.seata.common.exception.NotSupportYetException: MZSF.MZYS_TBS_DPRESP needs to contain the primary key. 或者 TCC模式 下报io.seata.common.exception.NotSupportYetException: multi pk only support mysql !
原因:@GlobalTransactional和@Transactional不一起使用,解决方案:去除@Transactional 或者 使用 saveAndFlush
2.ArrayIndexOutOfBoundsException: 22/4001
原因:AT模式Jpa saveAll报数组越界, 改成循环saveAndFlush
3.@BusinessActionContextParameter会将参数存在branch表,不必担心数据丢失
4.复合主键进行删除操作时先将删除的对象查询出来,可存在@BusinessActionContextParameter,用于二阶段回滚时使用
5.seata无法调用存储过程,原因:seata无法判断存储过程进行了什么操作