分布式事务-从TCC到Saga

柔性事务是什么?

柔性事务(Flexible transaction),相对于刚性事务而言, 是一种相对于传统事务而言更加灵活和容错的事务处理模型。传统的 ACID 事务模型在保证数据的一致性和完整性方面表现出色,但是在分布式系统环境下容易出现性能瓶颈和单点故障等问题。柔性事务模型是一种分布式事务模型,它通过允许在某些条件下放宽事务的一致性要求,来提高系统的性能和可用性。TCC正是一种柔性事务。

TCC

什么是TCC?

TCC是“Try-Confirm-Cancel”(尝试-确认-取消)的缩写,是一种用于实现分布式事务的编程模型。TCC(Try-Confirm-Cancel)的概念来源于 Pat Helland 发表的一篇名为“Life beyond Distributed Transactions:an Apostate’s Opinion”的论文,文章中提出了 TCC 的概念,认为传统的 ACID 事务模型难以满足当今的分布式系统需求,而 TCC 可以通过细粒度的操作控制来提供更高的可用性。

TCC是如何工作的?

TCC模型通过将一个大的分布式事务拆分为多个小的本地事务,并在每个本地事务中实现“Try-Confirm-Cancel”三个步骤来保证事务的原子性。

事务开始时,业务应用会向事务协调器注册启动事务。之后业务应用会调用所有服务的try接口,相当于XA的第一阶段。如果有任何一个服务的try接口调用失败会向事务协调器发送事务回滚请求,否则发送事务提交请求。事务协调器收到事务回滚请求后会依次调用事务的confirm接口,否则调用cancel接口回滚,这相当于XA的第二阶段。如果第二阶段接口调用失败,会进行重试。

使用TCC的好处是什么?

TCC可以通过在分布式事务中使用本地事务来保证数据的一致性和原子性,同时可以避免全局锁和死锁等问题。此外,TCC还可以提高系统的可扩展性和可靠性,因为每个参与者节点都可以独立地执行本地事务,而不会受到其他节点的影响。

使用TCC可能存在哪些缺点?

使用TCC需要对系统进行较大的改造和重构,因为需要将原本的大型事务拆分为多个小的本地事务。此外,TCC还需要对业务逻辑进行较为复杂的设计和实现,业务逻辑的每个分支都需要实现 try、Confirm、Cancel 三个操作,并且 Confirm、Cancel 必须保证幂等,以保证所有的本地事务都能够正确地执行和协调。

TCC的使用场景有哪些?

TCC适用于需要执行跨多个参与者节点的分布式事务的场景,如电商平台的订单处理、分布式锁的管理、分布式缓存的更新等。

 

TCC代码

下面以一个银行转账扣款的例子来示范TCC的使用

// 1. 定义一个接口,包含 try、confirm 和 cancel 三个方法。
public interface BankTransferService {
    boolean tryTransfer(long fromAccount, long toAccount, double amount);
    void confirmTransfer(long fromAccount, long toAccount, double amount);
    void cancelTransfer(long fromAccount, long toAccount, double amount);
}

// 2.实现该接口,并在 try 方法中进行扣款和冻结金额的操作,在 confirm 方法中进行实际的转账操作,在 cancel 方法中进行资金的解冻和返还。
public class BankTransferServiceImpl implements BankTransferService {
    private AccountService accountService;
    private TransactionService transactionService;

    public boolean tryTransfer(long fromAccount, long toAccount, double amount) {
        Account from = accountService.getAccountById(fromAccount);
        Account to = accountService.getAccountById(toAccount);
        Transaction tx = new Transaction(from, to, amount);
        transactionService.createTransaction(tx);

        // 冻结金额
        boolean success = accountService.freezeAmount(fromAccount, amount);
        if (!success) {
            transactionService.deleteTransaction(tx);
            return false;
        }
        return true;
    }

    public void confirmTransfer(long fromAccount, long toAccount, double amount) {
        // 转账操作
        accountService.transferAmount(fromAccount, toAccount, amount);
        transactionService.completeTransaction(fromAccount, toAccount, amount);
    }

    public void cancelTransfer(long fromAccount, long toAccount, double amount) {
        // 解冻并返还金额
        accountService.unfreezeAmount(fromAccount, amount);
        transactionService.deleteTransaction(fromAccount, toAccount, amount);
    }
}

