如何简单实现TCC分布式事务框架
最近听到很多其他公司的小伙伴谈分布式事务的问题,各种业务场景都有,可能就是这两年很多公司都在往微服务发展,现在各个子系统都拆分、建设的差不多了,实现了模块化开发,但是也遇到了很多分布式事务等问题,大多都用消息重试来保证外部系统的最终一致,或者把外部参与者限制为一个,其他操作全部本地实现、再结合业务场景的方式来解决。
如果业务要求严格一致性、执行时间短、实时性要求高,那么使用补偿事务TCC是比较合适的,但是TCC事务模型虽然说起来简单,好像简单的调用一下Confirm/Cancel业务就可以了,但是如果不了解它的实现原理,直接去使用那些开源的、商用的框架,可能会有一定难度和风险。
然后自己的一些项目也一直有相似的问题,于是周末就尝试写了一个基于Spring Cloud的TCC分布式事务框架,在一个调用现金系统+红包系统完成支付订单的工程中跑了一下可用,demo仓库地址在下面有列出。
背景上面已经说了,然后说一下实现的思路:
目标:
基于spring cloud开发,代码侵入性少,可读性强,结构精简的TCC框架;
技术:
根据Spring Cloud的Fegin等组件的特性,调研了一些厂的做法,使用Spring、JDK的ThreadLocal、AOP、事务管理器、自定义注解等特性。
特点:
一阶段调用和平常的外部调用一样,依次调用外部参与者即可;
二阶段调用由框架自动完成;
独立的事务恢复服务,扩展性好,使用Spring Task实现。
代码已经实现差不多,已上传仓库,TwoStage:https://github.com/anylots/payment
TCC示意图
![](https://i-blog.csdnimg.cn/blog_migrate/96274fb3fb96d457578e85a3325158bc.png)
用语雀画了个图,TCC其实是这样,简单来说就是业务应用(发起者)需要将远程调用拆分为两步:
- 第一布Try锁定资源(比如账户冻结10块钱);
- 第二步Confirm/Cancel操作(比如扣减上一步冻结的10块钱/取消冻结)。
- 其中第二步的Confirm/Cancel操作由事务协调器自动完成,这个事务协调器一般是作为一个模块引入到发起者系统的,发起者只需满足简单的编码规范即可。
- 参与者需要实现两阶段中的Try,Confirm/Cancel三套逻辑,具体的业务场景实现也就不一样。
框架使用
这里写了一个调用现金系统+红包系统完成支付订单的工程demo,两个系统必须都调用成功,才能完成支付。
step1、开启分布式事务
首先在发起者方法上加上Spring事务注解@Transactional,然后在执行支付的代码中添加TwoStageStarter.startTwoStage()即可开启两阶段提交:
/**
* 两阶段支付服务
*
* @param payInfo 支付工具信息
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void payWithTwoStage(List<Map<String, Object>> payInfo) {
//step1.开启两阶段提交
TwoStageStarter.startTwoStage();
//step2.现金扣减
balanceManageService.balanceReduce(buildBalanceReduceInfo(payInfo));
//step3.红包使用
couponManageService.couponUse(buildCouponUseInfo(payInfo));
}
step2、在参与者方法前面加上@TwoStages注解
一阶段调用该方法时,拦截器将存储该方法的信息,在二阶段时自动再次调用。
/**
* 现金扣减
*
* @param reduceInfo
*/
@TwoStages
@Override
public void balanceReduce(BalanceReduceInfo reduceInfo) {
//step 1. balance reduce
String result = balanceServiceClient.balanceReduce(reduceInfo);
//step 2. assertion results
Assert.isTrue(ServiceConstants.SUCCESS.equals(result), "couponUse result is fail");
}
框架实现原理
1、Spring事务同步器
分布式事务的提交是和本地事务绑定在一起的,第一步的TwoStageStarter.startTwoStage()方法定义了一个事务同步器,并注册到Spring事务上下文中,事务同步器中的twoPhaseProcess逻辑将在本地事务提交时执行:
/**
* 开启两阶段提交
* <p>
* 两阶段提交TwoStage的启动须放在本地Spring事务中,
* 且须放在调用外部参与者之前。
* <p>
* 在一阶段调用时,TwoStagesAspect拦截器将参与者类名、方法名、参数保存在ThreadLocal中,
* 在本地事务提交、回滚后,Spring事务同步器将取出一阶段保存的信息,自动调用参与者二阶段方法,完成最终提交/回滚。
*/
public static void startTwoStage() {
//定义spring事务同步器
TransactionSynchronizationAdapter tccSynchronizationAdapter = new TransactionSynchronizationAdapter() {
//在事务提交/回滚后调用
@Override
public void afterCompletion(int status) {
switch (status) {
case 0:
//transaction status is commit
twoPhaseProcess(TransactionStatusEnum.STATUS_COMMITTED.getCode());
break;
case 1:
//transaction status is rollback
twoPhaseProcess(TransactionStatusEnum.STATUS_ROLLED_BACK.getCode());
break;
default:
logger.error("tcc transaction status is unknown");
throw new RuntimeException("tcc transaction status is unknown");
}
}
};
//注册spring事务同步器,spring本地事务提交、回滚时会执行事务同步器中对应的方法;
TransactionSynchronizationManager.registerSynchronization(
tccSynchronizationAdapter
);
}
/**
* 第二阶段处理
*
* @param stage 提交、回滚
*/
private static void twoPhaseProcess(String stage) throws RuntimeException {
//获取一阶段调用时保存的参与者信息
Set<TwoStageCompleter> stageCompletes = TwoStagesThreadLocal.get();
if (stageCompletes == null) {
logger.error("stageCompletes is null");
return;
}
for (TwoStageCompleter completer : stageCompletes) {
completer.invokeAfterPrepare(stage);
}
}
2、参与者拦截器
参与者方法上的@TwoStages注解,会被拦截器TwoStagesAspect的方法advice()拦截到,然后当识别到是一阶段调用时,会将一阶段调用涉及到的类、方法、参数封装成一个TwoStageCompleter对象,保存在线程变量TwoStagesThreadLocal中:
@Around("pointcut() && @annotation(twoStages)")
public void advice(ProceedingJoinPoint joinPoint, TwoStages twoStages) {
CommonInfo commonInfo = (CommonInfo) joinPoint.getArgs()[0];
//一阶段调用
if (commonInfo.isOnPrepareStage()) {
//保存参与者信息到线程变量
Set<TwoStageCompleter> stageCompletes = TwoStagesThreadLocal.get() == null ? new HashSet<>() : TwoStagesThreadLocal.get();
stageCompletes.add(buildTwoStageCompleter(joinPoint, commonInfo));
//保存事务订单记录
OrderRecordService orderRecordService = (OrderRecordService) ApplicationContextGetBeanHelper.getBean(OrderRecordService.class);
orderRecordService.saveOrderRecord(buildOrderRecord());
}
try {
joinPoint.proceed();
} catch (Throwable throwable) {
LoggerUtil.error("tcc invoke error", throwable);
throw new RuntimeException("tcc invoke error", throwable);
}
}
3、两阶段事务完成者TwoStageCompleter
TwoStageCompleter包含了参与者类、参与者方法名、参与者请求参数三个属性,以及二阶段执行方法invokeAfterPrepare():
/**
* 两阶段事务完成者
*
* @author anylots
* @version $Id: TwoStageSync.java, v 0.1 2020年10月18日 20:14 anylots Exp $
*/
public class TwoStageCompleter {
private static Logger logger = LoggerFactory.getLogger(TwoStageCompleter.class);
/**
* name of the class 参与者类
*/
private Class targetClass;
/**
* name of the class 参与者方法名
*/
private String methodName;
/**
* 参与者请求参数
*/
private CommonInfo commonInfo;
/**
* invoke after prepare
*
* @param stage
*/
public void invokeAfterPrepare(String stage) {
//设置参与者请求阶段
commonInfo.setStage(stage);
//调用参与者提交、回滚
try {
Method method = targetClass.getMethod(methodName, new Class[]{CommonInfo.class});
method.invoke(ApplicationContextGetBeanHelper.getBean(targetClass), commonInfo);
} catch (ReflectiveOperationException e) {
logger.error("tcc method invoke error", e);
throw new RuntimeException("tcc method invoke error", e);
}
logger.info("远程参与者事务提交/回滚完成");
}
参与者两阶段方法实现
外部系统参与者Service需要实现TwoStageCommonService抽象类,然后根据具体业务实现prepare、commit、cancel方法:
public abstract class TwoStageCommonService {
/**
* 现金扣减两阶段方法
*
* @param commonInfo
*/
public void process(CommonInfo commonInfo) {
switch (commonInfo.getStage()) {
case "prepare":
prepare(commonInfo);
break;
case "commit":
commit(commonInfo);
break;
case "cancel":
cancel(commonInfo);
break;
default:
break;
}
}
public abstract void prepare(CommonInfo commonInfo);
public abstract void commit(CommonInfo commonInfo);
public abstract void cancel(CommonInfo commonInfo);
参与者方法必须实现幂等,以支持事务恢复任务、发起者重试;
4、事务恢复任务TccScheduledTask
如果发生二阶段执行失败,TccScheduledTask将定期捞取未完成的订单,重复调用参与者直到成功:
/**
* 每隔十分钟执行, 单位:ms。
*/
@Scheduled(fixedRate = 10 * 60 * 1000)
public void executeFixRate() {
//捞取未完成事务记录
List<OrderRecord> orderRecords = orderRecordService.findByStatus(OrderStatusEnum.INIT.getCode());
for (OrderRecord orderRecord : orderRecords) {
//解析二阶段参与者列表
List<String> feignList = JSON.parseObject(orderRecord.getContext(), new TypeReference<List<String>>() {
});
//依次调用参与者,完成二阶段事务
for (String feignInfo : feignList) {
invokeForFeign(feignInfo, buildCommonInfo(orderRecord));
}
//更新发起者事务记录表
orderRecord.setStatus(OrderStatusEnum.COMPLETE.getCode());
orderRecordService.updateByOrderId(orderRecord);
}
}
调用参与者方法invokeForFeign的逻辑是这样的:
/**
* invoke for feign
*
* @param feignInfo
* @param commonInfo
*/
private void invokeForFeign(String feignInfo, CommonInfo commonInfo) {
//step1. feignInfo校验
if (StringUtils.isEmpty(feignInfo) || !feignInfo.contains("_")) {
LoggerUtil.error(String.format("feignInfo is not available,orderId=", commonInfo.getOrderId()));
return;
}
//step2. 获取feign class
Class clazz = null;
try {
clazz = Class.forName(feignInfo.split("_")[0]);
} catch (ClassNotFoundException e) {
LoggerUtil.error(String.format("feign class is not found,orderId=", commonInfo.getOrderId()), e);
}
//step3.调用参与者提交、回滚
try {
Method method = clazz.getMethod(feignInfo.split("_")[1], new Class[]{CommonInfo.class});
method.invoke(ApplicationContextGetBeanHelper.getBean(clazz), commonInfo);
} catch (ReflectiveOperationException e) {
LoggerUtil.error("tcc schedule invoke error", e);
}
}
项目结构:
说明:本文还未完善,对分布式事务框架还将继续研究,然后继续更新