在当今的软件开发领域,分布式系统已经成为了主流。随着业务的不断发展和复杂化,系统被拆分成多个微服务,这虽然带来了高可扩展性和灵活性,但也引入了新的挑战,其中分布式事务处理就是一个关键问题。接下来,我们就来详细聊聊几种常见的 Java 分布式事务处理方案,包括 Seata AT 模式、TCC 模式与本地消息表方案。

一、Seata AT 模式

应用场景

Seata AT 模式适用于对性能要求较高,且业务逻辑相对简单,数据一致性要求较高的场景。比如说电商系统中的订单支付流程,当用户下单并完成支付后,需要同时更新订单状态和扣减库存。在这个过程中,订单服务和库存服务可能是不同的微服务,就需要保证这两个操作要么都成功,要么都失败,Seata AT 模式就可以很好地解决这个问题。

技术原理

Seata AT 模式是一种无侵入的分布式事务解决方案。它的核心原理是在业务 SQL 执行前后,自动生成相应的回滚日志,并在事务提交或回滚时,根据这些日志进行相应的操作。具体来说,它分为两个阶段:

  • 一阶段:业务 SQL 执行前,Seata 会对业务 SQL 涉及的所有数据进行快照保存,然后执行业务 SQL。在这个过程中,Seata 会自动开启一个本地事务,保证业务 SQL 的原子性。
  • 二阶段:如果一阶段所有业务 SQL 都执行成功,Seata 会直接提交事务,释放相应的锁;如果有任何一个业务 SQL 执行失败,Seata 会根据之前保存的快照进行回滚操作。

示例代码(基于 Spring Boot 和 Seata)

import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderService {

    @Autowired
    private OrderDao orderDao;

    @Autowired
    private InventoryService inventoryService;

    // 开启全局事务
    @GlobalTransactional
    @Transactional
    public void createOrder(Order order) {
        // 插入订单记录
        orderDao.insertOrder(order);

        // 扣减库存
        inventoryService.reduceInventory(order.getProductId(), order.getQuantity());
    }
}

在这个示例中,@GlobalTransactional 注解用于开启全局事务,@Transactional 注解用于开启本地事务。当 createOrder 方法被调用时,Seata 会自动管理整个分布式事务的流程。

优缺点

  • 优点
    • 无侵入性,对业务代码的改动较小,只需要添加相应的注解即可。
    • 性能较高,一阶段直接提交本地事务,减少了锁的持有时间。
  • 缺点
    • 依赖数据库的支持,需要数据库支持 SQL 解析和回滚日志的生成。
    • 对于复杂的业务逻辑,可能会生成大量的回滚日志,增加数据库的负担。

注意事项

  • 数据库需要支持 SQL 解析,如 MySQL 等。
  • 要确保 Seata 服务的高可用性,避免因为 Seata 服务故障导致分布式事务失败。

二、TCC 模式

应用场景

TCC 模式适用于对性能要求较高,业务逻辑复杂,且需要对资源进行精细控制的场景。比如金融系统中的资金转账业务,在转账过程中,需要先冻结转出账户的资金,然后进行资金的转移,最后解冻转出账户的资金。这个过程涉及到多个资源的操作,TCC 模式可以很好地满足这种需求。

技术原理

TCC 模式分为三个阶段:

  • Try:尝试执行业务,完成所有业务检查,预留必须的业务资源。
  • Confirm:确认执行业务,使用 Try 阶段预留的业务资源。
  • Cancel:取消执行业务,释放 Try 阶段预留的业务资源。

示例代码(基于 Spring Boot)

import org.springframework.stereotype.Service;

@Service
public class AccountService {

    // Try 阶段:冻结账户资金
    public boolean tryFreezeAmount(String accountId, double amount) {
        // 检查账户余额是否充足
        if (checkBalance(accountId, amount)) {
            // 冻结资金
            freezeAmount(accountId, amount);
            return true;
        }
        return false;
    }

    // Confirm 阶段:确认资金转移
    public void confirmTransfer(String accountId, double amount) {
        // 扣除冻结的资金
        deductAmount(accountId, amount);
    }

    // Cancel 阶段:取消资金转移,解冻资金
    public void cancelTransfer(String accountId, double amount) {
        // 解冻资金
        unfreezeAmount(accountId, amount);
    }

    private boolean checkBalance(String accountId, double amount) {
        // 检查账户余额逻辑
        return true;
    }

