MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Saga 模式与 TCC 模式的对比与选择

2021-09-126.8k 阅读

分布式事务概述

在分布式系统中,由于服务的拆分,一个业务操作往往会涉及多个服务的交互。这些服务可能分布在不同的节点上,使用不同的数据库,此时如何保证这些跨服务操作的原子性,即要么全部成功,要么全部失败,就成为了一个关键问题,这就是分布式事务要解决的核心。

传统的单机事务通过数据库的 ACID(原子性、一致性、隔离性、持久性)特性可以轻松保证事务的完整性。但在分布式环境下,由于网络延迟、节点故障等不确定性因素,实现类似单机事务的效果变得复杂得多。常见的分布式事务解决方案包括 XA 协议、Saga 模式、TCC 模式等。本文着重对比 Saga 模式与 TCC 模式,帮助开发者在实际应用中做出更合适的选择。

Saga 模式

Saga 模式的概念

Saga 模式由 Hector Garcia - Molina 和 Kenneth Salem 在 1987 年发表的论文 “Sagas” 中提出。其核心思想是将一个长事务拆分成多个本地短事务,每个本地短事务都有对应的补偿事务。当其中某个本地事务失败时,系统会按照相反的顺序调用已执行成功的本地事务的补偿事务,以达到事务回滚的目的,从而保证整个业务操作的一致性。

例如,一个电商系统中的订单创建业务,可能涉及创建订单、扣减库存、冻结用户账户金额等操作。在 Saga 模式下,这每一步都可以看作是一个本地事务,并且都有对应的补偿事务,如取消订单、恢复库存、解冻用户账户金额。

Saga 模式的执行流程

  1. 正向执行:按照预定的顺序依次执行各个本地事务。例如,在上述电商订单创建场景中,首先创建订单,接着扣减库存,最后冻结用户账户金额。
  2. 异常处理:如果在执行过程中某个本地事务失败,比如扣减库存失败,系统会从失败点开始,反向依次调用已成功执行的本地事务的补偿事务。即先解冻用户账户金额,再恢复库存。

Saga 模式的实现方式

  1. 编排式(Choreography - based):在编排式 Saga 中,各个参与服务之间通过消息进行异步通信,每个服务根据接收到的消息决定自己的下一步操作。这种方式下,没有一个中央协调者,所有服务之间的交互逻辑分散在各个服务内部。例如,订单服务创建订单成功后,发送一条消息给库存服务,库存服务接收到消息后进行库存扣减操作,扣减成功后再发送消息给账户服务进行金额冻结。如果库存扣减失败,库存服务会发送补偿消息给订单服务,订单服务接收到后取消订单。
  2. 集中式(Orchestration - based):集中式 Saga 有一个中央协调器,负责协调各个本地事务的执行顺序。协调器知道整个业务流程,它向各个服务发送指令,告诉它们何时执行本地事务以及何时执行补偿事务。例如,在电商订单创建场景中,中央协调器首先通知订单服务创建订单,订单服务返回成功后,协调器再通知库存服务扣减库存,以此类推。如果某个环节失败,协调器会根据预先定义的规则,通知相应服务执行补偿事务。

Saga 模式代码示例

以 Python 和 RabbitMQ 为例,实现一个简单的编排式 Saga。假设我们有订单服务、库存服务和账户服务。

  1. 订单服务
import pika

# 连接 RabbitMQ
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

# 声明队列
channel.queue_declare(queue='order_queue')
channel.queue_declare(queue='order_compensation_queue')


def create_order():
    print("订单创建成功")
    # 发送消息给库存服务
    channel.basic_publish(exchange='', routing_key='stock_queue', body='create_order_success')


def cancel_order():
    print("订单取消成功")


# 接收补偿消息
def receive_compensation(ch, method, properties, body):
    if body.decode() == 'stock_fail':
        cancel_order()


channel.basic_consume(queue='order_compensation_queue', on_message_callback=receive_compensation, auto_ack=True)
  1. 库存服务
