使用RocketMQ的本地消息表+事务消息/普通消息方案实现分布式事务


使用RocketMQ的本地消息表+事务消息方案实现分布式事务

在分布式系统中,保证数据一致性是一个核心问题。RocketMQ 作为一款高性能、高可靠的消息中间件,提供了事务消息功能,可以很好地结合本地消息表来解决分布式事务的挑战。本文将详细讲解如何使用 RocketMQ 的本地消息表和事务消息方案,确保分布式系统中的数据一致性。

一、背景与问题

在分布式架构中,跨服务的数据操作通常涉及多个子系统。例如,在电商系统中,下单操作可能需要同时扣减库存、生成订单、扣款等。如果这些操作分布在不同的服务中,单纯依靠数据库事务无法保证一致性。这时,分布式事务的解决方案就显得尤为重要。

RocketMQ 的事务消息提供了一种基于消息的最终一致性方案,而本地消息表则是一种经典的分布式事务模式。两者结合,可以在性能和可靠性之间找到平衡。

二、本地消息表 + 事务消息的基本原理

1. 本地消息表的核心思想

本地消息表是一种基于数据库的分布式事务方案。其核心是将需要分布式执行的操作记录到本地数据库的消息表中,然后通过异步机制(如消息队列)将任务分发给下游系统。

2. RocketMQ 事务消息的工作机制

RocketMQ 的事务消息分为两个阶段:

  • 半消息(Half Message):Producer 先发送一个"半消息"到 Broker,Broker 接收后不会立即投递给 Consumer,而是等待事务状态确认。
  • 事务状态提交:Producer 执行本地事务后,向 Broker 提交 Commit 或 Rollback 状态。如果是 Commit,Broker 投递消息给 Consumer;如果是 Rollback,消息被丢弃。

结合本地消息表和 RocketMQ 事务消息,我们可以在本地事务中记录消息,并利用 RocketMQ 的事务机制确保消息的可靠投递。

三、实现方案

下面是一个典型的使用 RocketMQ 的本地消息表 + 事务消息的实现流程:

1. 系统架构

假设我们有一个电商系统,下单操作需要:

  • 扣减库存(库存服务)
  • 生成订单(订单服务)
  • 发送通知(通知服务)

我们将使用 RocketMQ 协调这些操作。

2. 数据库表设计

在订单服务中,创建一个本地消息表 message_log,用于记录消息状态:

sql 复制代码
CREATE TABLE message_log (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    order_id BIGINT NOT NULL,           -- 订单ID
    message_content TEXT NOT NULL,      -- 消息内容(JSON格式)
    status ENUM('INIT', 'SENT', 'FAIL') DEFAULT 'INIT', -- 消息状态
    create_time DATETIME NOT NULL,      -- 创建时间
    update_time DATETIME                -- 更新时间
);

3. 代码实现

(1) Producer 端:发送事务消息

在订单服务中,使用 RocketMQ 的 TransactionMQProducer 发送事务消息:

java 复制代码
import org.apache.rocketmq.client.producer.TransactionMQProducer;
import org.apache.rocketmq.client.producer.TransactionListener;
import org.apache.rocketmq.common.message.Message;

public class OrderService {
    private TransactionMQProducer producer;
    private OrderDao orderDao;
    private MessageLogDao messageLogDao;

    public void createOrder(Order order) throws Exception {
        // 初始化 RocketMQ 事务生产者
        producer = new TransactionMQProducer("order_producer_group");
        producer.setNamesrvAddr("localhost:9876");
        producer.setTransactionListener(new OrderTransactionListener());
        producer.start();

        // 构造消息
        String messageBody = "{\"orderId\":" + order.getOrderId() + "}";
        Message msg = new Message("OrderTopic", "TagA", messageBody.getBytes());

        // 发送事务消息
        producer.sendMessageInTransaction(msg, order);
    }
}

class OrderTransactionListener implements TransactionListener {
    private OrderDao orderDao;
    private MessageLogDao messageLogDao;

    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        Order order = (Order) arg;
        try {
            // 开启本地事务
            orderDao.beginTransaction();
            
            // 1. 保存订单
            orderDao.saveOrder(order);
            
            // 2. 记录消息到本地消息表
            MessageLog messageLog = new MessageLog(order.getOrderId(), new String(msg.getBody()), "INIT");
            messageLogDao.insert(messageLog);
            
            // 提交本地事务
            orderDao.commitTransaction();
            return LocalTransactionState.COMMIT_MESSAGE;
        } catch (Exception e) {
            orderDao.rollbackTransaction();
            return LocalTransactionState.ROLLBACK_MESSAGE;
        }
    }

    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        // RocketMQ 回查机制:检查本地消息表状态
        String body = new String(msg.getBody());
        Long orderId = parseOrderId(body); // 解析消息中的 orderId
        MessageLog log = messageLogDao.findByOrderId(orderId);
        
        if (log != null && "INIT".equals(log.getStatus())) {
            return LocalTransactionState.COMMIT_MESSAGE;
        }
        return LocalTransactionState.ROLLBACK_MESSAGE;
    }
}
(2) Consumer 端:消费消息

