2019-08-02
10002分布式事务实现TransactionManager

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

目前,还没有一款商用成熟的开源TCC框架,所以很多人基于不同思想实现了TCC的很多版本,本文所分析的TCC框架,是笔者在读了一些TCC开源代码、JTA设计思想之后逐步修改而来,由于代码还在逐步优化中,但是大体已经成型,所以将TCC框架的一点设计思想分享出来。

核心类图
enter image description here

事务管理器(TransactionManager)作为TCC分布式事务的核心组件,负责事务的管理工作(包括事务的提交和回滚)。 每一个事务的参与者(Participant)中都会有一个单例的事务管理器负责本地事务的管理工作。在TCC的try阶段,根事务(ROOT,表示事务的发起方)参与者中的事务管理器会负责创建根事务并持久化事务日志,同时会将创建的事务保存在ThreadLocal类型的队列中,处于分支事务参与者中的事务管理器会直接从事务上下文中传播(propagation)一个新的分支事务,同时也会持久化事务日志并将事务加入ThreadLocal队列中。在根事务端,try阶段结束后,TCC框架会根据try阶段是否有异常分别自动调用根事务管理器的commit和rollback方法,以commit为例,根事务管理器的commit又会调用根事务(transaction)的commit方法,根事务(transaction)的commit方法会遍历所有的事务参与者(Participant)的commit方法,这里的参与者一共有两种,分别为根事务参与者和分支事务参与者,这里的所谓的“根”和“分支”都只是逻辑上角色的区分,表示主动发起和被动参与的区别,它们在代码层面完全是共用一套逻辑的,比如,一个分支事务同样可以发起根事务,这样就是典型的嵌套型事务了。但是,整个事务的提交和回滚一定是由最顶层事务管理器发起的,其他的分支事务(包括多层嵌套事务)都是在最顶层根事务管理器的协调下完成自身的提交和回滚。至此,一个完整的分布式事务就结束了,当然,这只是一个大概的过程描述,还有很多细节没有提及,主要是目前的讨论还没有限制SOA框架,所以无法谈论具体的远程事务细节,等到后面专门讨论远程事务的文章(SOA为dubbo)时,会详细讨论根事务参与者和分支事务参与者。

事务管理器

事务管理器的属性只有transactionRepository和CURRENT,其中transactionRepository用于持久化事务日志,CURRENT用于保存该事务管理器上活动的事务,它是一个ThreadLocal队列。

enter image description here

事务管理器具有较多的方法,下面分表分析:

begin()

1
2
3
4
5
6
7
8
9
10
11
begin()表示开始一个事务,会在根事务(ROOT)的try方法中被调用。       

public Transaction begin() {
// 创建事务,事务类型为根事务ROOT
Transaction transaction = new Transaction(TransactionType.ROOT);
// 事务日志持久化
transactionRepository.create(transaction);
// 注册事务,就是加到线程局部变量的队列中
registerTransaction(transaction);
return transaction;
}

registerTransaction()

1
2
3
4
5
6
7
8
private void registerTransaction(Transaction transaction) {
// 如果队列还没有创建就先创建一个
if (CURRENT.get() == null) {
CURRENT.set(new LinkedList<Transaction>());
}
// 加入事务队列
CURRENT.get().push(transaction);
}

propagationNewBegin()
propagationNewBegin用于从一个事务上下文中传播一个新事务,通常会在分支事务(比如dubbo中的provider端)的try阶段被调用,此处的事务上下文可以使用方法传参,也可以使用特定SOA框架的隐式传参(比如dubbo)。

1
2
3
4
5
6
7
8
9
public Transaction propagationNewBegin(TransactionContext transactionContext) {
// 从transactionContext创建一个事务
Transaction transaction = new Transaction(transactionContext);
// 事务日志持久化
transactionRepository.create(transaction);
// 注册事务到事务管理器
registerTransaction(transaction);
return transaction;
}

