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

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

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

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

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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等)可以使用不同的事务上下文传递方式。
enter image description here

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

1
2
3
4
5
6
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方法为例。

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  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需要使用事务上下文、调用上下文以及事务上下文工厂作为参数,具体的调用原理非常简单,使用调用上下文携带的信息,先反射拿到要调用的方法,然后执行调用。同时,这里也可以清晰看到使用事务上下文工厂的好处。

Comments