以TCC-Transaction框架以及Seata为例分析分布式事务TCC执行原理

先以tcc-transaction开源分布式事务框架为例

场景

以下订单,之后扣减库存、扣款业务为例。
创建订单并付款
主事务:订单提交成功
两个子事务:扣款,扣减库存也都成功

成功场景:
数据库中 account_tblid为1的money会减少 5,order_tbl中会新增一条记录,storage_tblid为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

做了两件事

  1. 持久化事务形态,比如jdbc -> 全局事务编号
  2. 注册一个事务【Threadlocal】

整体上就是

进入拦截器:

  1. 开启全局事务
  2. 持久化全局事务
  3. 注册全局事务
  4. 判断是应该Confirm还是cancel
  5. 清除事务

离开拦截器

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,则需要

  1. 全局事务xid获取(与TC通讯)
  2. before image(解析SQL,查询一次数据库)
  3. after image(查询一次数据库)
  4. insert undo log(写一次数据库)
  5. before commit(与TC通讯,判断锁冲突)

可以看到,其是基于保存undo log来实现回滚的

使用Seata框架要考虑性价比

为了进行自动补偿,需要对所有交易生成前后镜像并持久化,可是在实际业务场景下,这个是成功率有多高,或者说分布式事务失败需要回滚的有多少比率?这个比例在不同场景下是不一样的,考虑到执行事务编排前,很多都会校验业务的正确性,所以发生回滚的概率其实相对较低。按照二八原则预估,即为了20%的交易回滚,需要将80%的成功交易的响应时间增加5倍,这样的代价相比于让应用开发一个补偿交易是否是值得?值得我们深思。

参考:分布式事务选型的取舍

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值