propagationExistBegin()
propagationExistBegin用于从事务上下文中传播一个已存在的事务,通常会在分支事务(比如dubbo中的provider端)的confirm阶段和cancel阶段被调用,此处的事务上下文可以使用方法传参,也可以使用特定SOA框架的隐式传参(比如dubbo)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 public Transaction propagationExistBegin(TransactionContext transactionContext) throws NoExistedTransactionException {
// 从持久化日志中根据事务id拿到事务
Transaction transaction = transactionRepository.findByXid(transactionContext.getXid());
// 如果找到了事物
if (transaction != null) {
// 更改事务状态为transactionContext中的状态
transaction.changeStatus(TransactionStatus.valueOf(transactionContext.getStatus()));
// 注册事务
registerTransaction(transaction);
return transaction;
} else {
throw new NoExistedTransactionException();
}
}

commit()
commit会在事务try阶段没有异常的情况下,由TCC框架自动调用。它首先从ThreadLocal队列中取出当前要处理的事务(但不从队列中删除这个事务),然后将事务状态改为CONFIRMING状态,更新事务日志。随后调用事务的commit方法进行事务提交处理,如果事务提交成功(没有抛出任何异常),那么就从事务日志仓库中删除这个事务日志。如果在事务commit过程中抛出了异常,那么这个事物日志此时不会被删除(稍后会被recovery任务处理),同时,框架会将异常全部转为ConfirmingException向业务层抛出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 public void commit() {
// 获取本地线程上事务队列中的时间最久的事务
Transaction transaction = getCurrentTransaction();
if(transaction == null){
return;
}
// 更改事务状态为CONFIRMING
transaction.changeStatus(TransactionStatus.CONFIRMING);
// 更新事务持久化
transactionRepository.update(transaction);

try {
// 调用事务的commit
transaction.commit();
// 如果上面的commit没有抛出任何异常就说明事务成功,就从事务日志中删除这个事务
transactionRepository.delete(transaction);
} catch (Throwable commitException) {
// 事务commit过程抛出了异常
logger.error("compensable transaction confirm failed.", commitException);
// 转为抛出ConfirmingException异常,这样会导致事务在事务日志中不被删除,recovery会去处理长时间没有被删除的事务
throw new ConfirmingException(commitException);
}
}

getCurrentTransaction()
获取当前要处理的事务,此处要注意的是,队列的peek只是取出队列头部元素,但是不会将其删除。

1
2
3
4
5
6
7
public Transaction getCurrentTransaction() {
if (isTransactionActive()) {
// 拿到队列头的事务(但是不从队列中删除,删除是在cleanAfterCompletion中进行)
return CURRENT.get().peek();
}
return null;
}

isTransactionActive()
判断当前事务管理中是否还有活动的事务。

1
2
3
4
public boolean isTransactionActive() {
Deque<Transaction> transactions = CURRENT.get();
return transactions != null && !transactions.isEmpty();
}

rollback()

rollback和commit相对,当try阶段抛出了任何异常,TCC框架会自动调用。它首先从事务管理器中取出当前活动的事务,更改事务状态为CANCELLING,并更新事务日志。然后调用事务的rollback进行事务回滚(事务的rollback会遍历所有参与者,并分别调用参与者的rollback,通常,根事务端的参与者包含根事务参与者和分支事务参与者,而分支事务参与者通常只有一个本地的事务参与者,除非它也发起了TCC分布式事务)。如果rollback成功,事务会被从事务日志持久化仓库中删除,否则直接向业务层代码抛出CancellingException异常,残留的事务日志稍后会被recovery任务处理(可选、可配置)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void rollback() {
// 回滚事务
Transaction transaction = getCurrentTransaction();
// 更改事务状态为CANCELLING
transaction.changeStatus(TransactionStatus.CANCELLING);
// 更新事务持久化日志
transactionRepository.update(transaction);

try {
// 调用事务的rollback
transaction.rollback();
// 没有异常,就从事务日志中删除这个事务
transactionRepository.delete(transaction);
} catch (Throwable rollbackException) {
logger.error("compensable transaction rollback failed.", rollbackException);
// 否则事务异常,抛出CancellingException
throw new CancellingException(rollbackException);
}
}