// 3. 在转账服务中,定义一个 try、confirm 和 cancel 的执行器,将其注入到 TCC 事务管理器中。
public class BankTransferServiceExecutor {
    private BankTransferService service;

    @TccTry
    public boolean tryTransfer(long fromAccount, long toAccount, double amount) {
        return service.tryTransfer(fromAccount, toAccount, amount);
    }

    @TccConfirm
    public void confirmTransfer(long fromAccount, long toAccount, double amount) {
        service.confirmTransfer(fromAccount, toAccount, amount);
    }

    @TccCancel
    public void cancelTransfer(long fromAccount, long toAccount, double amount) {
        service.cancelTransfer(fromAccount, toAccount, amount);
    }

    public void setService(BankTransferService service) {
        this.service = service;
    }
}

public class BankTransferExample {
    public static void main() {
        TccTransactionManager tm = new TccTransactionManager();
        BankTransferServiceImpl service = new BankTransferServiceImpl();
        BankTransferServiceExecutor executor = new BankTransferServiceExecutor();
        executor.setService(service);
        tm.registerTccParticipant(executor);
        // 调用转账服务
        boolean result = tm.executeTccTransaction(() -> {
            boolean success = executor.tryTransfer(fromAccount, toAccount, amount);
            if (success) {
                executor.confirmTransfer(fromAccount, toAccount, amount);
            } else {
                executor.cancelTransfer(fromAccount, toAccount, amount);
            }
            return success;
        });
        // ...
    }    
}

如上给出了一个银行转账的例子,由事务管理器tm判断执行器的执行结果,如果成功则提交,否则利用执行器执行回滚。真正的具体服务提供了三个接口,为try,确认和取消。

 

2PC协议

TCC是一种应用层的分布式事务处理模式,而不是一个具体的协议。而分布式事务的实现方式有多种,其中最具有代表性的是由Oracle 的XA分布式事务协议。XA协议包括两阶段提交(2PC)和三阶段提交(3PC)两种实现。

2PC(two phase commit)两阶段提交[5] 顾名思义它分成两个阶段,先由一方进行提议并收集其他节点的反馈(vote),再根据反馈决定提交或中止事务。我们将提议的节点称为协调者(coordinator),其他参与决议节点称为参与者(participants, 或cohorts)。

阶段1为准备阶段,coordinator发起一个提议,分别问询各参与者是否接受。

阶段2为提交阶段,coordinator根据参与者的反馈,提交或中止事务,如果参与者全部同意则提交,只要有一个参与者不同意就中止。

 

Q: 在2PC中,准备阶段理应全部同意,为什么参与者可能反对提交?

A: 在代码层面,当参与者接收到协调者发来的 prepare 请求后,需要执行本地事务,并将执行结果存储在某个内部数据结构中。如果在执行本地事务时发生了错误,参与者会将内部状态设置为“反对提交”,并向协调者发送反对提交的消息。如果本地事务执行成功,参与者会将内部状态设置为“同意提交”,并向协调者发送同意提交的消息。

3PC协议

2PC存在的问题

性能问题。执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态,系统长时间处于不确定状态。

单点故障。由于协调者的重要性,一旦协调者发生故障,参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)

数据不一致。在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这会导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据不一致性的现象。

二阶段无法解决的问题:协调者再发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。

 

3PC能够缓解这些问题。

3PC(Three-phase commit protocol,三阶段提交协议)最早是由 M. Raynal 在 1986 年提出的,目的是为了解决 2PC 协议可能出现的单点故障和阻塞问题。3PC 协议引入了一个预提交阶段,可以避免阻塞和不一致的问题,因此被广泛应用于一些对数据一致性要求较高的场景中。

CanCommit 阶段:参与者向协调者发送 CanCommit 请求,询问是否可以提交事务,如果协调者在这个阶段收到了所有参与者的 CanCommit 请求,且认为可以提交事务,则向参与者发送 DoCommit 消息,否则向参与者发送 DoAbort 消息。

PreCommit 阶段:参与者在收到协调者的 DoCommit 或 DoAbort 消息后,需要在本地执行相应的操作,并向协调者发送 Ack 消息,表示已经准备好提交或者撤销事务。

