RabbitMQ在Spring Boot中的一般实践

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

RabbitMQ的图示讲解

首先,这是一个消息队列的讲解:
简单消息队列
Producer->[queue]->Consumer
产生消息者可以向队列推送消息,就像人们的邮件。
队列就是邮车,或者说存储邮件的仓库,有点像某鸟驿站。
消费者就是收件人,等着邮局送来消息。

完整的RabbitMQ还有 Exchange -- 交易所

  • Exchange (交换机、交易所):生产者将消息发送到的地方。它根据规则将消息路由到队列。常见的类型有:

    • Direct:精确匹配路由键(Routing Key)。
    • Fanout:广播模式,发送给所有绑定的队列。
    • Topic:模糊匹配路由键(支持通配符 * 和 #)。
    • Headers:根据消息头属性匹配(较少用)。
      截屏2026-03-14 17.51
  • Queue (队列):消息被存起来的地方。核心作用就是存储和转发消息,可以想象成一个先进先出的缓冲区,生产者把消息发到队列里,消费者再从队列中取出来处理。

  • Routing Key (路由键):这是区分消息应该被送到哪个队列的唯一索引,就像分拣,将邮件根据目的地分拣到不同的区。
    截屏2026-03-14 17.51

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中的一般实践。笔者能力有限,其中不足之处甚多。

posted @ 2026-06-02 11:00  tre2e  阅读(11)  评论(0)    收藏  举报