文章目录
先以tcc-transaction开源分布式事务框架为例
场景
以下订单,之后扣减库存、扣款业务为例。
创建订单并付款
主事务:订单提交成功
两个子事务:扣款,扣减库存也都成功
成功场景:
数据库中 account_tbl
的id
为1的money
会减少 5,order_tbl
中会新增一条记录,storage_tbl
的id
为1的count
字段减少 1
失败场景:
数据库中的数据没有发生变化
整体处理流程
在TCC事务中TRY阶段,订单支付服务将订单状态变成PAYING,同时远程调用库存服务和资金帐户服务,对于库存服务,扣减库存;对于资金账户服务,将付款方的余额减掉(预留业务资源);
如果在TRY阶段,任何一个服务失败,tcc-transaction将自动调用这些服务对应的cancel方法,订单支付服务将订单状态变成PAY_FAILED,同时远程调用库存服务和资金帐户服务,将库存和付款方余额减掉的部分增加回去;
如果TRY阶段正常完成,则进入CONFIRM阶段,在CONFIRM阶段(tcc-transaction自动调用),远程调用库存服务和资金帐户服务对应的CONFIRM方法,将收款方的余额增加,如果没有问题,订单支付服务将订单状态变成CONFIRMED。如果执行失败,同try部分的失败处理。
使用TCC开发需要做的
order服务、库存服务、资金账户服务,都要实现try-confirm-cancel接口,同时,在confirm和cancel接口要做好幂等处理。开发成本大大增加。
TCC-Transaction开源框架执行原理
总结来说,其过程是这样的。
框架设计了两个AOP,来处理@Compensable注解
第一个切面
- 注册和初始化Transaction
第二个切面
- 组织事务参与者Participant
执行目标try方法
回到第一个切面,逐个执行List<Participant>的confirm或cancel方法
框架基本组成如下
1. 事务存储器
org.mengyun.tcctransaction.repository.JdbcTransactionRepository
支持将分布式事务信息存储到jdbc以及redis中
- TRANSACTION_ID:唯一标识,事务ID
- DOMAIN:用来标识 是哪个服务的事务
- GLOBAL_TX_ID:全局事务ID
- BRANCH_QUALIFIER:分支事务标识
- CONTENT:事务执行内容
- STATUS:事务执行状态 TRYING(1), CONFIRMING(2), CANCELLING(3);
- TRANSACTION_TYPE
- RETRIED_COUNT
- CREATE_TIME
- LAST_UPDATE_TIME
- VERSION:乐观锁版本
- IS_DELETE:逻辑删除
全局事务编号以及乐观锁版本是比较重要的,而且Transaction是要进行持久化存储的
事务JOB要根据这个持久化内容来处理
2. 事务拦截器
package org.mengyun.tcctransaction.interceptor
提供了两个切面
CompensableTransactionAspect
@Aspect
public abstract class CompensableTransactionAspect {
private CompensableTransactionInterceptor compensableTransactionInterceptor;
@Pointcut("@annotation(org.mengyun.tcctransaction.api.Compensable)")
public void compensableService() {
}
@Around("compensableService()")
public Object interceptCompensableMethod(ProceedingJoinPoint pjp) throws Throwable {
return compensableTransactionInterceptor.interceptCompensableMethod(pjp);
}
}
这个方法interceptCompensableMethod拦截的就是带有@Compensable注解的方法,
而这个方法的整体变成了一个入参,传递给了ProceedingJoinPoint pjp
可以获取到这个方法的入参、返回值、方法名等等
接下来执行org.mengyun.tcctransaction.interceptor.CompensableTransactionInterceptor#interceptCompensableMethod
public Object interceptCompensableMethod(ProceedingJoinPoint pjp) throws Throwable {
CompensableMethodContext compensableMethodContext = new CompensableMethodContext(pjp);
switch (compensableMethodContext.getMethodRole(isTransactionActive)) {
case ROOT:
return rootMethodProceed(compensableMethodContext);
case PROVIDER:
return providerMethodProceed(compensableMethodContext);
default:
return pjp.proceed();
}
}
分为:
-
主事务ROOT
-
分支事务,事务参与者PROVIDER
主事务ROOT
如果是主事务,那么就开启一个全新的事务
org.mengyun.tcctransaction.interceptor.CompensableTransactionInterceptor#rootMethodProceed
做了两件事
- 持久化事务形态,比如jdbc -> 全局事务编号
- 注册一个事务【Threadlocal】
整体上就是
进入拦截器:
- 开启全局事务
- 持久化全局事务
- 注册全局事务
- 判断是应该Confirm还是cancel
- 清除事务
离开拦截器
org.mengyun.tcctransaction.TransactionManager#begin(java.lang.Object)
public Transaction begin() {
Transaction transaction = new Transaction(TransactionType.ROOT);
transactionRepository.create(transaction);
registerTransaction(transaction);
return transaction;
}
继续向下执行
try {
// 开启一个全新的事务
/*
1、持久化事务形态 -> 全局事务编号
2、注册一个事务【Threadlocal】
*/
transaction = transactionManager.begin();
try {
// 执行目标方法 就是向下执行
returnValue = pjp.proceed();
}catch(Throwable tryingException){
transactionManager.rollback(asyncCancel);
throw tryingException;
}
transactionManager.commit(asyncConfirm);
}finally {
// 清除队列中的事务
transactionManager.cleanAfterCompletion(transaction);
}
这里要借鉴finally中的方法。当使用ThreadLocal之后,处理完成之后,要clear,防止信息错乱。
分支事务Provider
org.mengyun.tcctransaction.interceptor.CompensableTransactionInterceptor#providerMethodProceed
try {
switch (TransactionStatus.valueOf(compensableMethodContext.getTransactionContext().getStatus())) {
case TRYING:
// 初始化一份事务参与者的数据进入到当前服务中
transaction = transactionManager.propagationNewBegin(compensableMethodContext.getTransactionContext());
return compensableMethodContext.proceed();
case CONFIRMING:
try {
// 只是修改了状态
transaction = transactionManager.propagationExistBegin(compensableMethodContext.getTransactionContext());
transactionManager.commit(asyncConfirm);
} catch (NoExistedTransactionException excepton) {
//the transaction has been commit,ignore it.
}
break;
case CANCELLING:
try {
transaction = transactionManager.propagationExistBegin(compensableMethodContext.getTransactionContext());
transactionManager.rollback(asyncCancel);
} catch (NoExistedTransactionException exception) {
//the transaction has been rollback,ignore it.
}
break;
}
} finally {
// 清除事务
transactionManager.cleanAfterCompletion(transaction);
}
Method method = compensableMethodContext.getMethod();
作用总结
CompensableTransactionInterceptor作用总结
-
将事务区分为Root事务和分支事务
-
不断修改数据库内的事务状态【TRYING(1), CONFIRMING(2), CANCELLING(3)】
-
注册和清除事务管理器中的队列内容
org.mengyun.tcctransaction.TransactionManager
其使用ThreadLocal来实现隔离性
private static final ThreadLocal<Deque<Transaction>> CURRENT = new ThreadLocal<Deque<Transaction>>();
ResourceCoordinatorAspect
org.mengyun.tcctransaction.interceptor.ResourceCoordinatorInterceptor#interceptTransactionContextMethod
// 使用事务管理器,传递一些事务相关的信息
Transaction transaction = transactionManager.getCurrentTransaction();
if (transaction != null) {
switch (transaction.getStatus()) {
case TRYING:
enlistParticipant(pjp);
break;
case CONFIRMING:
break;
case CANCELLING:
break;
}
}
return pjp.proceed(pjp.getArgs());
主要处理try阶段的事情
org.mengyun.tcctransaction.interceptor.ResourceCoordinatorInterceptor#enlistParticipant
// 获取配置的TCC方法名
Method method = CompensableMethodUtils.getCompensableMethod(pjp);
if (method == null) {
throw new RuntimeException(String.format("join point not found method, point is : %s", pjp.getSignature().getName()));
}
Compensable compensable = method.getAnnotation(Compensable.class);
String confirmMethodName = compensable.confirmMethod();
String cancelMethodName = compensable.cancelMethod();
// 获取事务对象
Transaction transaction = transactionManager.getCurrentTransaction();
// 获取全局事务编号
TransactionXid xid = new TransactionXid(transaction.getXid().getGlobalTransactionId());
// 判断是否有一个全局事务对象
if (FactoryBuilder.factoryOf(compensable.transactionContextEditor()).getInstance().get(pjp.getTarget(), method, pjp.getArgs()) == null) {
FactoryBuilder.factoryOf(compensable.transactionContextEditor()).getInstance().set(new TransactionContext(xid, TransactionStatus.TRYING.getId()), pjp.getTarget(), ((MethodSignature) pjp.getSignature()).getMethod(), pjp.getArgs());
}
// 反射机制获取目标对象
Class targetClass = ReflectionUtils.getDeclaringType(pjp.getTarget().getClass(), method.getName(), method.getParameterTypes());
// confirm方法的执行上下文对象
InvocationContext confirmInvocation = new InvocationContext(targetClass,
confirmMethodName,
method.getParameterTypes(), pjp.getArgs());
// cancel方法的执行上下文对象
InvocationContext cancelInvocation = new InvocationContext(targetClass,
cancelMethodName,
method.getParameterTypes(), pjp.getArgs());
// 组成事务对象Participant
Participant participant =
new Participant(
xid,
confirmInvocation,
cancelInvocation,
compensable.transactionContextEditor());
// 将所有的资源信息,交给了事务管理器
transactionManager.enlistParticipant(participant);
ResourceCoordinatorInterceptor作用总结
-
在try阶段,将所有的“资源”封装完成并交给事务管理器
资源指的是事务资源,事务资源主要封装的就是事务的参与者
事务参与者由三部分组成
confirm的上下文,cancel的上下文,分支事务信息
-
事务管理器修改数据库对象
这两个拦截器经过后,就开始真正调用我们的目标对象了,如order服务、库存服务、资金账户服务。
3. 事务管理器
org.mengyun.tcctransaction.TransactionManager
org.mengyun.tcctransaction.interceptor.CompensableTransactionAspect#interceptCompensableMethod
由于是@around,从CompenableTransactionInterceptor过去之后,还要再回来经过一次
@Around("compensableService()")
public Object interceptCompensableMethod(ProceedingJoinPoint pjp) throws Throwable {
return compensableTransactionInterceptor.interceptCompensableMethod(pjp);
}
什么情况下回来有问题呢?就是报错了
也就是
也就是
try {
// 执行目标方法 就是向下执行
returnValue = compensableMethodContext.proceed();
} catch (Throwable tryingException) {
if (!isDelayCancelException(tryingException, allDelayCancelExceptions)) {
logger.warn(String.format("compensable transaction trying failed. transaction content:%s", JSON.toJSONString(transaction)), tryingException);
// 1. 修改tcc数据库中此transaction状态 CANCELING
// 2. 对transaction中各个participant执行rollback
// 3. 如果执行成功 删除事务资源数据
transactionManager.rollback(asyncCancel);
}
throw tryingException;
}
// 1. 修改tcc数据库中此transaction状态 CONFIRMING
// 2. 对transaction中各个participant执行confirm
// 3. 如果执行成功 删除事务资源数据
transactionManager.commit(asyncConfirm);
可以看到rollback中,取到的是Resource拦截器组装的Participant对象
可以看到,事务一起rollback
public void rollback() {
for (Participant participant : participants) {
participant.rollback();
}
}
在Participant中,使用cancel方法的上下文,通过反射调用cancel方法
public void rollback() {
// 通过反射调用预先设置好的cancel方法
Terminator.invoke(new TransactionContext(xid, TransactionStatus.CANCELLING.getId()), cancelInvocationContext, transactionContextEditorClass);
}
在commit方法中,也是如此
4. 事务处理JOB
其内部集成了quartz
org.mengyun.tcctransaction.spring.recover.RecoverScheduledJob
org.mengyun.tcctransaction.recover.TransactionRecovery
public void startRecover() {
List<Transaction> transactions = loadErrorTransactions();
recoverErrorTransactions(transactions);
}
通过loadErrorTransactions
transactionRepository.findAllUnmodifiedSince(new Date(currentTimeInMillis - recoverConfig.getRecoverDuration() * 1000));
将执行状态异常的Transaction从数据库中取出来
然后通过recoverErrorTransactions方法来处理
按配置进行事务重试
seata
阿里的seata的tcc模式的处理思想也是如此,不过其不需要麻烦的每个服务接口写try-confirm-cancel方法,以及幂等性的保证。
其business部分为TM(Transaction Manager),TCC-Transaction中各个Participant为RM(Resource Manager),其TC(Transaction Coordinator)是独立部署的。
seata使用
io.seata.samples.integration.call.service.BusinessServiceImpl#handleBusiness
@GlobalTransactional(timeoutMills = 300000, name = "dubbo-gts-seata-example")
@Override
public ObjectResponse handleBusiness(BusinessDTO businessDTO) {
log.info("开始全局事务,XID = " + RootContext.getXID());
ObjectResponse<Object> objectResponse = new ObjectResponse<>();
//1、扣减库存
CommodityDTO commodityDTO = new CommodityDTO();
commodityDTO.setCommodityCode(businessDTO.getCommodityCode());
commodityDTO.setCount(businessDTO.getCount());
ObjectResponse storageResponse = storageDubboService.decreaseStorage(commodityDTO);
//2、创建订单
OrderDTO orderDTO = new OrderDTO();
orderDTO.setUserId(businessDTO.getUserId());
orderDTO.setCommodityCode(businessDTO.getCommodityCode());
orderDTO.setOrderCount(businessDTO.getCount());
orderDTO.setOrderAmount(businessDTO.getAmount());
ObjectResponse<OrderDTO> response = orderDubboService.createOrder(orderDTO);
// 报错,实现回滚
if (storageResponse.getStatus() != 200 || response.getStatus() != 200) {
throw new DefaultException(RspStatusEnum.FAIL);
}
objectResponse.setStatus(RspStatusEnum.SUCCESS.getCode());
objectResponse.setMessage(RspStatusEnum.SUCCESS.getMessage());
objectResponse.setData(response.getData());
return objectResponse;
}
Seata架构的亮点主要有几个:
- 应用层基于SQL解析实现了自动补偿,从而最大程度的降低业务侵入性;
- 将分布式事务中TC,即seata-server(事务协调者)独立部署,负责事务的注册、回滚;
在应用层只需要使用@GlobalTransactional注解,如果异常,就throw出来,就实现分布式事务了,没有业务侵入性。
Seata框架做了以下的事情:
一条Update的SQL,则需要
- 全局事务xid获取(与TC通讯)
- before image(解析SQL,查询一次数据库)
- after image(查询一次数据库)
- insert undo log(写一次数据库)
- before commit(与TC通讯,判断锁冲突)
可以看到,其是基于保存undo log来实现回滚的
使用Seata框架要考虑性价比
为了进行自动补偿,需要对所有交易生成前后镜像并持久化,可是在实际业务场景下,这个是成功率有多高,或者说分布式事务失败需要回滚的有多少比率?这个比例在不同场景下是不一样的,考虑到执行事务编排前,很多都会校验业务的正确性,所以发生回滚的概率其实相对较低。按照二八原则预估,即为了20%的交易回滚,需要将80%的成功交易的响应时间增加5倍,这样的代价相比于让应用开发一个补偿交易是否是值得?值得我们深思。
参考:分布式事务选型的取舍