cleanAfterCompletion()
还记得前文所说的,每次从事务管理器获取当前活动事务的时候,都不会从队列中将其删除,那么这些事务会在什么时候删除呢,这就是cleanAfterCompletion的作用。在每次事务处理结束时,TCC框架都会调用cleanAfterCompletion进行事务的清理操作。清理之前要比对要清理的事务是不是当前事务。

1
2
3
4
5
6
7
8
9
10
 public void cleanAfterCompletion(Transaction transaction) {
if (isTransactionActive() && transaction != null) {
Transaction currentTransaction = getCurrentTransaction();
if (currentTransaction == transaction) {
CURRENT.get().pop();
} else {
throw new SystemException("Illegal transaction when clean after completion");
}
}
}

enlistParticipant()
enlistParticipant用于向事务中添加一个事务参与者,同上,这里的参与者包含了本地参与者和远程参与者,添加参与者之后必须更新事务日志。enlistParticipant会在添加到TCC事务方法的切面中被调用

1
2
3
4
5
 public void enlistParticipant(Participant participant) {
Transaction transaction = this.getCurrentTransaction();
transaction.enlistParticipant(participant);//将参与者加入事务的参与者列表中
transactionRepository.update(transaction);// 更新事务日志
}

标记TCC事务方法

本文多次提到了TCC事务方法和切面逻辑,那么什么是一个TCC事务方法呢?给一个方法添加标记肯定要使用注解了,TCC框架中,使用Compensable注解来表示一个方法是一个TCC事务方法,同时TCC框架针对标记了Compensable注解的方法提供了两个切面:CompensableTransactionAspect和ResourceCoordinatorAspect。其中CompensableTransactionAspect用于封装事务逻辑(事务开启、提交和回滚等),而ResourceCoordinatorAspect切面用于封装一个参与者并添加到事务中(上文提及)。由于还有专门的文章介绍这两个切面,所以本文暂时不作深入讨论。这里需要注意的是,这两个切面是有优先级排序的,CompensableTransactionAspect优先级高于ResourceCoordinatorAspect,这个只要实现spring框架中的Ordered接口就可以了。
enter image description here

Read More

2019-08-02
10003分布式事务实现Transaction与Participant

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

Read More

2019-08-02
10001分布式事务原理

分布式事务原理

一个TCC事务框架需要解决的当然是分布式事务的管理。关于TCC事务机制的介绍,可以参考TCC事务机制简介。
TCC事务模型虽然说起来简单,然而要基于TCC实现一个通用的分布式事务框架,却比它看上去要复杂的多,不只是简单的调用一下Confirm/Cancel业务就可以了的。

本文将以Spring容器为例,试图分析一下,实现一个通用的TCC分布式事务框架需要注意的一些问题。

一、TCC全局事务必须基于RM本地事务来实现全局事务

TCC服务是由Try/Confirm/Cancel业务构成的,
其Try/Confirm/Cancel业务在执行时,会访问资源管理器(Resource Manager,下文简称RM)来存取数据。这些存取操作,必须要参与RM本地事务,以使其更改的数据要么都commit,要么都rollback。

这一点不难理解,考虑一下如下场景:

image

