RabbitMQ 事务与消息分发

RabbitMQ 事务与消息分发详解

在使用 RabbitMQ 构建异步系统时,消息能不能可靠发送、消费者能不能稳定处理、多个消费者之间能不能合理分担压力,都会直接影响系统的稳定性。本文围绕 RabbitMQ 的事务机制和消息分发机制展开,重点说明它们解决的问题、核心配置方式以及在实际业务中的使用思路。

RabbitMQ 事务机制

RabbitMQ 基于 AMQP 协议实现,而 AMQP 协议本身支持事务机制,因此 RabbitMQ 也提供了事务能力。事务的核心作用是保证消息发送或接收过程具备原子性:要么全部成功,要么全部失败。

在 Spring AMQP 中,也可以通过事务管理器配合 RabbitTemplate 来完成事务控制。典型场景是:业务代码中连续发送多条消息,如果中间出现异常,希望前面已经执行的消息发送操作也能够回滚,避免系统进入半成功状态。

配置事务管理器

在 Spring Boot 项目中,可以通过 RabbitTransactionManager 开启 RabbitMQ 事务支持。同时,需要将 RabbitTemplatechannelTransacted 设置为 true,表示该模板使用事务信道发送消息。

java 复制代码
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.transaction.RabbitTransactionManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class TransactionConfig {

    @Bean
    public RabbitTransactionManager transactionManager(
            CachingConnectionFactory connectionFactory) {
        return new RabbitTransactionManager(connectionFactory);
    }

    @Bean
    public RabbitTemplate rabbitTemplate(
            CachingConnectionFactory connectionFactory) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setChannelTransacted(true);
        return rabbitTemplate;
    }
}

这里有两个关键点:

  1. RabbitTransactionManager 用来管理 RabbitMQ 事务。
  2. rabbitTemplate.setChannelTransacted(true) 表示发送消息时启用事务信道。

如果只配置事务管理器,却没有让 RabbitTemplate 使用事务信道,消息发送过程并不会真正受到事务控制。

声明事务测试队列

为了验证事务效果,可以先声明一个普通的持久化队列:

java 复制代码
@Bean("transQueue")
public Queue transQueue() {
    return QueueBuilder.durable("trans_queue").build();
}

该队列用于接收事务发送的消息。

生产者发送消息

生产者中使用 @Transactional 注解开启事务控制。下面的示例中,第一条消息发送之后,代码主动制造了一个异常,因此第二条消息不会执行。

java 复制代码
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/trans")
@RestController
public class TransactionProducer {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Transactional
    @RequestMapping("/send")
    public String send() {
        rabbitTemplate.convertAndSend("", "trans_queue", "trans test 1...");
        int a = 5 / 0;
        rabbitTemplate.convertAndSend("", "trans_queue", "trans test 2...");
        return "发送成功";
    }
}

如果不加 @Transactional,第一条消息已经发送到 RabbitMQ,后续代码即使抛出异常,也不会影响这条消息。

如果加上 @Transactional,当方法执行过程中出现异常时,整个消息发送过程会回滚。也就是说,第一条消息和第二条消息都不会真正发送成功。

事务机制适合解决什么问题

RabbitMQ 事务机制适合用在对一致性要求较高的场景。例如:

  1. 一个业务动作需要连续发送多条消息,必须保证这些消息同时成功或同时失败。
  2. 消息发送逻辑和本地业务逻辑需要放在同一个事务流程中控制。
  3. 某些关键链路不能接受"前一条消息发送成功、后一条消息发送失败"的中间状态。

不过,事务机制会带来额外性能开销。因为事务需要等待提交或回滚结果,吞吐量会受到影响。在高并发消息发送场景中,更常见的做法是使用发送方确认机制来保证消息投递可靠性;事务更适合对一致性要求更强、吞吐要求相对没那么极端的业务。

RabbitMQ 消息分发机制

当一个队列绑定多个消费者时,RabbitMQ 会把队列中的消息分发给不同消费者处理。每条消息只会发送给订阅该队列的某一个消费者。这种模式非常适合横向扩展:当业务压力变大时,可以增加更多消费者实例来提升整体消费能力。

RabbitMQ 默认使用轮询方式分发消息。也就是说,消息会按照顺序依次分配给不同消费者,而不会主动判断消费者当前是否忙碌、处理速度是否足够快。

