TCC型分布式事务原理和实现之:Transaction与Participant
TCC型分布式事务原理和实现之:Transaction与Participant
黑客画家 发表于7个月前
TCC型分布式事务原理和实现之:Transaction与Participant
  • 发表于 7个月前
  • 阅读 1374
  • 收藏 51
  • 点赞 2
  • 评论 5

腾讯云 技术升级10大核心产品年终让利>>>   

前言

      在TCC型分布式事务原理和实现之:TransactionManager一文中,介绍了TCC事务管理器的主要功能和实现原理。相较于事务管理器,事务则包含了更多的属性状态,下面的UML图中可以清晰的体现Transaction与Participant的关系。

            

事务

      事务具有很多的属性状态。首先,事务必须具有一个唯一ID来标识自己(保证进程内唯一即可),这样不同的事务就可以进行隔离控制,常见的事务ID生成方法就是使用uuid了;TCC事务一共有try、confirm和cancel三个阶段,因此,事务必须有一个事务状态字段来标识事物当前的状态:TRYING, CONFIRMING, CANCELLING;在TransactionManager一文中,多次提到根事务和分支事务,此处再重新提一下,所谓根事务,就是指事务的主动发起方,而分支事务,就是事务的被动发起方,也就是谁先开始谁就是老大,剩下的都是追随者、参与者。那么事务当然需要一个类型字段来标识当前事务的类型了,根事务用ROOT标识,分支事务用BRANCH标识;事务不一定总是成功,否则的话分布式事务也就不再是什么难题和秘密了,事务失败了怎么办呢?很多人第一想法就是回滚啊,其实,可以完成回滚的事务我将其理解为“正常事务”,也就是事务回滚成功,事务的一致性依然保持。然后,真正的异常事务是指在commit和cancel阶段失败的事务,那么这个时候怎么办呢?业界常用的手段就是:补偿。很多人在第一次听说事务补偿的时候,都觉得这是一个很高大上的技术,恰恰相反,补偿甚至是事务处理中最笨的办法。补偿也可以理解为弥补,就是一件事情做错了,尽可能的通过各种方法去弥补,使之尽可能的变得正确。的确,补偿也不代表就一定能够成功,因此通常会给这个补偿动作加一个时长或者次数限制,实在不行,就需要人工介入了,这是最后一道防线了。这里的retriedCount就表示一个事务在异常之后又被补偿重试的次数统计,通常都会有专门的监控系统来监控该字段的变化。由于补偿通常意味着多次重试,因此需要补偿方法是幂等的;createTime、lastUpdateTime表示事务的创建时间和最近更新时间,这在处理事务的超时、事务统计和事务补偿时非常有用。version表示事务的版本;participants就表示事务的所有参与者了,这里的参与者包括事务发起方本身(我将其称为根事务参与者)和分支事务参与者。通常,分支事务参与者都代表了一个远程服务;attachments可以用于暂存事务的附加参数,该附加参数可以被事务上下文携带着传到分支事务(远程服务),也相当于dubbo中的隐式传参了。

                    

      事务本身仅含有很少的方法属性,首先来看其构造方法。

 public Transaction(TransactionContext transactionContext) {
        this.xid = transactionContext.getXid();        // 从transactionContext中获取事务id
        this.status = TransactionStatus.TRYING;        // 事务状态为TRYING(因为是首次嘛)
        this.transactionType = TransactionType.BRANCH; // 事务类型为分支事务
    }

     该构造方法需要一个事务上下文作为参数,事务上下文和一次事务活动是一一对应的,它包含了事务ID、事务的当前状态、以及事务的附加参数,事务上下文必须是可序列化的,因为它会被序列化传送到远端(分支事务)。总而言之,整个分布式事务就是靠事务上下文串接起来的。该构造方法一般会在分支事务端被调用,用于根据从根事务端传递过来的事务上下文中创建一个分支事务。

      下面还有一个重载的构造方法版本。它需要一个事务类型作为参数,该构造方法通常会在根事务端被调用。

public Transaction(TransactionType transactionType) {
        this.xid = new TransactionXid();               // 获取新的事务id
        this.status = TransactionStatus.TRYING;
        this.transactionType = transactionType;
    }

      下面是事务中最核心的两个方法了:commit和rollback。原理很简单,遍历调用每一个参与者(Participant)的commit或rollback方法。如果你去对比JTA的实现,会发现代码如出一辙。 

    public void commit() {
        // 遍历所有参与者
        for (Participant participant : participants) {
            // 调用所有参与者的commit,这个参与者有本地参与者也有远程参与者
            participant.commit();
        }
    }

    // 回滚这个事物
    public void rollback() {
        for (Participant participant : participants) {
            // 遍历所有参与者,并调用其rollback
            participant.rollback();
        }
    }

 