在库存服务和通知服务中,订阅 OrderTopic 并处理消息:

java 复制代码
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;

public class InventoryService {
    public void startConsumer() throws Exception {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("inventory_consumer_group");
        consumer.setNamesrvAddr("localhost:9876");
        consumer.subscribe("OrderTopic", "TagA");
        
        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
            for (MessageExt msg : msgs) {
                String body = new String(msg.getBody());
                Long orderId = parseOrderId(body);
                // 扣减库存
                deductInventory(orderId);
            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });
        
        consumer.start();
    }
}

4. 流程说明

  1. 订单服务
    • 保存订单和消息日志到本地数据库(一个事务)。
    • 发送事务消息到 RocketMQ。
    • 如果本地事务成功,提交 Commit;否则回滚。
  2. RocketMQ Broker
    • 接收半消息,等待事务状态。
    • 收到 Commit 后投递消息给 Consumer。
  3. 下游服务
    • 库存服务消费消息,扣减库存。
    • 通知服务消费消息,发送通知。

5. 异常处理

  • 本地事务失败:RocketMQ 收到 Rollback,消息不会投递。
  • Broker 超时未收到状态:触发回查机制,检查本地消息表状态。
  • Consumer 消费失败:通过重试机制或人工干预解决。

四、优缺点分析

优点

  1. 一致性保证:本地事务和消息发送绑定,确保数据和消息一致。
  2. 高可用性:RocketMQ 的事务机制和回查功能提高了可靠性。
  3. 解耦性:上下游服务通过消息队列解耦,降低依赖。

缺点

  1. 复杂度增加:需要维护本地消息表和事务逻辑。
  2. 性能开销:本地数据库和消息队列的两次写入会增加延迟。

以下是补充的博客内容,针对"是否必须使用事务消息"以及"如何设计更轻量的分布式事务"两个问题进行探讨,逻辑清晰且具有实用性。


六、扩展思考:必须使用事务消息吗?如何设计更轻量的分布式事务?

在上述方案中,我们结合了本地消息表和 RocketMQ 的事务消息来实现分布式事务。然而,一个自然的问题是:事务消息真的是必须的吗?如果我们希望系统更加轻量,减少复杂性,又该如何设计呢?让我们逐一分析。

1. 事务消息的必要性分析

为什么使用事务消息?

RocketMQ 的事务消息提供了两阶段提交机制,确保了本地事务和消息发送的一致性:

  • 如果本地事务失败,消息不会投递给下游(Rollback)。
  • 如果本地事务成功,消息一定会被投递(Commit)。
  • 通过回查机制,即使 Producer 宕机,Broker 也能根据本地消息表的状态决定消息的最终状态。

这种机制特别适合对一致性要求极高的场景,例如金融支付或订单生成,因为它避免了"消息已发送但本地事务未完成"或"本地事务完成但消息未发送"的问题。

不使用事务消息会怎样?

如果直接使用普通消息而非事务消息,可能出现以下问题:

  • 本地事务成功但消息未发送:例如,订单已保存,但网络抖动导致消息发送失败,下游服务无法感知。
  • 消息已发送但本地事务失败:例如,消息发送成功后数据库写入失败,导致下游服务执行了错误的业务逻辑。

这些不一致性需要额外的补偿逻辑或人工干预来解决,增加了系统的复杂性和维护成本。

结论

事务消息并不是绝对必须的,但它提供了更高的可靠性和一致性保障。如果你的业务场景对一致性要求不高,或者愿意通过其他方式处理不一致问题,可以考虑不使用事务消息。

2. 轻量化分布式事务的设计方案

为了降低复杂度,我们可以设计一个不依赖事务消息的轻量化方案。以下是一个基于普通消息和定时任务的替代方案:

方案设计
  1. 本地消息表依然保留
    • 表结构与之前相同,记录订单和消息状态(INIT, SENT, FAIL)。
  2. 使用普通消息发送
    • 在本地事务成功后,使用 RocketMQ 的普通消息(而不是事务消息)发送到 Broker。
  3. 定时任务补偿
    • 启动一个定时任务,扫描 message_log 表中状态为 INIT 的记录。
    • 如果发现消息未发送(例如,通过订单ID查询下游服务状态),则重发消息并更新状态为 SENT
  4. 下游服务幂等性
    • 库存服务、通知服务等下游系统需要保证幂等性,避免重复消费导致数据错误。
