提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 一、核心需求与技术选型
-
- [1. 核心需求(必满足)](#1. 核心需求(必满足))
- [2. 技术选型](#2. 技术选型)
- 二、环境配置(application.yml)
- 三、核心代码实现(全可复制)
-
- [1. 依赖导入(pom.xml)](#1. 依赖导入(pom.xml))
- [2. 实体类(OrderCreateDTO)](#2. 实体类(OrderCreateDTO))
- [3. 生产者:消息可靠投递(Confirm + 持久化)](#3. 生产者:消息可靠投递(Confirm + 持久化))
- [4. 消费者:手动 ACK + Redis 幂等防重](#4. 消费者:手动 ACK + Redis 幂等防重)
- [5. 业务层:Redis 幂等 + 数据库兜底](#5. 业务层:Redis 幂等 + 数据库兜底)
- [6. 交换机、队列绑定(可选,两种方式)](#6. 交换机、队列绑定(可选,两种方式))
- 方式1:代码绑定(推荐,部署时自动创建,无需手动操作)
- [方式2:RabbitMQ 管理界面手动绑定(适合测试环境,生产环境推荐代码绑定)](#方式2:RabbitMQ 管理界面手动绑定(适合测试环境,生产环境推荐代码绑定))
- 四、关键知识点与避坑点(实战重点)
- 五、测试验证(快速验证可用性)
- 六、总结
前言
Spring Boot + RabbitMQ 实战:消息可靠投递+防重复消费(可直接落地)
在高并发业务场景中,RabbitMQ 作为消息中间件,核心作用是削峰填谷、解耦服务,但最关键的两个问题的是:消息不丢失、不重复消费。
基于 Spring Boot 整合 RabbitMQ,提供一套可直接复制、生产环境可用的实战代码,涵盖「生产者 Confirm 确认、消息持久化、消费者手动 ACK、Redis 幂等防重」全流程,避开所有常见坑,新手也能快速落地。
适用场景:订单异步创建、短信/通知推送、物流状态同步等所有需要保证消息可靠性的业务,尤其适配高并发下单、秒杀等场景。
一、核心需求与技术选型
1. 核心需求(必满足)
-
生产者:消息必须送达 RabbitMQ Broker,失败可重试,杜绝生产者丢消息
-
消息本身:Broker 重启、服务宕机后,消息不丢失
-
消费者:业务处理成功后再确认消息,异常可重新入队,杜绝消费端丢消息
-
幂等性:避免因网络重试、消息重入队导致的重复消费(比如重复创建订单、重复扣库存)
2. 技术选型
-
框架:Spring Boot 2.x(兼容 3.x,只需微调依赖)
-
消息中间件:RabbitMQ 3.9+
-
幂等校验:Redis(高效判重)+ 数据库唯一索引(兜底)
-
核心依赖:spring-boot-starter-amqp、spring-boot-starter-data-redis
二、环境配置(application.yml)
核心配置:开启生产者 Confirm 机制、Return 机制,消费者手动 ACK,同时配置限流防止数据库被冲垮,注释清晰可直接复制。
yaml
spring:
# RabbitMQ 核心配置
rabbitmq:
host: 127.0.0.1 # 本地环境,生产环境替换为服务器地址
port: 5672 # RabbitMQ 默认端口
username: guest # 默认用户名,生产环境需修改为自定义账号
password: guest # 默认密码,生产环境需修改
virtual-host: / # 虚拟主机,默认即可
connection-timeout: 10000 # 连接超时时间,避免无限等待
# 1. 生产者确认机制:确保消息到达 Broker
publisher-confirm-type: correlated # correlated:异步回调,获取确认结果
# 2. 消息回退机制:消息无法路由时返回生产者,避免消息丢失
publisher-returns: true
# 3. 消费者配置
listener:
simple:
acknowledge-mode: manual # 手动 ACK(关键!避免自动确认丢消息)
concurrency: 5 # 消费者核心并发数
max-concurrency: 10 # 消费者最大并发数
prefetch: 10 # 限流:每次只获取10条消息,防止消费过快冲垮数据库
三、核心代码实现(全可复制)
1. 依赖导入(pom.xml)
无需额外配置,导入 Spring Boot 整合 RabbitMQ 和 Redis 的 starter 即可。
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- lombok 简化代码,可选但推荐 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
2. 实体类(OrderCreateDTO)
订单消息传输的实体类,根据自身业务调整字段,需实现 Serializable 接口(RabbitMQ 消息传输要求)。
java
import lombok.Data;
import java.io.Serializable;
/**
* 订单创建消息DTO
*/
@Data
public class OrderCreateDTO implements Serializable {
// 订单唯一编号(用于业务幂等)
private String orderSn;
// 用户ID
private Long userId;
// 订单金额
private BigDecimal orderAmount;
// 商品ID(多个可改为List)
private Long productId;
// 购买数量
private Integer quantity;
}
3. 生产者:消息可靠投递(Confirm + 持久化)
核心逻辑:
-
生成全局唯一 msgId(用于后续幂等判重)
-
设置消息持久化(Broker 重启后消息不丢失)
-
开启 Confirm 回调,监听消息是否成功送达 Broker,失败可重试/落库
java
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.MessageDeliveryMode;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;
import java.util.UUID;
/**
* 订单消息生产者(可靠投递)
*/
@Component
@Slf4j
@RequiredArgsConstructor
public class OrderProducer {
// 注入RabbitTemplate,用于发送消息
private final RabbitTemplate rabbitTemplate;
// 交换机名称(需与消费者队列绑定)
private static final String ORDER_EXCHANGE = "order.exchange";
// 路由键(需与队列绑定,确保消息能路由到指定队列)
private static final String ORDER_CREATE_ROUTING_KEY = "order.create";
/**
* 发送订单创建消息
* @param dto 订单创建DTO
*/
public void sendOrderMsg(OrderCreateDTO dto) {
// 1. 生成全局唯一消息ID,用于幂等判重(UUID保证唯一性)
String msgId = UUID.randomUUID().toString().replace("-", "");
// 2. 关联消息ID,用于Confirm回调获取消息标识
CorrelationData correlationData = new CorrelationData(msgId);
// 3. 发送消息(设置持久化 + 携带消息ID)
rabbitTemplate.convertAndSend(
ORDER_EXCHANGE, // 交换机
ORDER_CREATE_ROUTING_KEY,// 路由键
dto, // 消息内容
message -> {
// 设置消息持久化(MessageDeliveryMode.PERSISTENT:持久化)
message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
// 将msgId存入消息属性,供消费者获取
message.getMessageProperties().setMessageId(msgId);
return message;
},
correlationData // 关联消息ID,用于Confirm回调
);
// 4. 生产者Confirm回调:监听消息是否成功送达Broker
rabbitTemplate.setConfirmCallback((correlation, ack, cause) -> {
// 获取回调的消息ID
String msgIdCallback = correlation.getId();
if (ack) {
// ack为true:消息成功送达Broker
log.info("消息发送成功,msgId:{}", msgIdCallback);
} else {
// ack为false:消息发送失败
log.error("消息发送失败,msgId:{},失败原因:{}", msgIdCallback, cause);
// 失败处理:可重试发送(建议最多3次),或入库定时重发(避免消息丢失)
retrySendMsg(dto, msgIdCallback);
}
});
}
/**
* 消息发送失败重试(简单重试逻辑,可根据业务优化)
*/
private void retrySendMsg(OrderCreateDTO dto, String msgId) {
int retryCount = 3; // 重试3次
for (int i = 0; i < retryCount; i++) {
try {
Thread.sleep(1000 * (i + 1)); // 指数退避重试(1s、2s、3s)
CorrelationData correlationData = new CorrelationData(msgId);
rabbitTemplate.convertAndSend(
ORDER_EXCHANGE,
ORDER_CREATE_ROUTING_KEY,
dto,
message -> {
message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
message.getMessageProperties().setMessageId(msgId);
return message;
},
correlationData
);
log.info("消息重试发送成功,msgId:{},重试次数:{}", msgId, i + 1);
return;
} catch (Exception e) {
log.error("消息重试发送失败,msgId:{},重试次数:{}", msgId, i + 1, e);
if (i == retryCount - 1) {
// 重试3次仍失败,入库定时重发(此处省略入库逻辑,可结合定时任务实现)
log.error("消息重试3次失败,msgId:{},已入库待定时重发", msgId);
}
}
}
}
}
4. 消费者:手动 ACK + Redis 幂等防重
核心逻辑:
-
手动 ACK:业务处理成功后,调用 basicAck 确认消息;异常则调用 basicNack 重新入队
-
Redis 幂等:用 setIfAbsent 存储 msgId,已消费则直接 ACK,避免重复消费
-
业务兜底:结合数据库唯一索引,防止 Redis 挂了导致的重复消费
java
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import com.rabbitmq.client.Channel;
import java.util.concurrent.TimeUnit;
/**
* 订单消息消费者(手动ACK + 幂等防重)
*/
@Component
@Slf4j
@RequiredArgsConstructor
public class OrderConsumer {
// 注入Redis模板,用于幂等判重
private final StringRedisTemplate redisTemplate;
// 注入订单服务,处理核心业务逻辑
private final OrderService orderService;
// 队列名称(需与交换机、路由键绑定)
private static final String ORDER_CREATE_QUEUE = "order.create.queue";
// Redis 幂等键前缀(区分不同业务的消息)
private static final String MQ_CONSUMED_KEY_PREFIX = "mq:consumed:order:";
/**
* 消费订单创建消息
* @param dto 消息内容(自动反序列化)
* @param message 消息对象,用于获取msgId
* @param channel 信道对象,用于手动ACK/NACK
*/
@RabbitListener(queues = ORDER_CREATE_QUEUE) // 监听指定队列
public void consumeOrderMsg(OrderCreateDTO dto, Message message, Channel channel) throws Exception {
// 1. 获取消息ID(生产者存入的msgId)
String msgId = message.getMessageProperties().getMessageId();
// 2. 获取消息投递标签(用于手动ACK/NACK)
long deliveryTag = message.getMessageProperties().getDeliveryTag();
// 3. Redis 幂等判重:setIfAbsent 原子操作,避免并发重复消费
String redisKey = MQ_CONSUMED_KEY_PREFIX + msgId;
// 存入Redis,有效期24小时(根据业务调整,确保消息消费完成后不会被重复判断)
Boolean consumeFlag = redisTemplate.opsForValue().setIfAbsent(redisKey, "1", 24, TimeUnit.HOURS);
// 4. 已消费过:直接ACK,避免重复处理
if (consumeFlag == null || !consumeFlag) {
log.warn("消息已消费,无需重复处理,msgId:{}", msgId);
// 手动ACK:deliveryTag为当前消息标签,false表示不批量确认
channel.basicAck(deliveryTag, false);
return;
}
try {
// 5. 处理核心业务逻辑:创建订单、扣减库存等
orderService.createOrder(dto);
// 6. 业务处理成功:手动ACK,通知RabbitMQ删除消息
channel.basicAck(deliveryTag, false);
log.info("消息消费成功,msgId:{},订单号:{}", msgId, dto.getOrderSn());
} catch (Exception e) {
log.error("消息消费异常,msgId:{},订单号:{}", msgId, dto.getOrderSn(), e);
// 7. 异常处理:根据异常类型决定是否重入队
// 可重试异常(如网络波动、数据库临时不可用):重入队(third参数为true)
// 不可重试异常(如业务校验失败、参数错误):直接拒绝,不重入队(third参数为false)
if (isRetryException(e)) {
log.info("消息消费异常(可重试),将重入队,msgId:{}", msgId);
channel.basicNack(deliveryTag, false, true);
} else {
log.info("消息消费异常(不可重试),直接拒绝,msgId:{}", msgId);
// 不可重试异常:拒绝消息,不重入队(可结合死信队列处理)
channel.basicReject(deliveryTag, false);
}
}
}
/**
* 判断是否为可重试异常(根据自身业务调整)
*/
private boolean isRetryException(Exception e) {
// 示例:网络异常、数据库异常可重试,业务异常不可重试
return e instanceof RuntimeException
&& (e.getMessage().contains("网络") || e.getMessage().contains("数据库"));
}
}
5. 业务层:Redis 幂等 + 数据库兜底
**核心:**即使 Redis 挂了,通过数据库唯一索引(order_sn)兜底,确保不会重复创建订单、重复扣库存。
java
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 订单服务(核心业务逻辑)
*/
@Service
@Slf4j
@RequiredArgsConstructor
public class OrderService {
private final OrderMapper orderMapper;
private final ProductMapper productMapper;
/**
* 创建订单(带幂等校验)
* @param dto 订单创建DTO
*/
@Transactional(rollbackFor = Exception.class) // 事务管理,异常回滚
public void createOrder(OrderCreateDTO dto) {
String orderSn = dto.getOrderSn();
// 1. 数据库幂等兜底:查询订单是否已存在(订单表order_sn字段建唯一索引)
Integer orderCount = orderMapper.countByOrderSn(orderSn);
if (orderCount > 0) {
log.warn("订单已存在,无需重复创建,订单号:{}", orderSn);
return;
}
// 2. 核心业务逻辑:创建订单、扣减库存(根据自身业务实现)
// ① 扣减商品库存(需加锁,避免超卖,此处省略分布式锁逻辑)
Product product = productMapper.selectById(dto.getProductId());
if (product == null || product.getStock() < dto.getQuantity()) {
throw new RuntimeException("商品不存在或库存不足,订单号:" + orderSn);
}
product.setStock(product.getStock() - dto.getQuantity());
productMapper.updateById(product);
// ② 插入订单记录
Order order = new Order();
order.setOrderSn(orderSn);
order.setUserId(dto.getUserId());
order.setOrderAmount(dto.getOrderAmount());
order.setProductId(dto.getProductId());
order.setQuantity(dto.getQuantity());
order.setStatus(0); // 0:待支付
orderMapper.insert(order);
log.info("订单创建成功,订单号:{}", orderSn);
}
}
6. 交换机、队列绑定(可选,两种方式)
方式1:代码绑定(推荐,部署时自动创建,无需手动操作)
java
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* RabbitMQ 交换机、队列绑定配置
*/
@Configuration
public class RabbitMQConfig {
// 交换机名称(与生产者、消费者一致)
public static final String ORDER_EXCHANGE = "order.exchange";
// 队列名称(与消费者一致)
public static final String ORDER_CREATE_QUEUE = "order.create.queue";
// 路由键(与生产者一致)
public static final String ORDER_CREATE_ROUTING_KEY = "order.create";
// 1. 声明交换机(direct类型,持久化)
@Bean
public DirectExchange orderExchange() {
// durable=true:交换机持久化,重启后不丢失
return new DirectExchange(ORDER_EXCHANGE, true, false);
}
// 2. 声明队列(持久化)
@Bean
public Queue orderCreateQueue() {
// durable=true:队列持久化;exclusive=false:不排他;autoDelete=false:不自动删除
return new Queue(ORDER_CREATE_QUEUE, true, false, false);
}
// 3. 绑定交换机、队列、路由键
@Bean
public Binding orderCreateBinding() {
return BindingBuilder.bind(orderCreateQueue())
.to(orderExchange())
.with(ORDER_CREATE_ROUTING_KEY);
}
}
方式2:RabbitMQ 管理界面手动绑定(适合测试环境,生产环境推荐代码绑定)
-
登录 RabbitMQ 管理界面(默认地址:http://localhost:15672)
-
创建交换机:类型 direct,名称 order.exchange,勾选 durable
-
创建队列:名称 order.create.queue,勾选 durable
-
绑定:交换机 → 队列,路由键填写 order.create
四、关键知识点与避坑点(实战重点)
- 消息不丢失的3个关键
-
生产者:开启 Confirm 机制,确保消息到达 Broker,失败重试/落库
-
消息:设置持久化(MessageDeliveryMode.PERSISTENT),交换机、队列也需持久化
-
消费者:手动 ACK,业务成功后再确认,异常合理处理(重入队/死信)
- 防重复消费的2层保障
-
第一层:Redis setIfAbsent 原子操作(高效判重,适合高并发)
-
第二层:数据库唯一索引(兜底,防止 Redis 挂了导致的重复消费)
- 常见坑及解决方案
-
坑1:消息自动 ACK → 解决方案:配置 acknowledge-mode: manual,手动 ACK
-
坑2:消息未持久化 → 解决方案:设置消息、交换机、队列均为持久化(durable=true)
-
坑3:Redis 挂了导致重复消费 → 解决方案:数据库唯一索引兜底
-
坑4:消费者并发过高冲垮数据库 → 解决方案:配置 prefetch 限流,控制每次获取的消息数
-
坑5:消息发送失败后不重试 → 解决方案:实现 Confirm 回调,失败后指数退避重试,重试失败入库定时重发
五、测试验证(快速验证可用性)
-
启动 RabbitMQ 服务(本地可通过 Docker 快速部署)
-
启动 Spring Boot 项目,自动创建交换机、队列并绑定
-
编写测试类,调用 OrderProducer 的 sendOrderMsg 方法发送消息
-
查看日志:消息发送成功 → 消费成功 → 订单创建成功
-
测试异常场景:关闭数据库,发送消息,查看是否重入队;恢复数据库后,查看是否正常消费
-
测试重复消费:手动将消息重新入队,查看是否会重复创建订单(应提示"订单已存在")
六、总结
本文提供的代码的是生产环境真实落地版本,涵盖了 RabbitMQ 消息可靠投递和防重复消费的全流程,无需修改核心逻辑,只需根据自身业务调整实体类和业务方法,即可快速集成到项目中。
核心思路:生产者靠 Confirm 保送达,消息靠持久化保存活,消费者靠手动 ACK 保消费,幂等靠 Redis+数据库保唯一,四者结合,彻底解决 RabbitMQ 消息丢失和重复消费的痛点。
后续可优化方向 :消息重试机制(结合定时任务)、死信队列(处理不可重试异常消息)、分布式锁(防止库存超卖),可根据业务复杂度逐步迭代。