假设图中的服务B没有基于RM本地事务(以RDBS为例,可通过设置auto-commit为true来模拟),那么一旦[B:Try]操作中途执行失败,TCC事务框架后续决定回滚全局事务时,该[B:Cancel]则需要判断[B:Try]中哪些操作已经写到DB、哪些操作还没有写到DB:假设[B:Try]业务有5个写库操作,[B:Cancel]业务则需要逐个判断这5个操作是否生效,并将生效的操作执行反向操作。
不幸的是,由于[B:Cancel]业务也有n(0<=n<=5)个反向的写库操作,此时一旦[B:Cancel]也中途出错,则后续的[B:Cancel]执行任务更加繁重。因为,相比第一次[B:Cancel]操作,后续的[B:Cancel]操作还需要判断先前的[B:Cancel]操作的n(0<=n<=5)个写库中哪几个已经执行、哪几个还没有执行,这就涉及到了幂等性问题。而对幂等性的保障,又很可能还需要涉及额外的写库操作,该写库操作又会因为没有RM本地事务的支持而存在类似问题。。。可想而知,如果不基于RM本地事务,TCC事务框架是无法有效的管理TCC全局事务的。
反之,基于RM本地事务的TCC事务,这种情况则会很容易处理:[B:Try]操作中途执行失败,TCC事务框架将其参与RM本地事务直接rollback即可。后续TCC事务框架决定回滚全局事务时,在知道“[B:Try]操作涉及的RM本地事务已经rollback”的情况下,根本无需执行[B:Cancel]操作。

换句话说,基于RM本地事务实现TCC事务框架时,一个TCC型服务的cancel业务要么执行,要么不执行,不需要考虑部分执行的情况。

二、TCC事务框架应该接管Spring容器的TransactionManager

基于RM本地事务的TCC事务框架,可以将各Try/Confirm/Cancel业务看着一个原子服务:一个RM本地事务提交,参与该RM本地事务的所有Try/Confirm/Cancel业务操作都生效;反之,则都不生效。掌握每个RM本地事务的状态以及它们与Try/Confirm/Cancel业务方法之间的对应关系,以此为基础,TCC事务框架才能有效的构建TCC全局事务。

TCC服务的Try/Confirm/Cancel业务方法在RM上的数据存取操作,其RM本地事务是由Spring容器的PlatformTransactionManager来commit/rollback的,TCC事务框架想要了解RM本地事务的状态,只能通过接管Spring的事务管理器功能。

2.1. 为什么TCC事务框架需要掌握RM本地事务的状态?
首先,根据TCC机制的定义,TCC事务是通过执行Cancel业务来达到回滚效果的。仔细分析一下,这里暗含一个事实:
只有生效的Try业务操作才需要执行对应的Cancel业务操作。换句话说,只有Try业务操作所参与的RM本地事务被commit了,后续TCC全局事务回滚时才需要执行其对应的Cancel业务操作;否则,如果Try业务操作所参与的RM本地事务被rollback了,后续TCC全局事务回滚时就不能执行其Cancel业务,此时若盲目执行Cancel业务反而会导致数据不一致。

其次,Confirm/Cancel业务操作必须保证生效。Confirm/Cancel业务操作也会涉及RM数据存取操作,其参与的RM本地事务也必须被commit。TCC事务框架需要在确切的知道所有Confirm/Cancel业务操作参与的RM本地事务都被成功commit后,才能将标记该TCC全局事务为完成。如果TCC事务框架误判了Confirm/Cancel业务参与RM本地事务的状态,就会造成全局事务不一致。

最后,未完成的TCC全局,TCC事务框架必须重新尝试提交/回滚操作。重试时会再次调用各TCC服务的Confirm/Cancel业务操作。如果某个服务的Confirm/Cancel业务之前已经生效(其参与的RM本地事务已经提交),重试时就不应该再次被调用。否则,其Confirm/Cancel业务被多次调用,就会有“服务幂等性”的问题。

2.2. 拦截TCC服务的Try/Confirm/Cancel业务方法的执行,根据其异常信息可否知道其RM本地事务是否commit/rollback了呢?

