关键词 | 概念 |
TCC | (Try-Confirm-Cancel)分布式事务是一种在分布式系统中实现强一致性的事务机制,它将事务操作分为三个阶段:
通过这三个阶段的处理,可以保证在分布式系统中的事务操作可以随时回滚或者提交,从而保证数据的一致性和可靠性。TCC分布式事务常用于需要跨多个服务的事务场景,例如电商订单支付等。 |
执行分布式事务的各方必须要实现以下三个接口。
接口 | 职责 |
Try接口 | 负责执行分布式事务前的准备,包括对事务的各方是否能成功执行该事务进行校验,以及在执行事务前进行准备工作,如将状态置为“准备”状态等。 该阶段不会执行任何业务逻辑,仅做业务的一致性检查和预留相应的资源,这些资源能够和其他操作保持隔离 |
Confirm接口 | 负责真正执行分布式事务,执行完不等待立即提交,以获得更好的系统并发。 通常情况下,采用TCC方案解决分布式事务时会认为Confirm阶段是不会出错的。也就是说,只要Try阶段的操作执行成功了,Confirm阶段就一定会执行成功。如果Confirm阶段出错了,就需要引入重试机制或人工处理,对出错的事务进行干预。 |
Cancel接口 | 在业务执行异常或出现错误的情况下,需要回滚事务的操作,执行分支事务的取消操作,并且释放Try阶段预留的资源。通常情况下,采用TCC方案解决分布式事务时,同样会认为Cancel阶段也是一定会执行成功的。如果Cancel阶段出错了,也需要引入重试机制或人工处理,对出错的事务进行干预。 |
有了这三个接口,现在我们来看看TCC是怎么执行分布式事务的。
首先,业务应用会发起一个事务。这时,业务应用会到事务协调器中注册一个事务,然后调用事务的各方执行Try接口。接着,各方执行Try接口去进行事务前的验证。如果其中一方失败了,就没有必要执行该事务了,因此业务应用会通知事务协调器注销该事务,事务以失败结束。如果各方都验证成功,那么业务应用就会通知事务协调器启动事务。
当事务启动以后,业务应用会在那里等待结果吗?绝对不会!在系统高并发场景下,必须要尽量减少系统等待,以便有效提升系统吞吐量。因此,事务启动以后,业务应用的任务就完成了,剩下的事情就交给事务协调器了,而它则应当去做其他的事情,从而获得极高的系统并发。
紧接着,执行分布式事务的任务就交给事务协调器。事务协调器首先调用事务各方的Confirm接口执行事务操作,采用“操作完不等待立即提交”的策略,杜绝了等待状态,从而获得了极大的系统并发。如果各方的执行都成功,则通知事务协调器,事务执行结束。
场景 | 原因 | 解决方案 |
空回滚 | 出现空回滚的原因是一个分支事务所在的服务器宕机或网络发生异常,此分支事务调用失败,此时并未执行此分支事务Try阶段的方法。当服务器或者网络恢复后,TCC分布式事务执行回滚操作,会调用分支事务Cancel阶段的方法,如果Cancel阶段的方法不能处理这种情况,就会出现空回滚问题。 | 识别是否出现了空回滚操作的方法是判断是否执行了Try阶段的方法。如果执行了Try阶段的方法,就没有空回滚,否则,就出现了空回滚。 具体解决方案是在主业务发起全局事务时,生成全局事务记录,并为全局事务记录生成一个全局唯一的ID,叫作全局事务ID。这个全局事务ID会贯穿整个分布式事务的执行流程。再创建一张分支事务记录表,用于记录分支事务,将全局事务ID和分支事务ID保存到分支事务表中。执行Try阶段的方法时,会向分支事务记录表中插入一条记录,其中包含全局事务ID和分支事务ID,表示执行了Try阶段。当事务回滚执行Cancel阶段的方法时,首先读取分支事务表中的数据,如果存在Try阶段插入的数据,则执行正常操作回滚事务,否则为空回滚,不做任何操作。 |
幂等问题 | 由于服务器宕机、应用崩溃或者网络异常等原因,可能会出现方法调用超时的情况,为了保证方法的正常执行,往往会在TCC方案中加入超时重试机制。因为超时重试有可能导致数据不一致的问题,所以需要保证分支事务的执行以及TCC方案的Confirm阶段和Cancel阶段具备幂等性。 | 解决方案是在分支事务记录表中增加事务的执行状态,每次执行分支事务以及Confirm阶段和Cancel阶段的方法时,都查询此事务的执行状态,以此判断事务的幂等性。 |
悬挂问题 时序错乱 |
在TCC分布式事务中,通过RPC调用分支事务Try阶段的方法时,会先注册分支事务,再执行RPC调用。如果此时发生服务器宕机、应用崩溃或者网络异常等情况,RPC调用就会超时。如果RPC调用超时,事务管理器会通知对应的资源管理器回滚事务。可能资源管理器回滚完事务后,RPC请求达到了参与分支事务所在的业务方法,因为此时事务已经回滚,所以在Try阶段预留的资源就无法释放了。这种情况,就称为悬挂。总之,悬挂问题就是预留业务资源后,无法继续往下处理。 | 解决方案的思路是如果执行了Confirm阶段或者Cancel阶段的方法,则Try阶段的方法就不能再执行。具体方案是在执行Try阶段的方法时,判断分支记录表中是否已经存在同一全局事务下Confirm阶段或者Cancel阶段的事务记录,如果存在,则不再执行Try阶段的方法。 |
优点 | 缺点 |
1、在应用层实现具体逻辑,锁定资源的粒度变小,不会锁定所有资源,提升了系统的性能 2、Confirm阶段和Cancel阶段的方法具备幂等性,能够保证分布式事务执行完毕后数据的一致性 3、TCC分布式事务解决方案由主业务发起整个事务,无论是主业务还是分支事务所在的业务,都能部署为集群模式,从而解决了XA规范的单点故障问题 |
代码需要耦合在具体业务中,每个参与分布式事务的业务方法都要拆分成Try、Confirm、Cancel三个阶段的方法,提高了开发成本。 |
这是不考虑通信异常的示例,考虑通信异常的代码将更加复杂
好的,以下是Java代码演示TCC分布式事务:
我们将假设有两个服务A和B,它们需要在同一个分布式事务中进行操作。
首先,我们定义一个接口来描述TCC事务的行为:
public interface TccTransaction {
void tryPhase();
void confirmPhase();
void cancelPhase();
}
在这个接口中,我们定义了三个方法tryPhase(尝试阶段)、confirmPhase(确认阶段)和cancelPhase(撤销阶段)。
接下来,我们定义服务A和服务B,并实现这个接口:
public class ServiceA implements TccTransaction {
private Connection conn;
public void tryPhase() {
// 在tryPhase方法中执行事务操作
try {
conn.setAutoCommit(false);
// 执行一些操作
// 预留业务资源
conn.commit();
} catch (SQLException e) {
try {
conn.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
throw new RuntimeException(e.getMessage());
} finally {
try {
conn.setAutoCommit(true);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
public void confirmPhase() {
// 在confirmPhase方法中提交事务
try {
//将业务预留资源真正执行落地
conn.commit();
} catch (SQLException e) {
throw new RuntimeException(e.getMessage());
}
}
public void cancelPhase() {
// 在cancelPhase方法中回滚事务
try {
//将业务预留资源释放
conn.rollback();
} catch (SQLException e) {
throw new RuntimeException(e.getMessage());
}
}
}
public class ServiceB implements TccTransaction {
private Connection conn;
public void tryPhase() {
// 在tryPhase方法中执行事务操作
try {
conn.setAutoCommit(false);
// 执行一些操作
// 预留业务资源
conn.commit();
} catch (SQLException e) {
try {
conn.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
throw new RuntimeException(e.getMessage());
} finally {
try {
conn.setAutoCommit(true);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
public void confirmPhase() {
// 在confirmPhase方法中提交事务
try {
//将业务预留资源真正执行落地
conn.commit();
} catch (SQLException e) {
throw new RuntimeException(e.getMessage());
}
}
public void cancelPhase() {
// 在cancelPhase方法中回滚事务
try {
//将业务预留资源释放
conn.rollback();
} catch (SQLException e) {
throw new RuntimeException(e.getMessage());
}
}
}
在这里,我们假设服务A和服务B都有一个数据库连接,并且在tryPhase方法中执行一个事务操作,例如向数据库中插入一条数据。在confirmPhase方法中,我们将该事务提交到数据库中,而在cancelPhase方法中,我们将该事务回滚。
现在,我们需要将服务A和服务B结合起来,并在一个分布式事务中运行它们:
public class TccTransactionCoordinator {
private List transactions;
public void registerTransaction(TccTransaction transaction) {
transactions.add(transaction);
}
public void execute() {
try {
// 执行tryPhase方法
transactions.forEach(TccTransaction::tryPhase);
// 执行confirmPhase方法
transactions.forEach(TccTransaction::confirmPhase);
} catch (Exception e) {
// 执行cancelPhase方法
transactions.forEach(TccTransaction::cancelPhase);
throw e;
}
}
}
在这个例子中,我们创建了一个TccTransactionCoordinator类,它有一个transactions列表,用于注册所有的TCC事务。execute方法是TCC事务的核心方法。在这个方法中,我们首先执行所有的tryPhase方法。如果所有的tryPhase方法都成功执行,我们将所有的TCC事务提交到数据库中。然后,我们执行所有的confirmPhase方法。如果有任何错误发生,我们将所有的TCC事务回滚,并执行所有的cancelPhase方法。
现在,我们可以创建一个分布式事务,将服务A和服务B一起注册到该事务中,并执行它:
public class Main {
public static void main(String[] args) {
ServiceA serviceA = new ServiceA();
ServiceB serviceB = new ServiceB();
TccTransactionCoordinator coordinator = new TccTransactionCoordinator();
coordinator.registerTransaction(serviceA);
coordinator.registerTransaction(serviceB);
coordinator.execute();
}
}
在这里,我们创建了一个TccTransactionCoordinator实例,并将服务A和服务B注册到该实例中。然后,我们调用execute方法,开始TCC事务。如果事务中的所有操作都成功执行,我们将获得一个提交的事务。否则,我们将得到一个回滚的事务。