import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

channel.queue_declare(queue='stock_queue')
channel.queue_declare(queue='stock_compensation_queue')


def deduct_stock():
    print("库存扣减成功")
    # 发送消息给账户服务
    channel.basic_publish(exchange='', routing_key='account_queue', body='deduct_stock_success')


def restore_stock():
    print("库存恢复成功")


# 接收订单服务消息
def receive_order_message(ch, method, properties, body):
    if body.decode() == 'create_order_success':
        try:
            deduct_stock()
        except Exception:
            # 发送补偿消息给订单服务
            channel.basic_publish(exchange='', routing_key='order_compensation_queue', body='stock_fail')
            restore_stock()


# 接收账户服务补偿消息
def receive_account_compensation(ch, method, properties, body):
    if body.decode() == 'freeze_failure':
        restore_stock()
        # 发送补偿消息给订单服务
        channel.basic_publish(exchange='', routing_key='order_compensation_queue', body='stock_fail')


channel.basic_consume(queue='stock_queue', on_message_callback=receive_order_message, auto_ack=True)
channel.basic_consume(queue='stock_compensation_queue', on_message_callback=receive_account_compensation, auto_ack=True)
  1. 账户服务
import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

channel.queue_declare(queue='account_queue')
channel.queue_declare(queue='account_compensation_queue')


def freeze_amount():
    print("账户金额冻结成功")


def unfreeze_amount():
    print("账户金额解冻成功")
    # 发送补偿消息给库存服务
    channel.basic_publish(exchange='', routing_key='stock_compensation_queue', body='freeze_failure')


# 接收库存服务消息
def receive_stock_message(ch, method, properties, body):
    if body.decode() == 'deduct_stock_success':
        try:
            freeze_amount()
        except Exception:
            unfreeze_amount()


channel.basic_consume(queue='account_queue', on_message_callback=receive_stock_message, auto_ack=True)

TCC 模式

TCC 模式的概念

TCC 即 Try - Confirm - Cancel,由 Pat Helland 在 2007 年发表的论文 “Life beyond Distributed Transactions: an Apostate’s Opinion” 中提出。TCC 模式将事务处理过程分为三个阶段:

  1. Try 阶段:尝试执行业务,完成所有业务检查(一致性),预留业务资源(准隔离性)。例如,在电商订单创建场景中,Try 阶段可以检查库存是否足够、用户账户余额是否充足,并冻结相应的库存和金额。
  2. Confirm 阶段:确认执行业务,真正提交业务操作。如果 Try 阶段所有操作都成功,Confirm 阶段就会正式扣减库存、扣除用户账户金额。
  3. Cancel 阶段:取消执行业务,释放 Try 阶段预留的业务资源。如果 Try 阶段某个操作失败,Cancel 阶段就会解冻库存和金额。

TCC 模式的执行流程

  1. Try 阶段:各个参与服务依次执行 Try 操作。例如,订单服务在 Try 阶段创建订单记录并标记为待确认状态,库存服务检查并冻结库存,账户服务检查并冻结用户账户金额。
  2. Confirm 阶段:如果所有服务的 Try 操作都成功,协调器会通知各个服务执行 Confirm 操作。订单服务将订单状态更新为已确认,库存服务正式扣减库存,账户服务正式扣除用户账户金额。
  3. Cancel 阶段:如果有任何一个服务的 Try 操作失败,协调器会通知所有已执行 Try 操作的服务执行 Cancel 操作。订单服务删除待确认的订单记录,库存服务解冻库存,账户服务解冻用户账户金额。

TCC 模式的实现方式

  1. 业务代码实现:开发者需要在业务代码中实现 Try、Confirm 和 Cancel 三个方法。例如,在库存服务中,需要编写 tryDeductStock、confirmDeductStock 和 cancelDeductStock 方法。
  2. 事务协调器:可以使用一些开源框架如 Seata 来实现事务协调器的功能。Seata 能够管理分布式事务的全局状态,协调各个服务的 Try、Confirm 和 Cancel 操作。