这种默认方式在消费者处理能力相近时问题不大。但如果某些消费者处理速度很慢,另一些消费者处理速度很快,就可能出现不均衡现象:慢消费者手里堆着大量未确认消息,快消费者却处于空闲状态,最终导致整体吞吐量下降。

要解决这个问题,可以使用 channel.basicQos(int prefetchCount) 控制消费者一次最多能够持有多少条未确认消息。

prefetchCount 的作用

prefetchCount 用来限制消费者预取消息的数量。RabbitMQ 会为消费者维护一个未确认消息计数:

  1. 每发送一条消息给消费者,计数加一。
  2. 消费者确认一条消息后,计数减一。
  3. 当未确认数量达到 prefetchCount 上限时,RabbitMQ 暂停继续向该消费者推送消息。
  4. 等消费者确认已有消息后,RabbitMQ 才会继续发送新消息。

这种机制类似滑动窗口,可以让 RabbitMQ 根据消费者的处理进度控制消息流量。

需要注意的是,prefetchCount 设置为 0 表示没有上限。另外,它主要针对推模式消费生效,对拉模式消费无效。

消费端限流

限流是 prefetchCount 最常见的使用场景之一。

假设订单系统正常每秒最多处理 5000 个请求,但在秒杀活动中,请求量瞬间上涨到每秒 10000 个。如果所有请求都通过 MQ 一次性推送给订单服务,订单服务很可能会被压垮。

这时可以使用 RabbitMQ 的消费端限流机制,让消费者一次只拉取固定数量的消息。这样即使队列中瞬间堆积大量请求,消费者也会按照自身处理能力逐批处理。

配置手动确认和 prefetch

使用消费端限流时,需要配合手动确认模式。因为 RabbitMQ 判断消费者是否还能继续接收消息,依赖的是"未确认消息数量"。

yaml 复制代码
spring:
  rabbitmq:
    listener:
      simple:
        acknowledge-mode: manual
        prefetch: 5

上面的配置表示:每个消费者最多同时持有 5 条未确认消息。

配置交换机和队列

可以声明一个直连交换机、一个队列,并通过路由键完成绑定。

java 复制代码
import com.bite.rabbitmq.constant.Constant;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Exchange;
import org.springframework.amqp.core.ExchangeBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.QueueBuilder;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class QosConfig {

    @Bean("qosExchange")
    public Exchange qosExchange() {
        return ExchangeBuilder
                .directExchange(Constant.QOS_EXCHANGE_NAME)
                .durable(true)
                .build();
    }

    @Bean("qosQueue")
    public Queue qosQueue() {
        return QueueBuilder.durable(Constant.QOS_QUEUE).build();
    }

    @Bean("qosBinding")
    public Binding qosBinding(@Qualifier("qosExchange") Exchange exchange,
                              @Qualifier("qosQueue") Queue queue) {
        return BindingBuilder
                .bind(queue)
                .to(exchange)
                .with("qos")
                .noargs();
    }
}

一次发送多条消息

为了观察限流效果,可以一次性发送 20 条消息:

java 复制代码
@RequestMapping("/qos")
public String qos() {
    for (int i = 0; i < 20; i++) {
        rabbitTemplate.convertAndSend(
                Constant.QOS_EXCHANGE_NAME,
                "qos",
                "qos test..." + i
        );
    }
    return "发送成功";
}

消费者监听队列

消费者监听队列后,先打印消息内容和 deliveryTag。如果暂时不调用 basicAck,就可以观察到 RabbitMQ 的限流效果。

java 复制代码
import com.bite.rabbitmq.constant.Constant;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class QosQueueListener {

    @RabbitListener(queues = Constant.QOS_QUEUE)
    public void listenerQueue(Message message, Channel channel)
            throws Exception {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        System.out.printf("接收到消息: %s, deliveryTag: %d%n",
                new String(message.getBody(), "UTF-8"),
                deliveryTag);

        // 手动确认后,RabbitMQ 才会继续向该消费者推送新的消息
        // channel.basicAck(deliveryTag, true);
    }
}

prefetch: 5 的配置下,如果消费者没有确认消息,控制台最多只会打印 5 条消息。此时管理界面中可以看到:还有 15 条消息处于 Ready 状态,5 条消息处于 Unacked 状态。

如果去掉 prefetch: 5 配置,消费者可能会一次性收到全部 20 条消息。对于处理能力有限的服务来说,这就很容易形成瞬时压力。

更合理的消费者负载分配

prefetchCount 不仅可以限流,还可以让多个消费者之间的消息分配更加合理。