代码示例

Producer 端简化为普通消息发送:

java 复制代码
public class OrderService {
    private DefaultMQProducer producer;
    private OrderDao orderDao;
    private MessageLogDao messageLogDao;

    public void createOrder(Order order) throws Exception {
        producer = new DefaultMQProducer("order_producer_group");
        producer.setNamesrvAddr("localhost:9876");
        producer.start();

        // 本地事务
        orderDao.beginTransaction();
        orderDao.saveOrder(order);
        MessageLog messageLog = new MessageLog(order.getOrderId(), "{\"orderId\":" + order.getOrderId() + "}", "INIT");
        messageLogDao.insert(messageLog);
        orderDao.commitTransaction();

        // 发送普通消息
        Message msg = new Message("OrderTopic", "TagA", messageLog.getMessageContent().getBytes());
        producer.send(msg);
        messageLogDao.updateStatus(messageLog.getId(), "SENT"); // 更新状态
    }
}

定时任务补偿逻辑:

java 复制代码
public class MessageResendTask {
    private DefaultMQProducer producer;
    private MessageLogDao messageLogDao;

    @Scheduled(fixedDelay = 60000) // 每分钟扫描一次
    public void resendPendingMessages() {
        List<MessageLog> pendingMessages = messageLogDao.findByStatus("INIT");
        for (MessageLog log : pendingMessages) {
            Message msg = new Message("OrderTopic", "TagA", log.getMessageContent().getBytes());
            producer.send(msg);
            messageLogDao.updateStatus(log.getId(), "SENT");
        }
    }
}
流程说明
  1. 保存订单和消息日志到本地数据库(一个事务)。
  2. 发送普通消息到 RocketMQ,成功后更新消息状态为 SENT
  3. 定时任务定期扫描未发送的消息(状态为 INIT),重新发送并更新状态。
  4. 下游服务消费消息并执行相应操作。
优点
  • 简单性:去掉了事务消息的两阶段提交和回查逻辑,代码更简洁。
  • 性能提升:减少了与 Broker 的多次交互,发送延迟更低。
  • 灵活性:定时任务的频率和补偿逻辑可以根据业务需求调整。
缺点
  • 一致性稍弱:如果消息发送失败,依赖定时任务补偿,可能存在短暂的不一致。
  • 依赖下游幂等性:重复消息可能导致下游多次处理,必须确保幂等。
  • 补偿延迟:定时任务的间隔可能导致问题发现和修复的延迟。
适用场景

这种轻量化方案适用于:

  • 对一致性要求不高的业务(如日志记录、通知发送)。
  • 系统资源有限,不希望引入复杂的事务消息机制。
  • 下游服务已经具备良好的幂等性支持。

3. 如何选择?

特性 事务消息方案 轻量化方案
一致性 强一致性 最终一致性
复杂度 较高(两阶段提交+回查) 较低(普通消息+定时任务)
性能 稍低(多次网络交互) 较高(单次发送)
适用场景 高一致性需求(如支付、订单) 低一致性需求(如通知、日志)

根据业务需求权衡:

  • 如果一致性是首要考虑因素,选择事务消息方案。
  • 如果追求简单性和性能,且能接受短暂的不一致,选择轻量化方案。

七、总结

通过分析,我们发现事务消息并非分布式事务的唯一解法。结合本地消息表和普通消息,再辅以定时任务补偿,可以实现一个更轻量的分布式事务方案。这种方法在降低复杂度的同时,依然能满足许多业务场景的需求。最终的选择取决于你的业务对一致性、性能和复杂度的具体要求。

相关推荐
小杨40438 分钟前
架构系列二十三(全面理解IO)
java·后端·架构
uhakadotcom44 分钟前
Tableau入门:数据可视化的强大工具
后端·面试·github
demonlg01121 小时前
Go 语言 fmt 模块的完整方法详解及示例
开发语言·后端·golang
程序员鱼皮1 小时前
2025 年最全Java面试题 ,热门高频200 题+答案汇总!
java·后端·面试
测试盐1 小时前
django入门教程之cookie和session【六】
后端·python·django
天草二十六_简村人2 小时前
Rabbitmq消息被消费时抛异常,进入Unacked 状态,进而导致消费者不断尝试消费(下)
java·spring boot·分布式·后端·rabbitmq
uhakadotcom2 小时前
APM系统简介及案例
后端·面试·github
易元2 小时前
设计模式-外观模式
后端
低头不见2 小时前
Spring Boot 的启动流程
java·spring boot·后端
uhakadotcom2 小时前
Syslog投递日志到SIEM:基础知识与实践
后端·面试·github