TCC 模式代码示例

以 Java 和 Spring Boot 为例,使用 Seata 框架实现一个简单的 TCC 模式。假设我们有订单服务、库存服务和账户服务。

  1. 库存服务
import io.seata.spring.annotation.GlobalLock;
import io.seata.spring.annotation.TccAction;
import org.springframework.stereotype.Service;

@Service
public class StockService {

    @TccAction(name = "stockTccAction", confirmMethod = "confirmDeductStock", cancelMethod = "cancelDeductStock")
    @GlobalLock
    public boolean tryDeductStock(String productId, int quantity) {
        // 检查库存并冻结
        System.out.println("Try 阶段:检查并冻结库存,商品ID:" + productId + ",数量:" + quantity);
        return true;
    }

    public boolean confirmDeductStock(String productId, int quantity) {
        // 正式扣减库存
        System.out.println("Confirm 阶段:正式扣减库存,商品ID:" + productId + ",数量:" + quantity);
        return true;
    }

    public boolean cancelDeductStock(String productId, int quantity) {
        // 解冻库存
        System.out.println("Cancel 阶段:解冻库存,商品ID:" + productId + ",数量:" + quantity);
        return true;
    }
}
  1. 账户服务
import io.seata.spring.annotation.GlobalLock;
import io.seata.spring.annotation.TccAction;
import org.springframework.stereotype.Service;

@Service
public class AccountService {

    @TccAction(name = "accountTccAction", confirmMethod = "confirmFreezeAmount", cancelMethod = "cancelFreezeAmount")
    @GlobalLock
    public boolean tryFreezeAmount(String userId, double amount) {
        // 检查账户余额并冻结
        System.out.println("Try 阶段:检查并冻结账户金额,用户ID:" + userId + ",金额:" + amount);
        return true;
    }

    public boolean confirmFreezeAmount(String userId, double amount) {
        // 正式扣除账户金额
        System.out.println("Confirm 阶段:正式扣除账户金额,用户ID:" + userId + ",金额:" + amount);
        return true;
    }

    public boolean cancelFreezeAmount(String userId, double amount) {
        // 解冻账户金额
        System.out.println("Cancel 阶段:解冻账户金额,用户ID:" + userId + ",金额:" + amount);
        return true;
    }
}
  1. 订单服务
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderService {

    private final StockService stockService;
    private final AccountService accountService;

    public OrderService(StockService stockService, AccountService accountService) {
        this.stockService = stockService;
        this.accountService = accountService;
    }

    @GlobalTransactional
    @Transactional
    public void createOrder(String userId, String productId, int quantity, double amount) {
        // 创建订单记录
        System.out.println("创建订单记录,用户ID:" + userId + ",商品ID:" + productId + ",数量:" + quantity + ",金额:" + amount);

        boolean stockResult = stockService.tryDeductStock(productId, quantity);
        boolean accountResult = accountService.tryFreezeAmount(userId, amount);

        if (!stockResult ||!accountResult) {
            throw new RuntimeException("Try 阶段失败");
        }
    }
}

Saga 模式与 TCC 模式的对比

一致性保证

  1. Saga 模式:通过补偿事务来保证最终一致性。在 Saga 执行过程中,如果某个本地事务失败,系统通过反向执行补偿事务来撤销之前已执行成功的本地事务的影响。由于各个本地事务是异步执行的,在事务执行过程中可能会出现短暂的不一致状态,但最终会达到一致。例如,在电商订单创建场景中,当库存扣减失败后,订单取消和库存恢复可能会有一定的时间差,在这个时间差内,数据处于不一致状态。
  2. TCC 模式:Try 阶段完成业务检查和资源预留,保证了数据的一致性。Confirm 阶段如果所有 Try 操作都成功,则正式提交业务操作,进一步保证一致性。Cancel 阶段在 Try 操作失败时释放预留资源,避免数据不一致。TCC 模式在整个事务执行过程中,数据的一致性相对更容易保证,因为它是一种强一致性模型,在事务结束时,数据一定是一致的。