基本上很难做到。为什么这么说呢?
第一,事务是可以在多个(本地/远程)服务之间互相传播其事务上下文的,一个业务方法(Try/Confirm/Cancel)执行完毕并不一定会触发当前事务的commit/rollback操作。比如,被传播事务上下文的业务方法,在它开始执行时,容器并不会为其创建新的事务,而是它的调用方参与的事务,使得二者操作在同一个事务中;同样,在它执行完毕时,容器也不会提交/回滚它参与的事务的。因此,这类业务方法上的异常情况并不能反映他们是否生效。不接管Spring的TransactionManager,就无法了解事务于何时被创建,也无法了解它于何时被提交/回滚。
第二、一个业务方法可能会包含多个RM本地事务的情况。比如: A(REQUIRED)->B(REQUIRES_NEW)->C(REQUIRED),这种情况下,A服务所参与的RM本地事务被提交时,B服务和C服务参与的RM本地事务则可能会被回滚。
第三、并不是抛出了异常的业务方法,其参与的事务就回滚了。Spring容器的声明式事务定义了两类异常,其事务完成方向都不一样:系统异常(一般为Unchecked异常,默认事务完成方向是rollback)、应用异常(一般为Checked异常,默认事务完成方向是commit)。二者的事务完成方向又可以通过@Transactional配置显式的指定,如rollbackFor/noRollbackFor等。
第四、Spring容器还支持使用setRollbackOnly的方式显式的控制事务完成方向;
最后、自行拦截业务方法的拦截器和Spring的事务处理的拦截器还会存在执行先后、拦截范围不同等问题。例如,如果自行拦截器执行在前,就会出现业务方法虽然已经执行完毕但此时其参与的RM本地事务还没有commit/rollback。

TCC事务框架的定位应该是一个TransactionManager,其职责是负责commit/rollback事务。而一个事务应该commit、还是rollback,则应该是由Spring容器来决定的:Spring决定提交事务时,会调用TransactionManager来完成commit操作;Spring决定回滚事务时,会调用TransactionManager来完成rollback操作。

接管Spring容器的TransactionManager,TCC事务框架可以明确的得到Spring的事务性指令,并管理Spring容器中各服务的RM本地事务。否则,如果通过自行拦截的机制,则使得业务系统存在TCC事务处理、RM本地事务处理两套事务处理逻辑,二者互不通信,各行其是。这种情况下要协调TCC全局事务,基本上可以说是缘木求鱼,本地事务尚且无法管理,更何谈管理分布式事务?

三、TCC事务框架应该具备故障恢复机制

一个TCC事务框架,若是没有故障恢复的保障,是不成其为分布式事务框架的。

分布式事务管理框架的职责,不是做出全局事务提交/回滚的指令,而是管理全局事务提交/回滚的过程。它需要能够协调多个RM资源、多个节点的分支事务,保证它们按全局事务的完成方向各自完成自己的分支事务。这一点,是不容易做到的。因为,实际应用中,会有各种故障出现,很多都会造成事务的中断,从而使得统一提交/回滚全局事务的目标不能达到,甚至出现”一部分分支事务已经提交,而另一部分分支事务则已回滚”的情况。比较常见的故障,比如:业务系统服务器宕机、重启;数据库服务器宕机、重启;网络故障;断电等。这些故障可能单独发生,也可能会同时发生。作为分布式事务框架,应该具备相应的故障恢复机制,无视这些故障的影响是不负责任的做法。

一个完整的分布式事务框架,应该保障即使在最严苛的条件下也能保证全局事务的一致性,而不是只能在最理想的环境下才能提供这种保障。退一步说,如果能有所谓“理想的环境”,那也无需使用分布式事务了。

TCC事务框架要支持故障恢复,就必须记录相应的事务日志。事务日志是故障恢复的基础和前提,它记录了事务的各项数据。TCC事务框架做故障恢复时,可以根据事务日志的数据将中断的事务恢复至正确的状态,并在此基础上继续执行先前未完成的提交/回滚操作。

四、TCC事务框架应该提供Confirm/Cancel服务的幂等性保障

一般认为,服务的幂等性,是指针对同一个服务的多次(n>1)请求和对它的单次(n=1)请求,二者具有相同的副作用。