事务参与者

      事务参与者(Participant)表示事务的一个参与方,通常,一次事务活动中会有多个事务参与者,否则也就没有必要使用分布式事务了。在TCC分布式事务中,通常会有多个远程服务作为分支事务参与者。

     下图为Participant的UML,先看属性字段。Participant需要一个事务ID来标识自己所属的事务。confirmInvocationContext和cancelInvocationContext都为InvocationContext类型,InvocationContext标识一个调用上下文,它非常像dubbo中的Invocation,封装了一个方法调用中的:目标对象、方法名、参数类型、参数列表等,不了解的可以先去看一下dubbo。其中,confirmInvocationContext标识参与者的confirm方法调用上下文,cancelInvocationContext标识cancel方法的调用上下文,这些上下文通过构造器注入的,下文将会提到。Terminator表示终结的意思,他表示执行最终的方法调用,暂时不做细说,后文再论。最后一个属性transactionContextEditorClass标识事务上下文的获取工厂,因为TCC框架本身要做到与具体的SOA框架无关,因此默认情况下,TCC的事务上下文都会作为事务方法的第一个参数显示传递,这样做的好处是通用性比较好,缺点就是对业务代码造成了侵入。实际上,某些SOA框架(比如dubbo)提供了非常良好的隐式传参的特性,因此事务上下文无需作为方法第一个参数了。为了TCC框架本身在保持框架无关性的同时,又能保证针对特定SOA的优化,所以对事务上下文抽象出了工厂,目前工厂的主要实现由:DubboTransactionContextEditor和MethodTransactionContextEditor,该属性可以在Compensable注解中指定,也就是在一次事务活动中,不同类型的事务参与者(比如基于dubbo的、基于http等)可以使用不同的事务上下文传递方式。

                            

      下面是Participant的构造方法,该构造方法会在ResourceCoordinatorInterceptor切面中被调用。

   public Participant(TransactionXid xid, InvocationContext confirmInvocationContext, InvocationContext cancelInvocationContext, Class<? extends TransactionContextEditor> transactionContextEditorClass) {
        this.xid = xid;
        this.confirmInvocationContext = confirmInvocationContext;
        this.cancelInvocationContext = cancelInvocationContext;
        this.transactionContextEditorClass = transactionContextEditorClass;
    }

      由于Participant的commit和rollback方法执行逻辑基本相同,因此此处只以commit方法为例。

 public void commit() {
        // 会调用真正的commit方法(业务提供的)
        terminator.invoke(new TransactionContext(xid, TransactionStatus.CONFIRMING.getId()), confirmInvocationContext, transactionContextEditorClass);
    }

       这里用到了上文提到的Terminator,看一下invoke方法源码:

  public Object invoke(TransactionContext transactionContext, InvocationContext invocationContext, Class<? extends TransactionContextEditor> transactionContextEditorClass) {


        if (StringUtils.isNotEmpty(invocationContext.getMethodName())) {

            try {
                // 获取targetClass单例
                Object target = FactoryBuilder.factoryOf(invocationContext.getTargetClass()).getInstance();

                Method method = null;

                // 反射拿到真正方法
                method = target.getClass().getMethod(invocationContext.getMethodName(), invocationContext.getParameterTypes());
                // dubbo隐士传参或者方法显示传参
                FactoryBuilder.factoryOf(transactionContextEditorClass).getInstance().set(transactionContext, target, method, invocationContext.getArgs());
                // 反射调用真正的方法(本地或者远程)
                return method.invoke(target, invocationContext.getArgs());

            } catch (Exception e) {
                throw new SystemException(e);
            }
        }
        return null;
    }

      invoke需要使用事务上下文、调用上下文以及事务上下文工厂作为参数,具体的调用原理非常简单,使用调用上下文携带的信息,先反射拿到要调用的方法,然后执行调用。同时,这里也可以清晰看到使用事务上下文工厂的好处。

系列文章

TCC型分布式事务原理和实现之:原理介绍

TCC型分布式事务原理和实现之:TransactionManager

TCC型分布式事务原理和实现之:Transaction与Participant

TCC型分布式事务原理和实现之:事务切面

TCC型分布式事务原理和实现之:事务recovery

TCC型分布式事务原理和实现之:兼容dubbo

共有 人打赏支持
粉丝 85
博文 58
码字总数 150060
评论 (5)
小遥yao
写的非常好
什么都会点
求更新
黑客画家

引用来自“小遥yao”的评论

写的非常好
谢谢
黑客画家

引用来自“什么都会点”的评论

求更新
最近比较忙,晚点更新
zqhxuyuan
很好。找到了一个REST TCC的PPT,很不错。期待LZ更新,今天发现了一个不错的PPT:http://www.pautasso.info/talks/2013/cascon/rest-tcc.html。讲REST的TCC
×
黑客画家
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
* 金额(元)
¥1 ¥5 ¥10 ¥20 其他金额
打赏人
留言
* 支付类型
微信扫码支付
打赏金额:
已支付成功
打赏金额: