RabbitMQ最佳实践总结

探讨RabbitMQ中消息持久化、发送确认、消费确认、幂等性及异常处理等关键问题,确保消息准确投递,提升系统可靠性。

在使用消息机制时,我们通常需要考虑以下几个问题:

  • 消息不能丢失
  • 保证消息一定能投递到目的地
  • 保证业务处理和消息发送/消费的一致性

本文以RabbitMQ为例,讨论如何解决以上问题。

消息持久化

如果希望RabbitMQ重启之后消息不丢失,那么需要对以下3种实体均配置持久化:

  • exchange
  • queue
  • message

声明exchange时设置持久化(durable = true)并且不自动删除(autoDelete = false):

 

RabbitMQ最佳实践总结,帮你少踩很多坑,收藏了

 

 

声明queue时设置持久化(durable = true)并且不自动删除(autoDelete = false):

 

RabbitMQ最佳实践总结,帮你少踩很多坑,收藏了

 

 

发送消息时通过设置deliveryMode=2持久化消息:

 

RabbitMQ最佳实践总结,帮你少踩很多坑,收藏了

 

 

发送确认

有时,业务处理成功,消息也发了,但是我们并不知道消息是否成功到达了rabbitmq,如果由于网络等原因导致业务成功而消息发送失败,那么发送方将出现不一致的问题,此时可以使用rabbitmq的发送确认功能,即要求rabbitmq显式告知我们消息是否已成功发送。

首先需要在channel上设置ConfirmListener:

 

RabbitMQ最佳实践总结,帮你少踩很多坑,收藏了

 

 

然后在发送消息直线需要开启发送确认模式:

//开启发送者确认channel.confirmSelect();

然后发送消息:

channel.basicPublish("order", "order.created", false, properties, "sample-data".getBytes());

当消息正常投递时,rabbitmq客户端将异步调用handleAck()表示消息已经成功投递,此时程序可以自行处理投递成功之后的逻辑,比如在数据库中将消息设置为已发送。当消息投递出现异常时,handleNack()将被调用。

通常来讲,发送端只需要保证消息能够发送到exchange即可,而无需关注消息是否被正确地投递到了某个queue,这个是rabbitmq和消息的接收方需要考虑的事情。基于此,如果rabbitmq找不到任何需要投递的queue,那么rabbitmq依然会ack给发送方,此时发送方可以认为消息已经正确投递,而不好用关系消息没有queue接收的问题。但是,对于rabbitmq而言,这种消息是需要记录下来的,否则rabbitmq将直接丢弃该消息。此时可以为exchange设置alternate-exchange,即表示rabbitmq将把无法投递到任何queue的消息发送到alternate-exchange指定的exchange中,通常来说可以设置一个死信交换(DLX)。

事实上,对于exchange存在但是却找不到任何接收queue时,如果发送是设置了mandatory=true,那么在消息被ack前将return给客户端,此时客户端可以创建一个ReturnListener用于接收返回的消息:

 

RabbitMQ最佳实践总结,帮你少踩很多坑,收藏了

 

 

但是需要注意的是,在return之后,消息依然会被ack而不是nack,还不如不设置madatory呢,因此return有时并不见得有用。

需要注意的是,在发送消息时如果exchange不存在,rabbitmq直接丢弃该消息,并且不会ack或者nack操作,但是在Spring中,会nack。

综合起来,要完成发送方确认,需要做以下几个点:

  • 设置ConfirmListener
  • 通过confirmSelect()开启
  • 为exchange设置alternate-exchange到DLX
  • 发送时没有必要设置mandotory
  • 发送方将消息记录在数据库中,收到ack时在数据库中标记消息为已发送状态
  • 如果收到reject或者由于网络原因没有收到ack,那么消息状态不会改变,下次发送时再次发送,此时可能导致消息重复,解决重复问题请参考“保证至少一次投递,并且消费端幂”小节。

手动消费确认

有时,消息被正确投递到消费方,但是消费方处理失败,那么便会出现消费方的不一致问题。比如订单已创建的消息发送到用户积分子系统中用于增加用户积分,但是积分消费法处理却都失败了,用户就会问:我购买了东西为什么积分并没有增加呢?

要解决这个问题,需要引入消费方确认,即只有消息被成功处理之后才告知rabbitmq以ack,否则告知rabbitmq以nack,此时的处理流程如下:

  1. 接收消息,不作ack,处理消息成功则ack,不成功nack
  2. 对于nack的消息,可以配置rabbitmq要么重新投递,要么直接扔掉,要么传到死信交换(DLX)
  3. 如果处理成功,但是由于网络等问题导致确认(无论是ack还是nack)不成功,那么rabbitmq会重新投递消息,但是此时由于消息已经成功,重新投递便导致了消费重复的消息,此时请参考“保证至少一次投递,并且消费端幂”小节。

在rabbitmq中,消息默认是字段ack的,即消息到达消费方立即ack,而不管消费方业务处理是否成功,为此可以开启手动确认模式,即有消费方自行决定何时应该ack,通过设置autoAck=false开启手动确认模式:

 

RabbitMQ最佳实践总结,帮你少踩很多坑,收藏了

 

 

可以看到,在autoAck=false情况下,通过业务处理的是否成功(success())来判断应该ack还是nack。

另外,为了避免消息反复requeue的情况,如果消息第一次消费不成功,则在nack时设置requeue=true,表示告知rabbitmq将reject的消息重新投递,如果第二次消费依然不成功,那么nack时设置requeue=false,告知rabbitmq不要重新投递了,此时rabbitmq将根据自己的配置要么直接扔掉消息,要么将消息发送到DLX中,具体配置请参考“设置死信交换(DLX)和死信队列(DLQ)”。

保证至少一次投递,并且消费端幂等

通常来说,程序中会先完成写数据库的操作,然后发送消息,此时一个重要的点是保证这两者的一致性,即一旦数据库保存成功消息必须也能够发送成功。要保证发送发一致性,一种做法是使用全局事务,即将数据库操作和消息发送放到一个事务中,比如JTA,但是全局事务是很重的,并且rabbitmq目前并不支持全局事务。

要解决发送发的一致性问题,可以实现将消息保存到数据库的事件表中,此时业务处理的数据库操作和保存消息到数据库属于同一个本地数据库事务,那么到此可以保证业务处理和消息产生的原子性,然后有一个异步的后台任务从数据库的事件表中一次读取未发送的消息发送至rabbitmq,发送成功后更新消息的状态为已发布。

然而,此时我们依然无法保证发送消息和更新消息状态之间的原子性,因为可能发生消息发送成功但是数据库状态更新不成功的情况,为了解决这种极端情况,可以多次重试消息发送,步骤如下:

  1. 读取时间表中未发送消息,发送到rabbitmq
  2. 如果发送成功,事件表中消息状态也更新成功,皆大欢喜
  3. 如果消息发送不成功,那么消息状态也不作改变,下次重试
  4. 如果消息发送成功而状态更新不成功,下次重试

不断重试,总有一个能够达到发送消息和状态更新的原子性。

那么问题也来了:rabbitmq中可能出现多条重复消息,此时消费端就懵了。为了解决这个问题,消费方应该设计为幂等的,即对相同消息的多次消费与单次消费结果相同。有些消费方的业务逻辑本身便是幂等的,而对于本身不幂等的消费方,需要在数据库中记录已经被正确消费的消息,当重复消息来时,判断该消息是否已经被消费,如果没有则执行消费逻辑,如果已经消费则直接忽略。此时消费方的处理步骤如下:

  1. 接收到消息,判断消息是否已经消费,如果是,则直接忽略,此时已然需要做消费成功确认
  2. 如果消息还未被消费,则处理业务逻辑,记录消息,业务逻辑本身和记录消息在同一个数据库事务中,如果都成功,则皆大欢喜;如果失败,那么消费方业务回滚,消息也不记录,此时reject消息,等下次重发

设置消息的TTL和消息队列的max-length

为了保证消息的时效性,可以设置队列中消息的TTL(x-message-ttl),而为了保证消息队列不至于太大而影响性能,可以设置队列的最大消息数(x-max-length)。在创建队列时设置如下:

 

RabbitMQ最佳实践总结,帮你少踩很多坑,收藏了

 

 

设置死信交换(DLX)和死信队列(DLQ)

对于无法投递的消息,我们需要将其记录下来便于后续跟踪排查,此时可以将这样的消息放入DLX和DLQ中。默认情况下,queue中被抛弃的消息将被直接丢掉,但是可以通过设置queue的x-dead-letter-exchange参数,将被抛弃的消息发送到x-dead-letter-exchange做指定的exchange中,这样的exchange成为DLX。

设置了x-dead-letter-exchange之后,在以下三种情况下消息将被扔到DLX中:

  1. 消费方nack时指定了requeue=false
  2. 消息的TTL已到
  3. 消息队列的max-length已到

在声明queue时定义x-dead-letter-exchange:

 

RabbitMQ最佳实践总结,帮你少踩很多坑,收藏了

 

 

  • 设置DLQ为lazy,并且没有TTL,并且没有max-length
    在以下3种情况下,消息会被投递到DLX中:

需要注意的是,在发送消息时,当已经达到queue的上限,而当queue定义为x-overflow=reject-publish时,rabbitmq将nack。当有多个queue同时绑定到exchange时,如果有些queue设置了reject-publish,而有些却没有,那么依然会nack,这对发送方来说不好处理。因此,还是那句话,发送方只需要保证正确地投递到了exchange即可,而不用关系exchange后面有哪些queue。

设置Prefetch count

Prefetch count表示消费方一次性从rabbitmq读取的消息数量,如果设置过大,那么消费方可能始终处于高负荷运转状态,而如果太小又会增加网络开销,通常设置为20-50。另外,有时为了保证多个消费方均衡地分摊消息处理任务,通常设置prefetch count为1。

异常处理

在以上设置的情况下,我们来看看当各种异常发生时,rabbitmq是如何运作的:

  • broker不可达:直接抛出异常;
  • 发送方自己始终发送不出去:消息状态始终处于“未发送”,不会破坏一致性,但是对于事件表中累计太多的事件需要关注;
  • exchange不存在:消息被丢掉,rabbitmq不会ack,消息状态始终处于“未发送”,下次将重新发送,不会破坏一致性,但是当exchange持续不存在下去,那么事件表中事件也会累计太多;
  • exchange存在但是没有接受queue:消息将被ack并标记为“已发送”,但由于设置了alternative exchange为dlx,那么消息将发送到dlx对应的dlq中保存以便后续处理;
  • consumer不在线,而累积消息太多:消息一致性没有问题,但是当累计到了max-length上限,消息队列头部的消息将被放置dlq中以便后续处理;
  • consumer临时性失败:通过redelivered判断是否为重复投递,如果是,则nack并且requeue=false,表示如果重复投递的一次的消息如果再失败,那么直接扔到dlx中,也即消息最多重复投递一次;
  • consumer始终失败:所有消息均被投入dlq以便后续处理,此时可能需要关注dlq的长度是否太长。

路由策略

系统中往往会发布多种类型的消息,在发送时有几种路由策略:

  • 所有类型的消息都发送到同一个exchange中
  • 每种类型的消息都单独配置一个exchange
  • 对消息类型进行归类,同一类型的消息对应一个exchange

笔者建议采用最后一种,并且结合DDD中的聚合划分,路由策略建议如下:

每一个聚合根下发布的所有类型的事件对应一个exchange,exchange设置为topic,queue可以配置接收某一种类型的事件,也可以配置接收所有某种聚合相关的事件,还可以配置接收所有事件。

<think> 我们正在讨论RabbitMQ连接和channel管理的最佳实践。结合之前关于channel关闭后仍在消费的问题,这里需要系统性地总结连接和channel的管理方法。 RabbitMQ中,连接(Connection)和通道(Channel)是核心概念: - 连接:一个TCP连接,通常每个应用程序(或服务)维护一个到RabbitMQ服务器的连接(除非有特殊需求需要多个连接)。 - 通道:在连接内部创建的虚拟连接(轻量级),用于执行大部分的操作(发布消息、消费、事务等)。一个连接可以包含多个通道,通道是线程隔离的。 最佳实践: 1. **连接管理**: - 尽量保持长连接:频繁建立和关闭连接会消耗资源。通常,在应用程序启动时建立连接,并在整个生命周期中复用。 - 每个进程或服务实例使用一个连接:多个线程可以共享同一个连接,但通过不同的通道进行操作。 - 实现连接恢复机制:网络问题可能导致连接断开。使用自动恢复连接(客户端库通常提供,如Java客户端有`AutomaticRecoveryEnabled`)或手动实现重连逻辑。 2. **通道管理**: - 每个线程使用独立的通道:通道不是线程安全的,因此每个线程应该创建并使用自己的通道。避免多个线程共享同一个通道。 - 通道的创建和关闭:通道的创建开销较小,但也要避免过度创建(例如,为每个操作都创建新通道)。通常,为每个需要执行操作的线程创建一个通道,并尽量复用。当不再需要时(如线程结束)关闭通道。 - 限制通道数量:虽然一个连接可以支持多个通道,但过多的通道会增加RabbitMQ的负担。根据实际需求创建通道。 3. **错误处理**: - 监听连接和通道的关闭事件:注册监听器来处理连接或通道的关闭,以便进行重连或清理。 - 处理通道级别的异常:通道操作(如发布消息)可能会抛出异常,需要捕获并处理(例如重试或记录)。 4. **资源释放**: - 确保关闭不再使用的通道:虽然连接关闭时会自动关闭所有通道,但显式关闭通道是良好的实践。 - 关闭顺序:先关闭所有通道,再关闭连接。 5. **性能优化**: - 确认模式(Publisher Confirm)和事务:对于消息可靠性要求高的场景,使用确认模式(轻量级)而不是事务(重量级)。 - 预取(QoS):在消费者端设置合理的预取值(`basicQos`),以避免单个消费者过载。 6. **消费者管理**: - 如前所述,在关闭通道前显式取消消费者(`basicCancel`)。 - 使用消费者标签(consumer tag)来管理消费者。 示例代码(Java)展示如何安全地创建连接和通道,并实现自动恢复: ```java import com.rabbitmq.client.*; public class RabbitMQExample { public static void main(String[] args) throws Exception { ConnectionFactory factory = new ConnectionFactory(); factory.setHost("localhost"); factory.setAutomaticRecoveryEnabled(true); // 启用自动恢复 try (Connection connection = factory.newConnection(); Channel channel = connection.createChannel()) { // 声明队列 channel.queueDeclare("my_queue", false, false, false, null); // 设置预取值 channel.basicQos(10); // 创建消费者 DeliverCallback deliverCallback = (consumerTag, delivery) -> { String message = new String(delivery.getBody(), "UTF-8"); try { // 处理消息... System.out.println("Processing: " + message); channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false); } catch (Exception e) { channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, true); // 重试 } }; String consumerTag = channel.basicConsume("my_queue", false, deliverCallback, tag -> {}); // ... 应用程序逻辑 ... // 关闭前取消消费者 channel.basicCancel(consumerTag); } // 自动关闭通道和连接(try-with-resources) } } ``` 注意事项: - 使用try-with-resources确保连接和通道正确关闭。 - 自动恢复连接(`setAutomaticRecoveryEnabled(true)`)可以在连接断开后自动恢复连接和重新注册消费者(但需要确保消费者处理逻辑是幂等的)。 总结: - 连接:长连接,每个应用实例一个。 - 通道:每个线程一个,复用,及时关闭。 - 错误处理:自动恢复,异常捕获。 - 资源释放:显式取消消费者,按顺序关闭。 相关问题: 1. RabbitMQ中如何实现消息的可靠传输? 2. RabbitMQ的消费者如何正确处理消息并保证幂等性? 3. RabbitMQ有哪些常见的性能调优方法? </think> ### RabbitMQ 连接与 Channel 管理最佳实践 RabbitMQ 的连接(Connection)和通道(Channel)是消息系统的核心组件,合理管理可显著提升性能、可靠性和资源利用率。以下是关键实践总结: --- #### **一、连接(Connection)管理实践** 1. **单应用复用长连接** - 每个应用进程只需维护 **1个 TCP 连接**(特殊场景除外)。 - 连接建立开销大(TCP 握手、TLS 协商),应复用而非频繁启闭。 - 示例:Spring Boot 中通过 `CachingConnectionFactory` 实现连接池[^1]。 2. **连接异常处理** - 监听连接中断事件(如 `ShutdownListener`),实现自动重连: ```java connection.addShutdownListener(cause -> { System.out.println("Connection lost, reconnecting..."); // 指数退避重连逻辑 }); ``` - 设置心跳检测(默认 580 秒),防止网络僵死连接: ```java ConnectionFactory factory = new ConnectionFactory(); factory.setRequestedHeartbeat(60); // 心跳间隔(秒) ``` 3. **连接安全性** - 使用 TLS 加密生产环境连接: ```java factory.useSslProtocol(); // 启用 SSL ``` - 避免硬编码凭据,通过环境变量注入密码。 --- #### **二、通道(Channel)管理实践** 1. **通道隔离原则** - **每个线程独享一个 Channel**:Channel 非线程安全,共享会导致竞争或消息乱序[^2]。 - 通道创建公式:设线程数为 $n$,则通道数 $C \approx n$(非固定,按需分配)。 2. **通道生命周期控制** - **显式关闭通道**:用后立即关闭,避免泄漏(尤其消费者侧): ```java try (Channel channel = connection.createChannel()) { // 业务操作 } // 自动关闭(try-with-resources) ``` - **关闭顺序**:先关闭所有 Channel,再关闭 Connection。 3. **消费者通道的特殊处理** - 关闭前显式取消消费者(`basicCancel`),防止消息残留: ```java channel.basicCancel(consumerTag); // 取消消费者 channel.close(); // 再关闭通道 ``` - 设置合理的预取值(QoS),避免消息积压: ```java channel.basicQos(10); // 每次最多推送 10 条消息 ``` --- #### **三、高可靠场景实践** 1. **生产者确认机制** - 启用 Publisher Confirms,确保消息落地 Broker: ```java channel.confirmSelect(); // 开启确认模式 channel.addConfirmListener((seq, multiple) -> { // 处理 ACK/NACK }); ``` 2. **消费者幂等性** - 消费逻辑需支持重复消息处理(如唯一 ID + 数据库去重)。 - 手动 ACK 时捕获异常,防止 channel 关闭导致消息丢失: ```java try { channel.basicAck(deliveryTag, false); } catch (AlreadyClosedException e) { // 记录日志并重试 } ``` 3. **连接/通道监控** - 通过 RabbitMQ Management API 监控连接数、通道状态。 - 关键指标报警:通道泄漏(持续增长)、未 ACK 消息数突增。 --- #### **四、性能优化实践** 1. **通道复用 vs 新建** - 频繁操作(如批量发消息)复用通道;短时操作用后即关。 - 避免为每个消息新建通道(开销 $O(1)$ 但累积影响大)。 2. **资源释放模式** ```mermaid graph LR A[启动应用] --> B[创建连接] B --> C[线程1创建Channel1] B --> D[线程2创建Channel2] C --> E[执行业务] D --> F[执行业务] E --> G[关闭Channel1] F --> H[关闭Channel2] G & H --> I[关闭连接] ``` 3. **客户端库选择** - Java:用 `amqp-client` + Spring `RabbitTemplate`(内置连接池)。 - Go:用 `streadway/amqp` 并实现连接重试逻辑。 - Python:`pika` 库需显式处理连接恢复。 --- #### **典型错误示例与修复** ❌ **错误**:多线程共享 Channel ```java // 线程1 channel.basicPublish("ex", "key", null, "msg1".getBytes()); // 线程2(并发) channel.basicPublish("ex", "key", null, "msg2".getBytes()); // 可能乱序或崩溃 ``` ✅ **修复**: ```java // 每个线程创建独立 Channel Thread thread1 = new Thread(() -> { try (Channel ch = connection.createChannel()) { ch.basicPublish(...); } }); ``` --- **最佳实践核心公式** RabbitMQ 资源管理效率 $E$ 可表示为: $$ E = \frac{\text{有效消息处理量}}{\text{连接开销 + 通道管理开销}} $$ 通过复用连接、隔离通道、及时释放资源,最大化 $E$ 值[^3]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值