RocketMQ如何实现消息Exactly-Once

🚀 RocketMQ如何实现消息Exactly-Once语义?

🔍 核心实现原理

RocketMQ通过生产者幂等性 + 消费者幂等性 + 事务消息三重保障机制实现准Exactly-Once语义(需业务配合)


🛠️ 生产者端保障措施

1. 消息发送幂等性(Idempotent Producer)

java 复制代码
// 使用Message的UNIQUE_KEY设置业务唯一标识
Message msg = new Message("OrderTopic", 
    "20230815-ORDER-10086".getBytes()); // 业务唯一ID作为消息Key
msg.setKeys("ORDER_10086");  // 设置唯一消息Key
SendResult sendResult = producer.send(msg);

实现原理:

  • 🌟 通过keys字段设置业务唯一标识
  • 🔑 Broker端会基于MessageID + Key进行重复判断
  • ⏱️ 默认保留10分钟重复检查(可通过transactionTimeout配置)

🛒 消费者端保障措施

2. 消费幂等设计

scss 复制代码
consumer.registerMessageListener((MessageListenerOrderly) (msgs, context) -> {
    for (MessageExt msg : msgs) {
        String orderId = msg.getKeys(); // 获取业务唯一标识
        if (isProcessed(orderId)) {     // 检查本地去重表
            return ConsumeOrderlyStatus.SUCCESS;
        }
        // 业务处理逻辑
        processOrder(orderId);
        // 记录已处理标识
        markAsProcessed(orderId); 
    }
    return ConsumeOrderlyStatus.SUCCESS;
});

实现要点:

  • 🧰 建立业务去重表(Redis/DB)记录处理状态
  • ⚡ 使用UNIQUE KEY约束防止重复插入
  • 🔄 消息重试时跳过已处理记录

💼 事务消息机制

typescript 复制代码
TransactionMQProducer producer = new TransactionMQProducer("GROUP");
producer.setTransactionListener(new TransactionListener() {
    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        try {
            // 执行本地事务
            boolean success = bizService.process(msg);
            return success ? LocalTransactionState.COMMIT_MESSAGE : 
                LocalTransactionState.ROLLBACK_MESSAGE;
        } catch (Exception e) {
            return LocalTransactionState.UNKNOW;
        }
    }

    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        return bizService.checkTransactionStatus(msg) ? 
            LocalTransactionState.COMMIT_MESSAGE : 
            LocalTransactionState.ROLLBACK_MESSAGE;
    }
});

事务流程:

  1. 📨 发送半消息(对消费者不可见)
  2. ✅ 执行本地事务
  3. 🧑💻 根据结果提交/回滚消息
  4. 🔄 事务回查机制兜底(默认15次检查)

🧩 终极方案:分布式锁+幂等表

typescript 复制代码
public void handleMessage(MessageExt msg) {
    String lockKey = "MSG_LOCK:" + msg.getMsgId();
    try {
        // 获取分布式锁
        if (redisLock.tryLock(lockKey, 30)) {
            if (dupCheckService.isProcessed(msg.getKeys())) {
                return;
            }
            // 业务处理
            processBusiness(msg);
            // 记录处理状态
            dupCheckService.markProcessed(msg.getKeys());
        }
    } finally {
        redisLock.unlock(lockKey);
    }
}

优势组合:

  • 🔒 分布式锁保证并发安全
  • 📝 幂等表持久化处理状态
  • ⏳ 锁自动过期防止死锁

📌 总结要点

  1. 🚫 RocketMQ无法100%保证Exactly-Once(需业务配合)
  2. 🔗 生产端:消息Key+事务消息机制
  3. 🛡️ 消费端:分布式锁+幂等表组合拳
相关推荐
IT规划师25 分钟前
编程术语中的同步与异步:从"等不等"到工程取舍
后端·面试
孟陬1 小时前
Foobar 技术周刊#159:当看不懂代码时,如何高效排障调试。脏页陷阱:一条只读查询,写入数 TB 数据。推测解码:大模型加速利器
前端·人工智能·后端
GoGeekBaird1 小时前
别再给每个 AI 做自我介绍了:我做了一个「个人上下文」管理系统
后端·github·agent
码事漫谈1 小时前
腾讯混元 Hy4 正式版发布了
后端
Mikko71 小时前
Redis 连接数满了服务端有错误信息吗?连接数不增加却命令超时?6.2 实测连接池、超时与大 key 怎么判定
java·spring boot·redis·后端
苍何3 小时前
DeepSeek的Super-agent 设计,和我们的工程实践
后端
Patrick_Wilson3 小时前
第一次读 Go 项目:从两个源码文件弄清 package、import 和 go.mod
后端·go·编程语言
Patrick_Wilson3 小时前
执行 go build . 后,Go 怎样找到代码并生成程序
后端·go·编程语言
用户6919026813393 小时前
Redis 2:Redis 在怎么处理harness记忆
后端
Patrick_Wilson3 小时前
Go 变量不赋值时,为什么也有值
后端·go·编程语言