    private void freezeAmount(String accountId, double amount) {
        // 冻结资金逻辑
    }

    private void deductAmount(String accountId, double amount) {
        // 扣除资金逻辑
    }

    private void unfreezeAmount(String accountId, double amount) {
        // 解冻资金逻辑
    }
}

在这个示例中,tryFreezeAmount 方法对应 Try 阶段,confirmTransfer 方法对应 Confirm 阶段,cancelTransfer 方法对应 Cancel 阶段。

优缺点

  • 优点
    • 对业务的控制粒度较高,可以根据业务需求灵活实现各个阶段的逻辑。
    • 性能较高,不需要像 2PC 那样长时间持有锁。
  • 缺点
    • 开发成本较高,需要手动实现 Try、Confirm 和 Cancel 三个阶段的逻辑。
    • 代码耦合度较高,各个阶段的逻辑需要紧密配合。

注意事项

  • 要确保 Try、Confirm 和 Cancel 方法的幂等性,避免因为重试导致数据不一致。
  • 各个阶段的逻辑要尽可能简单,避免出现复杂的业务逻辑。

三、本地消息表方案

应用场景

本地消息表方案适用于对数据一致性要求相对较低,且需要异步处理业务的场景。比如电商系统中的订单通知业务,当订单创建成功后,需要发送消息通知用户,这个过程可以使用本地消息表方案进行异步处理。

技术原理

本地消息表方案的核心思想是将消息的发送和业务操作放在同一个本地事务中,保证消息的发送和业务操作的原子性。具体步骤如下:

  1. 在业务数据库中创建一个本地消息表。
  2. 当业务操作执行时,将消息记录插入到本地消息表中。
  3. 启动一个消息发送任务,定时从本地消息表中读取未发送的消息,并发送到消息队列中。
  4. 消费方从消息队列中接收消息,并处理相应的业务逻辑。

示例代码(基于 Spring Boot 和 RabbitMQ)

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.UUID;

@Service
public class OrderService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Transactional
    public void createOrder(Order order) {
        // 插入订单记录
        jdbcTemplate.update("INSERT INTO orders (order_id, product_id, quantity) VALUES (?,?,?)",
                order.getOrderId(), order.getProductId(), order.getQuantity());

        // 插入消息记录
        String messageId = UUID.randomUUID().toString();
        jdbcTemplate.update("INSERT INTO local_message (message_id, content, status) VALUES (?,?,?)",
                messageId, "订单创建成功:" + order.getOrderId(), "UN_SENT");

        // 发送消息
        sendMessage(messageId);
    }

    private void sendMessage(String messageId) {
        // 从本地消息表中查询消息内容
        String content = jdbcTemplate.queryForObject("SELECT content FROM local_message WHERE message_id =?",
                new Object[]{messageId}, String.class);

        // 发送消息到 RabbitMQ
        rabbitTemplate.convertAndSend("order_exchange", "order_routing_key", content);

        // 更新消息状态为已发送
        jdbcTemplate.update("UPDATE local_message SET status = 'SENT' WHERE message_id =?", messageId);
    }
}

在这个示例中,createOrder 方法将订单插入和消息插入操作放在同一个本地事务中,保证了原子性。然后通过 sendMessage 方法将消息发送到 RabbitMQ 中,并更新消息状态。

优缺点

  • 优点
    • 实现简单,不需要依赖第三方的分布式事务框架。
    • 对业务代码的侵入性较小,只需要在业务代码中添加消息插入和发送的逻辑。
  • 缺点
    • 消息发送的实时性较差,需要定时任务来发送消息。
    • 可能会出现消息丢失的情况,需要有相应的重试机制。

注意事项

  • 要确保消息的幂等性,避免因为重试导致业务重复处理。
  • 定时任务的执行频率要根据业务需求进行合理设置。

四、总结

Seata AT 模式、TCC 模式和本地消息表方案各有优缺点,在实际应用中,需要根据具体的业务场景来选择合适的方案。

  • 如果业务逻辑简单,对性能要求较高,且数据一致性要求较高,可以选择 Seata AT 模式。
  • 如果业务逻辑复杂,需要对资源进行精细控制,可以选择 TCC 模式。
  • 如果对数据一致性要求相对较低,且需要异步处理业务,可以选择本地消息表方案。

同时,在使用这些方案时,要注意相应的注意事项,确保分布式事务的可靠性和稳定性。