在TCC事务模型中,Confirm/Cancel业务可能会被重复调用,其原因很多。比如,全局事务在提交/回滚时会调用各TCC服务的Confirm/Cancel业务逻辑。执行这些Confirm/Cancel业务时,可能会出现如网络中断的故障而使得全局事务不能完成。因此,故障恢复机制后续仍然会重新提交/回滚这些未完成的全局事务,这样就会再次调用参与该全局事务的各TCC服务的Confirm/Cancel业务逻辑。

既然Confirm/Cancel业务可能会被多次调用,就需要保障其幂等性。
那么,应该由TCC事务框架来提供幂等性保障?还是应该由业务系统自行来保障幂等性呢?
个人认为,应该是由TCC事务框架来提供幂等性保障。如果仅仅只是极个别服务存在这个问题的话,那么由业务系统来负责也是可以的;然而,这是一类公共问题,毫无疑问,所有TCC服务的Confirm/Cancel业务存在幂等性问题。TCC服务的公共问题应该由TCC事务框架来解决;而且,考虑一下由业务系统来负责幂等性需要考虑的问题,就会发现,这无疑增大了业务系统的复杂度。

五、TCC事务框架不能盲目的依赖Cancel业务来回滚事务

前文以及提到过,TCC事务通过Cancel业务来对Try业务进行回撤的机制暗含了一个事实:Try操作已经生效。也就是说,只有Try操作所参与的RM本地事务已经提交的情况下,才需要执行其Cancel操作进行回撤。没有执行、或者执行了但是其RM本地事务被rollback的Try业务,是一定不能执行其Cancel业务进行回撤的。因此,TCC事务框架在全局事务回滚时,应该根据TCC服务的Try业务的执行情况选择合适的处理机制。而不能盲目的执行Cancel业务,否则就会导致数据不一致。

一个TCC服务的Try操作是否生效,这是TCC事务框架应该知道的,因为其Try业务所参与的RM事务也是由TCC事务框架所commit/rollbac的(前提是TCC事务框架接管了Spring的事务管理器)。所以,TCC事务回滚时,TCC事务框架可考虑如下处理策略:
1)如果TCC事务框架发现某个服务的Try操作的本地事务尚未提交,应该直接将其回滚,而后就不必再执行该服务的cancel业务;
2)如果TCC事务框架发现某个服务的Try操作的本地事务已经回滚,则不必再执行该服务的cancel业务;
3)如果TCC事务框架发现某个服务的Try操作尚未被执行过,那么,也不必再执行该服务的cancel业务。

总之,TCC事务框架应该保障:
1)已生效的Try操作应该被其Cancel操作所回撤;
2)尚未生效的Try操作,则不应该执行其Cancel操作。这一点,不是幂等性所能解决的问题。如上文所述,幂等性是指服务被执行一次和被执行n(n>0)次所产生的影响相同。但是,未被执行和被执行过,二者效果肯定是不一样的,这不属于幂等性的范畴。

六、Cancel业务与Try业务并行,甚至先于Try操作完成

这应该算TCC事务机制特有的一个不可思议的陷阱。一般来说,一个特定的TCC服务,其Try操作的执行,是应该在其Confirm/Cancel操作之前的。Try操作执行完毕之后,Spring容器再根据Try操作的执行情况,指示TCC事务框架提交/回滚全局事务。然后,TCC事务框架再去逐个调用各TCC服务的Confirm/Cancel操作。

然而,超时、网络故障、服务器的重启等故障的存在,使得这个顺序会被打乱。比如:

image

上图中,假设[B:Try]操作执行过程中,网络闪断,[A:Try]会收到一个RPC远程调用异常。A不处理该异常,导致全局事务决定回滚,TCC事务框架就会去调用[B:Cancel],而此刻A、B之间网络刚好已经恢复。如果[B:Try]操作耗时较长(网络阻塞/数据库操作阻塞),就会出现[B:Try]和[B:Cancel]二者并行处理的现象,甚至[B:Cancel]先完成的现象。
这种情况下,由于[B:Cancel]执行时,[B:Try]尚未生效(其RM本地事务尚未提交),因此,[B:Cancel]是不能执行的,至少是不能生效(执行了其RM本地事务也要rollback)的。然而,当
[B:Cancel]处理完毕(跳过执行、或者执行后rollback其RM本地事务)后,[B:Try]操作完成又生效了(其RM本地事务成功提交),这就会使得[B:Cancel]虽然提供了,但却没有起到回撤[B:Try]的作用,导致数据的不一致。