性能

  1. Saga 模式:由于 Saga 模式采用异步消息通信,各个本地事务可以并行执行,在高并发场景下具有较好的性能表现。例如,在电商订单创建场景中,订单创建、库存扣减和账户金额冻结可以通过消息异步触发,不需要等待前一个操作完成再执行下一个操作,从而提高了系统的吞吐量。
  2. TCC 模式:TCC 模式在 Try 阶段需要进行资源预留,这可能会导致资源长时间被锁定,影响系统的并发性能。在高并发场景下,大量的资源预留可能会导致资源争用,从而降低系统的吞吐量。例如,在库存服务中,Try 阶段冻结库存,如果有大量并发请求,可能会导致库存长时间被冻结,其他请求无法获取库存。

复杂性

  1. Saga 模式:编排式 Saga 的实现相对复杂,因为各个服务之间的交互逻辑分散在各个服务内部,需要开发者仔细设计和维护消息的发送和接收逻辑。集中式 Saga 虽然有中央协调器,但协调器的实现也需要一定的工作量,并且协调器可能成为系统的单点故障。此外,Saga 模式的事务回滚依赖于补偿事务的正确实现,如果补偿事务出现问题,可能会导致数据不一致。
  2. TCC 模式:TCC 模式需要开发者在业务代码中实现 Try、Confirm 和 Cancel 三个方法,增加了业务代码的复杂性。同时,TCC 模式依赖于事务协调器,如 Seata 框架,框架的配置和使用也需要一定的学习成本。而且,TCC 模式对业务侵入性较大,因为它需要业务代码紧密配合来实现事务控制。

适用场景

  1. Saga 模式:适用于业务流程较长、涉及多个服务且对最终一致性要求较高的场景。例如,电商的订单创建、物流配送、售后等复杂业务流程。由于这些业务流程通常具有异步性和松耦合性,Saga 模式的异步消息通信和补偿机制能够很好地适应这种需求。
  2. TCC 模式:适用于对数据一致性要求较高、业务流程相对较短且对性能要求不是特别高的场景。例如,银行转账、支付等场景,这些场景对数据的准确性和一致性要求极高,TCC 模式的强一致性保证能够满足这种需求。

Saga 模式与 TCC 模式的选择

在选择 Saga 模式还是 TCC 模式时,开发者需要综合考虑以下因素:

  1. 业务需求:如果业务流程复杂、涉及多个服务且允许一定时间内的数据不一致,Saga 模式可能更适合。例如,在一个大型电商平台的订单处理流程中,订单创建、库存管理、物流配送等环节可以使用 Saga 模式,通过异步消息通信和补偿事务来保证最终一致性。如果业务对数据一致性要求极高,如金融领域的交易操作,TCC 模式更能满足需求,它能够在事务结束时确保数据的一致性。
  2. 性能要求:在高并发场景下,如果系统对吞吐量要求较高,Saga 模式的异步执行特性能够提高系统的并发处理能力。而 TCC 模式由于资源预留可能会导致性能瓶颈,不太适合高并发且对性能要求苛刻的场景。
  3. 开发成本:Saga 模式无论是编排式还是集中式,都需要处理消息通信和补偿事务,开发和维护成本相对较高。TCC 模式虽然业务侵入性大,但如果开发者对相关框架如 Seata 熟悉,并且业务逻辑相对简单,TCC 模式的开发成本可能相对较低。

综上所述,Saga 模式和 TCC 模式各有优缺点,在实际应用中,开发者需要根据具体的业务场景、性能要求和开发成本等因素,权衡利弊,选择最适合的分布式事务解决方案。在一些复杂的分布式系统中,也可能会结合使用这两种模式,以充分发挥它们的优势。例如,在一个电商系统中,订单创建和支付环节可以使用 TCC 模式保证数据一致性,而订单的后续物流和售后流程可以使用 Saga 模式,以适应异步和复杂的业务流程。