默认轮询分发只关心"该轮该发给谁",不关心消费者是否已经处理完上一条消息。因此,当两个消费者处理速度不同,一个很快、一个很慢时,慢消费者仍然可能持续拿到新消息,导致它手里的任务越积越多。

一种常见优化方式是将 prefetch 设置为 1

yaml 复制代码
spring:
  rabbitmq:
    listener:
      simple:
        acknowledge-mode: manual
        prefetch: 1

这表示 RabbitMQ 每次只给消费者分配一条未确认消息。在消费者确认当前消息之前,不会继续给它推送新消息,而是把消息分配给其他空闲消费者。

启动两个消费者观察效果

下面的代码模拟两个消费者,其中第二个消费者通过 Thread.sleep(100) 模拟较慢的处理速度。

java 复制代码
import com.bite.rabbitmq.constant.Constant;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class QosQueueListener {

    @RabbitListener(queues = Constant.QOS_QUEUE)
    public void listenerQosQueue(Message message, Channel channel)
            throws Exception {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        System.out.printf("消费者1接收到消息: %s, deliveryTag: %d%n",
                new String(message.getBody(), "UTF-8"),
                deliveryTag);

        channel.basicAck(deliveryTag, true);
    }

    @RabbitListener(queues = Constant.QOS_QUEUE)
    public void listenerQueue2(Message message, Channel channel)
            throws Exception {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        System.out.printf("消费者2接收到消息: %s, deliveryTag: %d%n",
                new String(message.getBody(), "UTF-8"),
                deliveryTag);

        Thread.sleep(100);
        channel.basicAck(deliveryTag, true);
    }
}

当发送多条消息后,可以看到处理速度快的消费者会消费更多消息,而处理速度慢的消费者不会被持续塞满任务。这样整体消费能力会更加接近真实处理能力,而不是被固定轮询策略限制。

另外,日志中可能会看到两个消费者的 deliveryTag 出现重复。这是正常现象,因为 deliveryTag 是在每个 Channel 内部独立递增的,不同 Channel 之间并不共享同一个计数器。

总结

RabbitMQ 的事务机制主要用于保证消息操作的原子性。在 Spring AMQP 中,可以通过 RabbitTransactionManager、事务信道和 @Transactional 配合使用,让消息发送过程支持回滚。不过事务会影响性能,因此更适合一致性要求较高的场景。

消息分发机制则关注消费者之间如何分担消息。RabbitMQ 默认采用轮询分发,简单直接,但在消费者处理速度不一致时可能造成任务堆积。通过 prefetchCount 可以限制消费者持有的未确认消息数量,从而实现消费端限流,也可以让多个消费者之间的任务分配更加公平。

在实际项目中,可以按照下面的思路选择:

  1. 需要保证多条消息同时成功或失败时,考虑使用事务。
  2. 需要保护下游服务,避免瞬时流量压垮消费者时,配置手动确认和合理的 prefetch
  3. 多个消费者处理能力不一致时,可以设置 prefetch: 1,让 RabbitMQ 优先把消息分配给更空闲的消费者。
  4. 对性能要求很高的消息发送链路,应谨慎使用事务,并结合发送方确认机制设计可靠投递方案。

理解事务和消息分发后,RabbitMQ 就不只是一个简单的消息中转站,而是可以在可靠性、吞吐量和消费者负载之间进行细粒度调节的消息系统。

相关推荐
梅头脑10 小时前
交易跨10个库、日均千万订单——选错一次分布式事务方案,加班三个月重写
java·分布式
霸道流氓气质13 小时前
分布式系统中接口时序不确定性处理
java·开发语言·分布式
YOU OU15 小时前
Redis分布式锁
数据库·redis·分布式
小罗水17 小时前
第9章 RabbitMQ 消息队列与索引任务
分布式·rabbitmq
naierfengdian18 小时前
分布式风力发电机机抗风扰、稳发电结构设计技术解析
分布式·能源
wWYy.1 天前
基于Raft分布式Kv存储:sendRequestVote
分布式
笨鸟先飞的橘猫1 天前
游戏后端分布式学习——消息队列在游戏的用法
分布式·学习·游戏
Wang's Blog1 天前
Go-Zero项目开发40: 基于Jaeger的分布式链路跟踪实战
开发语言·分布式·golang
wWYy.1 天前
基于Raft分布式Kv存储:sendAppendEntries
分布式
范什么特西2 天前
关于kafka
分布式·kafka