DoCommit/DoAbort 阶段:如果协调者在 CanCommit 阶段收到了所有参与者的 CanCommit 请求,并认为可以提交事务,则向参与者发送 DoCommit 消息,参与者在收到 DoCommit 消息后,需要在本地提交事务并向协调者发送 Ack 消息;如果协调者在 CanCommit 阶段收到了任何一个参与者的 CanCommit 请求失败或者在 PreCommit 阶段收到了任何一个参与者的 Ack 失败,则向所有参与者发送 DoAbort 消息,参与者需要撤销本地事务并向协调者发送 Ack 消息。

图片截取自wikipedia


注:不一致的问题是指:如果提交的副本与coordinator一起崩溃,系统将无法判断事务的结果是什么(因为只有coordinator和收到消息的参与者才能确定)。此时参与者无法知道其对应的数据。

XA/2PC和TCC

XA 协议能够保证分布式事务的原子性和持久性。XA 协议在架构上与TCC 模型相比,最大的不同是XA 直接作用于资源层,而后者作用于服务层。

TCC 模型更加灵活,但是需要开发者在业务逻辑中加入特定的 TCC 代码来实现柔性事务。根据具体业务场景和需求,可以选择合适的分布式事务实现方式。

SAGA和TCC

TCC 更适合于业务场景简单、数据量小的场景,例如积分兑换、红包领取等等。为了应对一些其他的场景,要使用其他的模型。

Hector & Kenneth发表了论文《sagas》,提出了一种基于有限状态机的分布式事务协议,其核心思想是将一个分布式事务拆分成多个可撤销的本地事务,并且在事务执行过程中,记录当前事务的状态,并在需要回滚时通过状态机执行相应的补偿事务。SAGA 非常适用于长时间运行的、异步的、事件驱动的业务场景,例如电商交易、机票预定等等。SAGA 常用的实现方式有基于协同(Choreography-based) 和基于编排(Orchestration-based )两种模式,其中 Orchestration-based 是最常见的模式。

在基于协同的 Saga 中,事务中涉及的每个服务都直接与其他服务通信以协调事务的进度,而不是依赖集中式协调器来管理事务的流程。 每个服务都知道自己在事务中的角色,并通过事件或消息与其他服务进行通信以推进事务。它是一种去中心化的事务模型。

而基于编排的Saga中,使用一个中心化的调度器或协调器来管理事务的流程。

 

实践:Seata框架

Seata:由阿里巴巴开源的分布式事务解决方案,提供了 TCC、AT 和 Saga 等多种事务模式的支持。

AT(Auto Transaction)模式的一阶段、二阶段提交和回滚均由 Seata 框架自动生成,用户只需编写“业务 SQL”,便能无缝接入分布式事务,AT 模式是一种对业务无任何侵入的分布式事务解决方案。但AT模式存在的不足就是当操作的数据是共享型数据,会存在脏写的问题,也就是说两个事务可能同时修改了一份数据并且没有任何互斥操作保证事务的原子性,因此仅适用于用户独有数据。

使用Seata:

Seata分TC、TM和RM三个角色,TC(Server端)为单独服务端部署,TM和RM(Client端)由业务系统集成。

安装Seata服务器

下载Seata服务器软件包:
访问Seata GitHub仓库的发布页面:https://github.com/seata/seata/releases下载最新版本的Seata服务器软件包。

2.解压后修改file.conf,
TCC模式不需要创建undolog表
3.在目录下找到seta-server.sh并执行sh seata-server.sh,启动seata-server.sh
4.在代码中使用seata的客户端,即 编写TM和RM的Try、Confirm、Cancel接口

分布式事务的适用性

能够用业务规避的就用业务规避,可以不用分布式事务的就不用,因为会增加系统的复杂度

Reference

XA规范与TCC事务模型 https://cloud.tencent.com/developer/article/1627837

分布式系统理论基础 - 一致性、2PC和3PC https://zhuanlan.zhihu.com/p/21994882

分布式事务框架和事务模式 https://help.aliyun.com/document_detail/157850.html

分布式事务(二)两阶段提交和三阶段提交https://juejin.cn/post/7028930388106051598

Saga distributed transactions pattern  https://learn.microsoft.com/en-us/azure/architecture/reference-architectures/saga/saga

seata-tcc-demo https://github.com/Kerry2019/seata-tcc-demo

手把手教你Spring Cloud集成Seata TCC模式 https://juejin.cn/post/7153214568133558279

Seata部署指南 https://seata.io/zh-cn/docs/ops/deploy-guide-beginner.html

posted @ 2021-12-31 21:33  stackupdown  阅读(56)  评论(0编辑  收藏  举报