RocketMQ消息幂等控制:借助数据库唯一约束实现

在Spring Boot中,使用数据库唯一约束来实现RocketMQ消费端的幂等性,是一种非常可靠的方式。其核心思想是利用数据库的唯一索引来防止同一条消息被处理多次。

下面我将为你详细解释实现方案,并提供清晰的代码示例。

实现思路与流程

使用数据库唯一约束实现幂等性的关键在于,为每条消息赋予一个全局唯一的业务标识(例如订单ID),并在处理消息前,尝试将这个标识插入一个专门的"防重表"中。如果该标识已存在,数据库会因唯一约束冲突而抛出异常,我们捕获到这个异常即可判断为重复消息。

其核心流程可以通过下表展示:

步骤 关键动作 说明
1. 定义防重表 创建包含唯一索引的表 用于记录已成功处理的消息唯一标识。
2. 获取业务唯一标识 从消息中提取key 使用生产者设置的业务ID(如订单号),而非RocketMQ的msgId
3. 插入去重记录 尝试将标识插入防重表 核心步骤​:利用数据库唯一约束保证原子性,插入成功代表首次处理。
4. 处理业务逻辑 执行具体的业务操作 只有第3步成功后,才执行扣款、发货等业务。
5. 异常处理 捕获唯一约束冲突异常 捕获到异常则判定为重复消息,跳过业务处理。

代码实现步骤

1. 创建消息防重表

首先,在数据库中创建一张表,用于记录已经处理过的消息的唯一标识。message_key字段需要建立唯一索引,这是整个方案的基石。

sql 复制代码
CREATE TABLE `message_idempotent` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `message_key` varchar(64) NOT NULL COMMENT '消息的唯一业务标识,如订单号',
  `topic` varchar(255) NOT NULL COMMENT '消息主题',
  `created_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_key` (`message_key`) -- 核心:唯一约束索引
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='消息幂等性防重表';

2. 创建对应的JPA实体类

kotlin 复制代码
import javax.persistence.*;
import java.util.Date;

@Entity
@Table(name = "message_idempotent", uniqueConstraints = {
        @UniqueConstraint(columnNames = {"message_key"}) // 映射数据库的唯一约束
})
public class MessageIdempotent {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "message_key", nullable = false, length = 64)
    private String messageKey; // 消息的唯一业务标识

    @Column(nullable = false)
    private String topic; // 消息主题

    @Column(name = "created_time")
    @Temporal(TemporalType.TIMESTAMP)
    private Date createdTime; // 记录创建时间

    // 构造器、Get和Set方法
    public MessageIdempotent() {}

    public MessageIdempotent(String messageKey, String topic) {
        this.messageKey = messageKey;
        this.topic = topic;
        this.createdTime = new Date();
    }
    // ... 省略 getter 和 setter
}

3. 创建数据访问层(Repository)

java 复制代码
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;

@Repository
public interface MessageIdempotentRepository extends JpaRepository<MessageIdempotent, Long> {

    /**
     * 根据消息的唯一键查找记录
     * @param messageKey 消息唯一键
     * @return 存在的记录
     */
    Optional<MessageIdempotent> findByMessageKey(String messageKey);
}

4. 实现幂等性服务(Service)

这是业务逻辑的核心,我们通过@Transactional注解保证插入记录和业务操作在同一个事务中。

typescript 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.PersistenceException;

@Service
public class IdempotentService {

    @Autowired
    private MessageIdempotentRepository idempotentRepository;

    /**
     * 在事务中尝试处理消息,保证幂等性
     *
     * @param messageKey 消息唯一标识
     * @param topic      消息主题
     * @param businessFunction 具体的业务处理逻辑(函数式接口)
     * @return true 处理成功;false 消息重复,已跳过
     */
    @Transactional(rollbackFor = Exception.class)
    public boolean processWithIdempotence(String messageKey, String topic, Runnable businessFunction) {
        try {
            // 1. 尝试插入防重记录
            MessageIdempotent record = new MessageIdempotent(messageKey, topic);
            idempotentRepository.save(record); // 如果messageKey已存在,此处会抛出异常

            // 2. 如果插入成功,说明是第一次处理,执行业务逻辑
            businessFunction.run();

            return true;

        } catch (Exception e) {
            // 3. 检查异常是否为唯一约束冲突(重复消息)
            if (isDuplicateKeyException(e)) {
                // 是重复消息,记录日志并返回false,不进行重试
                System.out.println("消息已被消费,幂等处理。业务键: " + messageKey);
                return false;
            } else {
                // 是其他业务异常,需要抛出,让消息重试
                throw e;
            }
        }
    }

