使用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. 如何选择?

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

根据业务需求权衡:

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

七、总结

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

相关推荐
uzong2 小时前
技术故障复盘模版
后端
GetcharZp2 小时前
基于 Dify + 通义千问的多模态大模型 搭建发票识别 Agent
后端·llm·agent
桦说编程2 小时前
Java 中如何创建不可变类型
java·后端·函数式编程
IT毕设实战小研3 小时前
基于Spring Boot 4s店车辆管理系统 租车管理系统 停车位管理系统 智慧车辆管理系统
java·开发语言·spring boot·后端·spring·毕业设计·课程设计
wyiyiyi3 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
阿华的代码王国4 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
Jimmy4 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
AntBlack5 小时前
不当韭菜V1.1 :增强能力 ,辅助构建自己的交易规则
后端·python·pyqt
bobz9655 小时前
pip install 已经不再安全
后端
寻月隐君5 小时前
硬核实战:从零到一,用 Rust 和 Axum 构建高性能聊天服务后端
后端·rust·github