RabbitMQ在Spring Boot中的一般实践
先来学一下什么是RabbitMQ,RabbitMQ可以被想象成一个邮局,(对,就像计算机网络的那个邮局)这个兔子邮局负责的是传输业务中存在的二进制数据,最终准确的运输到对应的地方,无误地将消息送到你的手中👋。RabbitMQ是如何做到的呢?
RabbitMQ的图示讲解
首先,这是一个消息队列的讲解:

Producer->[queue]->Consumer
产生消息者可以向队列推送消息,就像人们的邮件。
队列就是邮车,或者说存储邮件的仓库,有点像某鸟驿站。
消费者就是收件人,等着邮局送来消息。
完整的RabbitMQ还有 Exchange -- 交易所
-
Exchange (交换机、交易所):生产者将消息发送到的地方。它根据规则将消息路由到队列。常见的类型有:
Direct:精确匹配路由键(Routing Key)。Fanout:广播模式,发送给所有绑定的队列。Topic:模糊匹配路由键(支持通配符*和#)。Headers:根据消息头属性匹配(较少用)。

-
Queue (队列):消息被存起来的地方。核心作用就是存储和转发消息,可以想象成一个先进先出的缓冲区,生产者把消息发到队列里,消费者再从队列中取出来处理。
-
Routing Key (路由键):这是区分消息应该被送到哪个队列的唯一索引,就像分拣,将邮件根据目的地分拣到不同的区。

Exchange通过Routing Key将消息发送到Queue里,这就是RabbitMQ消息队列的基本概念。当然如果不用分拣,是个小地方,那么直接fanout广播出去也行。
设计事务的时候要注意:幂等保护是基本的,否则会造成重复队列。
如果设置手动确认队列消息,要设计好,重复或失败后,该消息要怎么处理?重复不需要重发,放到死信队列以便回溯事件。
考虑这样一个场景,在一个答题项目的基础上添加一个记账系统,用于发放答题奖励,奖品兑换,流水记账。你会怎么实现?写好CRUD,然后呢?开始动原有项目的逻辑,在解题正确的controller后加奖励,加流水记录。在出题成功后加奖励,加流水记录。这样可以,但是不好。
一、代码耦合,维护困难。业务堆积在一块,带来复杂的报错异常处理链。
二、拓展性差,代码堆积。若添加转账系统呢?重复之前的操作,让代码和你的肚子一样,越来越胖。
以上两点为笔者视角,可以讨论。
接下来在Spring Boot中使用RabbitMQ来进行业务解耦的一般实践。因为没研究透,所以一般。
有了消息队列,那么要实现记账系统就可以这么来写。
在原有的解题正确逻辑后添加一个发送事件,“该用户解题成功啦!”,该消息通过Exchange推送到Queue里,然后写一个监听器,也就是消费者,监听这个Queue。这样就变成了,真正意义上的业务解耦,因为记账系统的失败不会影响到原有系统,它不在解题controller的报错异常处理链里。换句话说,发送事件是独立的,解题系统只负责发送一个解题正确的消息而已,然后在Listener监听器里进行记账系统的业务流程。
Spring Boot 中 Rabbit 的使用
1.配置pom.xml,添加RabbitMQ依赖
<!--引入rabbitmq-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2.编写RabbitMQ的application.yml文件配置
# rabbitmq配置
rabbitmq:
host: localhost # 通常是 localhost 或具体 IP
port: 5672 # RabbitMQ 消息通信端口AMQP(不是 15672 / HTTP)
username: admin # 默认是guest
password: admin # 默认是guest
connection-timeout: 15000 # 连接超时时间,单位为毫秒
template:
retry:
enabled: true # 发送重试
publisher-confirm-type: correlated # 确认模式
publisher-returns: true # 返回模式
listener:
simple:
# 手动确认消息
acknowledge-mode: manual
# 并发消费者数量
concurrency: 2
max-concurrency: 10
3.编写RabbitMQ的Config类
@Configuration
public class SlcRabbitConfig {
// --- 业务交换机 --- public static final String EXCHANGE_WALLET = "exchange.wallet.topic";
public static final String QUEUE_WALLET_REWARD = "queue.wallet.reward";
public static final String RK_REWARD_SUCCESS = "reward.solve.success";
// --- 死信交换机 (DLX) 配置 --- // 钱包奖励失败消息路由到死信交换机
public static final String EXCHANGE_DLX = "exchange.slc.dlx";
public static final String QUEUE_DLX_REWARD = "queue.dlx.wallet.reward";
public static final String RK_DLX_REWARD = "dlx.wallet.reward";
// 定义死信交换机
@Bean
public TopicExchange dlxExchange() {
return new TopicExchange(EXCHANGE_DLX, true, false);
}
// 钱包奖励死信队列及绑定
@Bean
public Queue dlxRewardQueue() {
return QueueBuilder.durable(QUEUE_DLX_REWARD).build();
}
@Bean
public Binding bindingDlxReward() {
return BindingBuilder.bind(dlxRewardQueue()).to(dlxExchange()).with(RK_DLX_REWARD);
}
// 业务队列配置(绑定死信参数)
@Bean
public TopicExchange rewardExchange(){ return new TopicExchange(EXCHANGE_WALLET); }
@Bean
public Queue walletRewardQueue() {
return QueueBuilder.durable(QUEUE_WALLET_REWARD)
.deadLetterExchange(EXCHANGE_DLX) // 指定死信送往哪个交换机
.deadLetterRoutingKey(RK_DLX_REWARD) // 指定死信的 RK .ttl(1000 * 60 * 15) // 15分钟未消费 → 死信
.build();
}
@Bean
public Binding bindingWalletReward() {
return BindingBuilder.bind(walletRewardQueue()).to(rewardExchange()).with(RK_REWARD_SUCCESS);
}
// MessageConverter用于将Java对象转换为RabbitMQ的消息。
@Bean
public MessageConverter jsonMessageConverter() {
return new Jackson2JsonMessageConverter();
}
}
上面的配置类,通过调用了spring提供的方法,自动执行了创建Exchange和queue的步骤。
4.写一个生产者的服务
@Service
@RequiredArgsConstructor
public class SlcRabbitMqService {
private final RabbitTemplate rabbitTemplate;
private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);
@PostConstruct
public void initCallbacks() {
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
if (ack) {
logger.info("Message has been accepted by exchange");
} else {
logger.error("Rabbit refused to receive. Cause:{}", cause);
}
});
}
public void sendCorrectMessageToWallet(SolveRewardDTO solveRewardDTO) {
rabbitTemplate.convertAndSend(
SlcRabbitConfig.EXCHANGE_WALLET,
SlcRabbitConfig.RK_REWARD_SUCCESS,
solveRewardDTO
);
}
// TODO 死信、延迟发消息
}
@PostConstruct会在检查完所有Bean依赖注入后自动执行该方法。也就是在确保rabbitTemplate有值后,执行该方法。该方法给rabbitTemplate添加一个回调确认。
5.在业务中调用生产者服务,即业务中发送消息事件
if (correct) {
SolveRewardDTO solveRewardDTO = new SolveRewardDTO((Integer) user.getUserId(),id);
// 生产者发送消息
slcRabbitMqService.sendCorrectMessageToWallet(solveRewardDTO);
return BaseResponseDTO.makeResponse(200, "Correct");
}
6.写一个消费者来处理消息
@RequiredArgsConstructor
@Component
public class WalletChangeListener {
private final WalletRewardService walletRewardService;
private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);
/**
* 处理解决题目发放奖励队列
*/
@RabbitListener(queues = SlcRabbitConfig.QUEUE_WALLET_REWARD)
public void challengeSolvedLedger(Message message,
Channel channel,
@Payload SolveRewardDTO solveRewardDTO) {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
walletRewardService.processSolveReward(solveRewardDTO);
channel.basicAck(deliveryTag, false); // 确认消息
} catch (DuplicateKeyException e) {
try {
// 幂等命中(唯一键冲突):视为已处理,直接确认避免重复重试
logger.info("Skip duplicate reward event for user={}, challenge={}",
solveRewardDTO.getUserId(), solveRewardDTO.getChallengeId());
channel.basicAck(deliveryTag, false);
} catch (IOException ioException) {
logger.error("Failed to ack duplicate msg: {}", ioException.getMessage());
}
} catch (DataIntegrityViolationException e) {
try {
logger.error("Data integrity violation, route to DLQ: {}", e.getMessage());
channel.basicNack(deliveryTag, false, false);
} catch (IOException ioException) {
logger.error("Failed to reject data-integrity msg: {}", ioException.getMessage());
}
} catch (Exception e) {
try {
// 业务处理失败:不重回队列,交给 DLQ,避免无限重试
logger.error("Failed to process reward message: {}", e.getMessage());
channel.basicNack(deliveryTag, false, false);
} catch (IOException ioException ) {
logger.error("Failed to refuse msg: {}", ioException.getMessage());
}
}
}
}
以上便是RabbitMQ在Spring Boot中的一般实践。笔者能力有限,其中不足之处甚多。

浙公网安备 33010602011771号