    /**
     * 判断异常是否为唯一键冲突异常
     * 根据具体的JPA实现(Hibernate)和数据库驱动来精确判断
     */
    private boolean isDuplicateKeyException(Exception e) {
        Throwable cause = e.getCause();
        // 常见的唯一约束违反的异常信息或状态码
        // MySQL: Duplicate entry ... for key ...
        // 也可以通过异常链判断具体的异常类,如DataIntegrityViolationException
        return e.getMessage() != null && e.getMessage().contains("Duplicate entry");
    }
}

5. 在消息监听器中使用幂等服务

最后,在RocketMQ的监听器中注入我们编写的幂等服务。

kotlin 复制代码
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.apache.rocketmq.common.message.MessageExt;

@Component
@RocketMQMessageListener(topic = "YOUR_ORDER_TOPIC", consumerGroup = "YOUR_CONSUMER_GROUP")
public class OrderMessageListener implements RocketMQListener<MessageExt> {

    @Autowired
    private IdempotentService idempotentService;

    @Override
    public void onMessage(MessageExt message) {

        // 1. 从消息中获取业务的唯一标识(由生产者设置,如订单号)
        String orderId = message.getKeys(); 
        String topic = message.getTopic();

        // 2. 调用幂等服务处理消息
        boolean isProcessed = idempotentService.processWithIdempotence(orderId, topic, () -> {
            // 这里是你的核心业务逻辑,例如创建订单、扣减库存等
            // 只有当消息是第一次处理时,这段代码才会执行
            System.out.println("正在处理订单业务,订单ID: " + orderId);
            // ... 你的业务代码
        });

        if (isProcessed) {
            System.out.println("订单处理成功: " + orderId);
        } else {
            System.out.println("订单已处理,本次跳过: " + orderId);
        }
    }
}

关键注意事项

  1. 使用业务标识而非消息ID :务必使用消息的业务唯一标识(message.getKeys())​ 作为去重依据。因为同一条业务消息在生产者重发时,RocketMQ会生成不同的msgId,但业务的key应该是相同的。
  2. 并发安全性:数据库的唯一索引在数据库层面提供了原子性保证,可以安全地处理高并发场景。这正是此方案比"先查询再插入"方式更可靠的原因。
  3. 异常处理 :在isDuplicateKeyException方法中,需要根据你使用的具体数据库(如MySQL, PostgreSQL)来精确判断唯一约束冲突的异常类型,以确保准确识别重复消息。
  4. 防重表清理:防重表会不断增长,需要建立定期清理归档数据的机制(例如,只保留最近30天的记录),避免数据无限膨胀。

这种基于数据库唯一约束的方案,原理简单,可靠性高,是解决消息重复消费问题的经典方法。

相关推荐
间彧4 小时前
RocketMQ消息幂等控制:借助Redis实现
后端
霸道流氓气质4 小时前
SpringBoot+MybatisPlus+自定义注解+切面实现水平数据隔离功能(附代码下载)
java·spring boot·后端
间彧4 小时前
RocketMQ消息幂等控制:借助ConcurrentHashMap的putIfAbsent方法实现
后端
why技术4 小时前
1K+Star的开源项目能给一个在校大学生带来什么?
前端·人工智能·后端
克莱恩~莫雷蒂4 小时前
Spring Boot 中 controller层注解
java·spring boot·后端
追逐时光者5 小时前
分享 4 款基于 .NET 开源免费、实用的文件搜索工具,效率提升利器!
后端·.net
程序新视界5 小时前
什么是OLTP ,MySQL是如何支持OLTP的?
数据库·后端·mysql
songroom5 小时前
dbpystream webapi: 一次clickhouse数据从系统盘迁至数据盘的尝试
后端·clickhouse·阿里云
Lisonseekpan6 小时前
为什么要避免使用 `SELECT *`?
java·数据库·后端·sql·mysql·oracle