開心一刻
今天坐在太陽下刷着手機
老媽走過來問我:這麼好的天氣,怎麼沒出去玩
我:我要是有錢,你都看不見我的影子
老媽:你就不知道帶個碗,別要邊玩?
我:......
優先級隊列
說到隊列,相信大家一定不陌生,是一種很基礎的數據結構,它有一個很重要的特點:先進先出
但說到優先級隊列,可能有些小夥伴還不清楚,因爲接觸的不多嘛
示例基於: RabbitMQ 3.9.11
業務場景
我手頭上正好有一個項目,系統之間通過 RabbitMQ 通信,調度系統 是消息生產者, 文件生成系統 是消息消費者
默認情況下,先下發的消息會先被消費,也就是先進隊列的消息會先出隊列
業務高峯期,重要程度不同的文件都需要生成,那如何保證重要文件先生成了?
1、調整調度
1.1 將重要文件的調度提前,保證重要文件的消息先進入隊列;但需要考慮調度能否提前,如果生成文件依賴的上游數據還未就緒了?
1.2 將普通文件的調度延後,有點圍魏救趙的感覺,萬一某一天不需要生成重要文件,那服務器豈不是有一段時間的空置期,而這段空置期本可以生成普通文件
總的來說就是不夠靈活:有重要文件的時候先生成重要文件,沒有重要文件的時候生成普通文件
2、提高服務器配置
這個就不用過多解釋了把,加大 文件生成系統 的硬件配置,提高其文件生成能力
保證文件(不論重要還是普通)都能在調度的時間開始生成,也就無需區分重要與普通了
那麼重要文件先生成這個命題就不成立了
想想都美,可實際情況,大家都懂的!
3、優先級隊列
RabbitMQ 的 Priority Queue 非常契合這個業務場景,詳情請往下看
隊列優先級
相較於普通隊列,優先級隊列肯定有一個標誌來標明它是一個優先級隊列
這個標誌就是參數: x-max-priority ,定義優先級的最大值
我們先來看下 RabbitMQ 控制檯如何配置
相關參數配置好之後,點擊 Add queue 即創建出了一個 優先級隊列
創建完成之後,你會發現隊列上有一個 Pri 標誌,說明這是一個優先級隊列
實際開發工程中,一般不會在 RabbitMQ 控制檯創建隊列,往往是服務啓動的時候,通過服務自動創建 exchange 、 queue
實現也非常簡單
@Configuration public class RabbitConfig { @Bean public DirectExchange directExchange() { return new DirectExchange(QSL_EXCHANGE, true, false); } @Bean public Queue queue() { Map<String, Object> args = new HashMap<>(); args.put("x-max-priority", 5); return new Queue(QSL_QUEUE, true, false, false, args); } @Bean public Binding bindingQueue() { return BindingBuilder.bind(queue()).to(directExchange()).with("com.qsl"); } }
服務啓動成功後,我們可以在 RabbitMQ 控制檯看到隊列: com.qsl.queue ,其 x-max-priority 等於 5
消息優先級
消息屬性 priority 可以指定消息的優先級
停止服務後,我們手動往隊列 com.qsl.queue 中放一些帶有優先級的消息
優先級分別是: 3,1,5,5,10,4 對應的消息體分別是: 3,1,5_1,5_2,10,4
此時隊列中共有 6 個消息準備就緒
啓動服務,進行消息消費,消費順序如下
可以總結出一個規律:優先級高的先出隊列,優先級相同的,先進先出
那優先級是 10 的那個消息是什麼情況,它爲什麼不是第一個出隊?
因爲隊列 com.qsl.queue 的最大優先級是 5,即使消息的優先級設置成 10,其實際優先級也只有 5,這樣是不是就理解了?
實際開發工程中,基本不會在 RabbitMQ 控制檯手動發消息,肯定是由服務發送消息
我們模擬下帶有優先級的消息發送
是不是 so easy !
x-max-priority
值支持範圍是 1 ~ 255 ,推薦使用 1 ~ 5 之間的值,如果需要更高的優先級則推薦 1 ~ 10
1 ~ 10 已經足夠使用,不推薦使用更高的優先級,更高的優先級值需要更多的 CPU 和 內存資源
沒有設置優先級的消息將被視爲優先級爲 0,優先級高於隊列最大優先級的消息將被視爲以隊列最大優先級發佈的消息
數據結構
底層數據結構:堆
具體請看:數據結構之堆 → 不要侷限於堆排序
ACK超時
之前一直不知道這一點,直到有一次碰到了如下異常
一查才知道ACK超時了
超時異常
從消費者獲取到消息(消息投遞成功)開始,在超時時間(默認30分鐘)內未確認回覆,則關閉通道,並拋出 PRECONDITION_FAILED 通道異常
並且消息會重新進入隊列,等待再次被消費
ACK超時的配置項: consumer_timeout ,默認值是 1800000 ,單位是毫秒,也就是 30 分鐘
可用命令 rabbitmqctl eval 'application:get_env(rabbit,consumer_timeout).' 查看
判斷是否ACK超時的調度間隔是一分鐘,所以 consumer_timeout 不支持低於一分鐘的值,也不建議低於五分鐘的值
我們將 consumer_timeout 調整成 2 分鐘,看看超時異常
有 2 種調整方式
1、修改 /etc/rabbitmq.conf
配置文件沒有則新建,然後在配置文件中將 consumer_timeout 設置成 120000 (沒有該配置項則新增)
然後重啓 rabbitmq
2、動態修改
執行命令 rabbitmqctl eval 'application:set_env(rabbit,consumer_timeout,120000).' 即可
不需要重啓 rabbitmq
需要注意的是,這種修改不是永久生效,一旦 rabbitmq 重啓, consumer_timeout 將會恢復到默認值
我們用第 2 種方式進行調整
然後我們在消費端睡眠 3 分鐘後進行ACK
最後在 rabbitmq 控制檯手動發送一個消息,異常信息如下
2024-02-15 13:08:47|org.springframework.amqp.rabbit.RabbitListenerEndpointContainer#0-1|com.qsl.rabbit.listener.TestListener|INFO|28|消費者接收到消息:6 2024-02-15 13:10:47|AMQP Connection 192.168.3.225:5672|org.springframework.amqp.rabbit.connection.CachingConnectionFactory|ERROR|1575|Channel shutdown: channel error; protocol method: #method<channel.close>(reply-code=406, reply-text=PRECONDITION_FAILED - delivery acknowledgement on channel 1 timed out. Timeout value used: 120000 ms. This timeout value can be configured, see consumers doc guide to learn more, class-id=0, method-id=0) 2024-02-15 13:11:47|org.springframework.amqp.rabbit.RabbitListenerEndpointContainer#0-1|com.qsl.rabbit.listener.TestListener|ERROR|33|消息確認異常: java.lang.IllegalStateException: Channel closed; cannot ack/nack at org.springframework.amqp.rabbit.connection.CachingConnectionFactory$CachedChannelInvocationHandler.invoke(CachingConnectionFactory.java:1175) at com.sun.proxy.$Proxy50.basicAck(Unknown Source) at com.qsl.rabbit.listener.TestListener.onMessage(TestListener.java:31) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.springframework.messaging.handler.invocation.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:171) at org.springframework.messaging.handler.invocation.InvocableHandlerMethod.invoke(InvocableHandlerMethod.java:120) at org.springframework.amqp.rabbit.listener.adapter.HandlerAdapter.invoke(HandlerAdapter.java:53) at org.springframework.amqp.rabbit.listener.adapter.MessagingMessageListenerAdapter.invokeHandler(MessagingMessageListenerAdapter.java:220) at org.springframework.amqp.rabbit.listener.adapter.MessagingMessageListenerAdapter.invokeHandlerAndProcessResult(MessagingMessageListenerAdapter.java:148) at org.springframework.amqp.rabbit.listener.adapter.MessagingMessageListenerAdapter.onMessage(MessagingMessageListenerAdapter.java:133) at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.doInvokeListener(AbstractMessageListenerContainer.java:1591) at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.actualInvokeListener(AbstractMessageListenerContainer.java:1510) at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.invokeListener(AbstractMessageListenerContainer.java:1498) at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.doExecuteListener(AbstractMessageListenerContainer.java:1489) at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.executeListener(AbstractMessageListenerContainer.java:1433) at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer.doReceiveAndExecute(SimpleMessageListenerContainer.java:975) at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer.receiveAndExecute(SimpleMessageListenerContainer.java:921) at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer.access$1600(SimpleMessageListenerContainer.java:83) at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer$AsyncMessageProcessingConsumer.mainLoop(SimpleMessageListenerContainer.java:1296) at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer$AsyncMessageProcessingConsumer.run(SimpleMessageListenerContainer.java:1202) at java.lang.Thread.run(Thread.java:745) 2024-02-15 13:11:47|org.springframework.amqp.rabbit.RabbitListenerEndpointContainer#0-1|org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer|INFO|1436|Restarting Consumer@2e6f610d: tags=[[amq.ctag-hE7fVqLNKO44ytMHalsf2A]], channel=Cached Rabbit Channel: AMQChannel(amqp://[email protected]:5672/,1), conn: Proxy@3b1ed14b Shared Rabbit Connection: SimpleConnection@13275d8 [delegate=amqp://[email protected]:5672/, localPort= 55710], acknowledgeMode=MANUAL local queue size=0 2024-02-15 13:11:47|org.springframework.amqp.rabbit.RabbitListenerEndpointContainer#0-2|com.qsl.rabbit.listener.TestListener|INFO|28|消費者接收到消息:6
從 RabbitMQ 3.12 開始,可以爲每個隊列配置過期時長,而之前只能爲每個 Rabbit 節點配置過期時長
如何處理
如果碰到ACK超時,那麼我們該如何處理
1、增加超時時長
這往往是最容易想到的,默認 30 分鐘不行就改成 60 分鐘嘛
但並不是無腦增加超時時長,默認值往往是綜合情況下比較優的一個值,並不推薦加長
2、異步處理
用線程池處理異步處理消息,及時進行消息ACK
但需要考慮拒絕策略,如果用的是: CallerRunsPolicy ,還是有可能觸發ACK超時
3、冪等處理
消息消費做冪等處理,是規範,而不僅僅只是針對ACK超時
消息正在消費中,或者已經消費完成,這個消息就不應該再次被消費,可以打印日誌然後直接ACK,而無需進行業務處理
4、自動ACK
雖然自動ACK可以簡化消息確認的流程,但它也可能帶來一些潛在的問題,例如:
消息丟失風險:自動ACK意味着一旦消費者接收到消息, RabbitMQ 就會將其從隊列中移除。如果消費者在處理消息時發生故障或崩潰,未處理的消息可能會丟失
限流作用減弱:ACK機制可以幫助限流,即通過控制ACK的發送速率來限制消費者處理消息的速度。如果使用自動ACK,這種限流作用會減弱,可能導致消費者過快地消費消息,超出其實際處理能力
缺乏靈活性:自動ACK不允許消費者在處理完消息後再決定是否要確認消息,這限制了消費者的靈活性。例如,消費者可能需要根據消息內容或處理結果來決定是否重新入隊或丟棄消息
等等
總之,自動ACK慎用
具體如何處理,需要結合具體業務,選擇比較合適的方式
總結
優先級隊列
通過配置 x-max-priority 參數標明隊列是優先級隊列
隊列的優先級取值範圍推薦 1 ~ 5 ,不推薦超過 10
通過屬性 priority 可以指定消息的優先級,沒有設置優先級的消息將被視爲優先級爲 0,優先級高於隊列最大優先級的消息將被視爲以隊列最大優先級發佈的消息
優先級高的消息先出隊列(先被處理),優先級低的消息後出隊列(後被處理),優先級相同的則是先進先出
ACK超時
ACK超時是一種保護機制,其實可以類比 HTTP 請求超時、數據庫連接查詢超時
RabbitMQ 的ACK超時默認是 30 分鐘,可以修改配置項 consumer_timeout 進行調整
至於如何避免ACK超時,需要結合具體的業務選擇合適的方式