一、分布式事务
为解决微服务建设后,各个业务中心之间耦合业务的数据强一致性问题,引入分布式事务。
本文档主要以seata为例说明。
分布式事务:http://seata.io/zh-cn/blog/seata-at-tcc-saga.html
GTS事务框架以及模式(偏server侧的说明):https://help.aliyun.com/document_detail/157850.html?spm=a2c4g.11186623.6.554.688b24a1NYmh7D
AT模式:https://mp.weixin.qq.com/s/Pypkm5C9aLPJHYwcM6tAtA
MT(Manual Transaction)模式(TCC):
http://seata.io/zh-cn/blog/tcc-mode-applicable-scenario-analysis.html
http://seata.io/zh-cn/blog/tcc-mode-design-principle.html
AT模式和MT模式的区别:
AT 模式基于 支持本地 ACID 事务 的 关系型数据库:
一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录。
二阶段 commit 行为:马上成功结束,自动 异步批量清理回滚日志。
二阶段 rollback 行为:通过回滚日志,自动 生成补偿操作,完成数据回滚。
相应的,MT 模式,不依赖于底层数据资源的事务支持:
一阶段 prepare 行为:调用 自定义 的 prepare 逻辑。
二阶段 commit 行为:调用 自定义 的 commit 逻辑。
二阶段 rollback 行为:调用 自定义 的 rollback 逻辑。
二、依赖
接入seata需要依赖以下两个基础包
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.2.0</version>
</dependency>
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.9</version>
</dependency>
三、配置
## seata
seata.enabled=true
## 是否启用数据源代理(如果需要接入AT模式,则需要设置为true,如果不需要AT模式,则需要设置为false,默认为true)
seata.enable-auto-data-source-proxy=false
seata.application-id=${spring.application.name}
seata.client.rm.report-success-enable=true
seata.client.rm.table-meta-check-enable=false
seata.client.rm.report-retry-count=5
seata.client.rm.async-commit-buffer-limit=10000
seata.client.rm.lock.retry-interval=10
seata.client.rm.lock.retry-times=30
seata.client.rm.lock.retry-policy-branch-rollback-on-conflict=true
seata.client.tm.commit-retry-count=3
seata.client.tm.rollback-retry-count=3
seata.client.log.exceptionRate=100
## 服务信息
## 分组
seata.tx-service-group=supplier-chain-seata-group
seata.service.vgroup-mapping.supplier-chain-seata-group=default
## seata server的地址
seata.service.grouplist.default=127.0.0.1:8091
seata.service.enable-degrade=false
seata.service.disable-global-transaction=false
seata.transport.shutdown.wait=3
seata.transport.thread-factory.boss-thread-prefix=NettyBoss
seata.transport.thread-factory.worker-thread-prefix=NettyServerNIOWorker
seata.transport.thread-factory.server-executor-thread-prefix=NettyServerBizHandler
seata.transport.thread-factory.share-boss-worker=false
seata.transport.thread-factory.client-selector-thread-prefix=NettyClientSelector
seata.transport.thread-factory.client-selector-thread-size=1
seata.transport.thread-factory.client-worker-thread-prefix=NettyClientWorkerThread
seata.transport.type=TCP
seata.transport.server=NIO
seata.transport.heartbeat=true
seata.transport.serialization=seata
seata.transport.compressor=none
seata.transport.enable-client-batch-send-request=true
## 事务注册中心,根据需要切换注册中心类型,此处为zk
seata.registry.type=zk
seata.registry.zk.cluster=default
## zk地址
seata.registry.zk.serverAddr=127.0.0.1:2181
三、事务实现
seata-srping-boot-starter包默认启动AT + MT 模式
AT模式不需要进行代码改造,直接启动即可
MT模式,需要针对业务进行接口改造,需要将现有业务拆分为三部分:prepare、commit、rollback
prepare:资源预占
commit:资源使用
rollback:资源返还
建议针对性的抽取出单独的service进行改造。
其中实现需要满足以下条件
1、需要定义接口类
2、接口类中需要标注 LocalTCC 注解
3、对应的prepare函数需要标注 TwoPhaseBusinessAction 注解,注解中需要指定 prepare、commit、rollback 三部分对应的函数名
4、prepare、commit、rollback三部分对应的函数都必须包含参数 BusinessActionContext actionContext
5、prepare 函数对于分布事务参数外,其他业务参数需要增加注解标记,保证对应的参数在commit和rollback中能够获取到。例如 @BusinessActionContextParameter(paramName = “param”)
代码示例
/**
* 分布式事务
*
* @author yingchengpeng
* @since 2020-11-11
*/
@LocalTCC
public interface TransactionalService {
/**
* 事务预提交
*/
@TwoPhaseBusinessAction(name = "TccTransactionalService", commitMethod = "commit" , rollbackMethod = "rollback")
boolean prepare(BusinessActionContext actionContext,
@BusinessActionContextParameter(paramName = "param") String param);
/**
* 事务提交,库存业务中没有具体的资源临时占用,因此提交环节直接返回true即可
*/
boolean commit(BusinessActionContext actionContext);
/**
* 事务回滚
*/
boolean rollback(BusinessActionContext actionContext);
}
四、事务流程保障
TCC模式下,分布式事务的接入需要注意以下注意事项:
1、prepare以及commit需要 保证幂等校验
2、commit需要保证执行时必定是成功的,或最终一定会执行成功
3、rollback需要支持空回滚
4、针对异常情况下先触发rollback后触发prepare的场景进行规避,防止事务悬挂
针对悬挂问题,给出以下实现思路
在prepare和rollback入口处进行行为标记,如果在rollback的时候发现正在prepare,则拒绝rollback,如果执行prepare的时候正在进行rollback或已经执行完成rollback,则拒绝prepare操作。
目前通过AOP实现,代码如下
事务状态标记管理类
@Component
public class TransactionalSafeManager {
private final static String DISTRIBUTE_TRANSACTIONAL_LOCK = "bussiness_name:distribute_transactional_lock:";
private interface DISTRIBUTE_TRANSACTIONAL_STATUS {
String RUNNING = "running";
String ROLLBACKING = "rollbacking";
String ROLLBACKED = "rollbacked";
String PRE_PARED = "pre_pared";
String PRE_PARE_FAIL = "pre_pare_fail";
String COMMITTED = "committed";
}
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 标记进行中
*
* @param actionContext 事务上下文
* @return 是否标记成功
*/
public boolean marketRunning(BusinessActionContext actionContext) {
String xid = getXid(actionContext);
if (StringUtils.isEmpty(xid)) {
return true;
}
Boolean result = redisTemplate.opsForValue().setIfPresent(getDistributeTransactionalKey(xid), DISTRIBUTE_TRANSACTIONAL_STATUS.RUNNING);
if (result != null && result) {
return true;
}
String value = redisTemplate.opsForValue().get(getDistributeTransactionalKey(xid));
return value == null || value.equalsIgnoreCase(DISTRIBUTE_TRANSACTIONAL_STATUS.RUNNING)
|| value.equalsIgnoreCase(DISTRIBUTE_TRANSACTIONAL_STATUS.PRE_PARE_FAIL);
}
/**
* 标记预处理完成
*
* @param actionContext 事务上下文
*/
public void marketPrepared(BusinessActionContext actionContext) {
String xid = getXid(actionContext);
if (StringUtils.isEmpty(xid)) {
return;
}
redisTemplate.opsForValue().set(getDistributeTransactionalKey(xid), DISTRIBUTE_TRANSACTIONAL_STATUS.PRE_PARED);
}
/**
* 标记预处理失败
*
* @param actionContext 事务上下文
*/
public void marketPreparedFail(BusinessActionContext actionContext) {
String xid = getXid(actionContext);
if (StringUtils.isEmpty(xid)) {
return;
}
redisTemplate.opsForValue().set(getDistributeTransactionalKey(xid), DISTRIBUTE_TRANSACTIONAL_STATUS.PRE_PARE_FAIL);
}
/**
* 标记回滚
*
* @param actionContext 事务上下文
*/
public boolean marketRollBack(BusinessActionContext actionContext) {
String xid = getXid(actionContext);
if (StringUtils.isEmpty(xid)) {
return true;
}
Boolean result = redisTemplate.opsForValue().setIfPresent(getDistributeTransactionalKey(xid), DISTRIBUTE_TRANSACTIONAL_STATUS.ROLLBACKING);
if (result != null && result) {
return true;
}
String value = redisTemplate.opsForValue().get(getDistributeTransactionalKey(xid));
return value == null || value.equalsIgnoreCase(DISTRIBUTE_TRANSACTIONAL_STATUS.ROLLBACKING)
|| value.equalsIgnoreCase(DISTRIBUTE_TRANSACTIONAL_STATUS.ROLLBACKED);
}
/**
* 标记回滚完成
*
* @param actionContext 事务上下文
*/
public void marketRollBacked(BusinessActionContext actionContext) {
String xid = getXid(actionContext);
if (StringUtils.isEmpty(xid)) {
return;
}
redisTemplate.opsForValue().set(getDistributeTransactionalKey(xid), DISTRIBUTE_TRANSACTIONAL_STATUS.ROLLBACKED);
}
/**
* 标记已提交
*
* @param actionContext 事务上下文
*/
public void marketCommitted(BusinessActionContext actionContext) {
String xid = getXid(actionContext);
if (StringUtils.isEmpty(xid)) {
return;
}
redisTemplate.opsForValue().set(getDistributeTransactionalKey(xid), DISTRIBUTE_TRANSACTIONAL_STATUS.COMMITTED);
redisTemplate.expire(getDistributeTransactionalKey(xid), 30, TimeUnit.MINUTES);
}
private String getXid(BusinessActionContext actionContext) {
// 不存在,认为不需要管控分布式事务
if (actionContext == null) {
return null;
}
return actionContext.getXid();
}
private String getDistributeTransactionalKey(String xid) {
return DISTRIBUTE_TRANSACTIONAL_LOCK + xid;
}
}
切面实现
@Aspect
@Slf4j
@Component
@Order(0)
public class TccSafeAop {
@Autowired
private TransactionalSafeManager transactionalSafeManager;
@Pointcut("execution(public * com.xxx.service.transactional.impl.*.*(..))")
public void pointcut() {
}
private Map<String, String> prepareMethod = Maps.newConcurrentMap();
private Map<String, String> commitMethod = Maps.newConcurrentMap();
private Map<String, String> rollbackMethod = Maps.newConcurrentMap();
@Around("pointcut()")
public Object doAop(ProceedingJoinPoint joinPoint) throws Throwable {
boolean isPrepare = false;
BusinessActionContext actionContext = null;
try {
Object[] args = joinPoint.getArgs();
if (args == null || args.length == 0) {
return joinPoint.proceed();
}
if (args[0] instanceof BusinessActionContext) {
Object returnObj;
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
actionContext = (BusinessActionContext) args[0];
String actionName = (String) actionContext.getActionContext("actionName");
String prepareMethodName = prepareMethod.get(actionName);
if(StringUtils.isEmpty(prepareMethodName)) {
prepareMethodName = (String) actionContext.getActionContext("sys::prepare");
prepareMethod.put(actionName, prepareMethodName);
}
if(method.getName().equals(prepareMethodName)) {
boolean marketResult = transactionalSafeManager.marketRunning(actionContext);
if(!marketResult) {
throw new BizException("operate-has-rollback", "事务操作已经被回滚,操作失败");
}
isPrepare = true;
returnObj = joinPoint.proceed();
transactionalSafeManager.marketPrepared(actionContext);
return returnObj;
}
String commitMethodName = commitMethod.get(actionName);
if(StringUtils.isEmpty(commitMethodName)) {
commitMethodName = (String) actionContext.getActionContext("sys::commit");
commitMethod.put(actionName, commitMethodName);
}
if(method.getName().equals(commitMethodName)) {
returnObj = joinPoint.proceed();
transactionalSafeManager.marketCommitted(actionContext);
return returnObj;
}
String rollbackMethodName = rollbackMethod.get(actionName);
if(StringUtils.isEmpty(rollbackMethodName)) {
rollbackMethodName = (String) actionContext.getActionContext("sys::rollback");
rollbackMethod.put(actionName, rollbackMethodName);
}
if(method.getName().equals(rollbackMethodName)) {
boolean marketResult = transactionalSafeManager.marketRollBack(actionContext);
if(!marketResult) {
throw new BizException("operate-can-not-rollback", "事务正在预提交操作,操作回滚失败");
}
returnObj = joinPoint.proceed();
transactionalSafeManager.marketRollBacked(actionContext);
return returnObj;
}
}
return joinPoint.proceed();
} catch (BizException e) {
if(isPrepare) {
transactionalSafeManager.marketPreparedFail(actionContext);
}
throw e;
} catch (Exception e) {
if(isPrepare) {
transactionalSafeManager.marketPreparedFail(actionContext);
}
throw new BizException("dubbo-api-invoke-fail", "dubbo接口调用失败");
}
}
}
五、Q & A
接入过程中遇到并解决的问题
5.1、启动加载找不到DruidDataSourceWrapper类
A: 依赖包中的dev-tools和seata的jar存在,冲突,目前主要通过去除dev-tools包解决该问题
5.2、xid组成?
xid = seata-server特定节点的ip:port:transactionId
5.3、如果发生回滚时,应用节点异常会怎么处理?
seata server在进行提交或回滚操作时会优先对同ip服务进行调用,如果找不到可用服务则会在找寻其他节点的服务进行调用。
源码:io.seata.core.rpc.netty.ChannelManager#getChannel