所以,TCC框架在这种情况下,需要:
1)将[B:Try]的本地事务标注为rollbackOnly,阻止其后续生效;
2)禁止其再次将事务上下文传递给其他远程分支,否则该问题将在其他分支上出现;
3)相应地,[B:Cancel]也不必执行,至少不能生效。

当然,TCC事务框架也可以简单的选择阻塞[B:Cancel]的处理,待[B:Try]执行完毕后,再根据它的执行情况判断是否需要执行[B:Cancel]。不过,这种处理方式因为需要等待,所以,处理效率上会有所不及。
同样的情况也会出现在confirm业务上,只不过,发生在Confirm业务上的处理逻辑与发生在Cancel业务上的处理逻辑会不一样,TCC框架必须保证:
1)Confirm业务在Try业务之后执行,若发现并行,则只能阻塞相应的Confirm业务操作;
2)在进入Confirm执行阶段之后,也不可以再提交同一全局事务内的新的Try操作的RM本地事务。

七、TCC服务复用性是不是相对较差?

TCC事务机制的定义,决定了一个服务需要提供三个业务实现:Try业务、Confirm业务、Cancel业务。可能会有人因此认为TCC服务的复用性较差。怎么说呢,要是将 Try/Confirm/Cancel业务逻辑单独拿出来复用,其复用性当然是不好的,Try/Confirm/Cancel 逻辑作为TCC型服务中的一部分,是不能单独作为一个组件来复用的。Try、Confirm、Cancel业务共同才构成一个组件,如果要复用,应该是复用整个TCC服务组件,而不是单独的Try/Confirm/Cancel业务。

八、TCC服务是否需要对外暴露三个服务接口?

不需要。TCC服务与普通的服务一样,只需要暴露一个接口,也就是它的Try业务。Confirm/Cancel业务逻辑,只是因为全局事务提交/回滚的需要才提供的,因此Confirm/Cancel业务只需要被TCC事务框架发现即可,不需要被调用它的其他业务服务所感知。

换句话说,业务系统的其他服务在需要调用TCC服务时,根本不需要知道它是否为TCC型服务。因为,TCC服务能被其他业务服务调用的也仅仅是其Try业务,Confirm/Cancel业务是不能被其他业务服务直接调用的。

九、TCC服务A的Confirm/Cancel业务中能否调用它依赖的TCC服务B的Confirm/Cancel业务?

最好是不要这样做。首先,没有必要。TCC服务A依赖TCC服务B,那么[A:Try]已经将事务上下文传播给[B:Try]了,后续由TCC事务框架来调用各自的Confirm/Cancel业务即可;其次,Confirm/Cancel业务如果被允许调用其他服务,那么它就有可能再次发起新的TCC全局事务。如此递归下去,将会导致全局事务关系混乱且不可控。

TCC全局事务,应该尽量在Try操作阶段传播事务上下文。Confirm/Cancel操作阶段仅需要完成各自Try业务操作的确认操作/补偿操作即可,不适合再做远程调用,更不能

Blockquote

再对外传播事务上下文。

综上所述,本文倾向于认为,实现一个通用的TCC分布式事务管理框架,还是相对比较复杂的。一般业务系统如果需要使用TCC事务机制,并不推荐自行设计实现。
这里,给大家推荐一款开源的TCC分布式事务管理器ByteTCC。ByteTCC基于Try/Confirm/Cancel机制实现,可与Spring容器无缝集成,兼容Spring的声明式事务管理。提供对dubbo框架、Spring Cloud的开箱即用的支持,可满足多数据源、跨应用、跨服务器等各种分布式事务场景的需求。

Read More