手把手教springboot整合rabbitmq發送延時消息
gitee demo地址:
一、Rabbit MQ 實現延時消息
1.死信隊列
通過死信隊列來間接實現延時消息,如果延時時間,不是一個固定的時間,則不建議使用這種方式。
(1) 具體實現
- 引入依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
<version>2.1.8.RELEASE</version>
</dependency>
- 聲明相關 exchange route-key queue
package com.lfg.message.delay.message.demo.delay.plugin;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* @author lfg
* @version v1.0
*/
@Configuration
public class RabbitMqConfig {
/**
* 流程:
* 1.消費者:重發隊列跟 重發交換機 重發路由綁定
* 2.死信交換機 死信路由不綁定消費者
* 3.死信消息找不到消費者,有效時間到,自動轉發到重發交換機
* 4.重發隊列消費成功
*/
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 死信交換機
*/
private String dlExchange = "dead-letter-exchange-name";
/**
* 死信 路由key
*/
private String dlRoutingKey = "dead-letter-routing-key-name";
/**
* 死信隊列
*/
private String dlQueue = "dead-letter-queue-name";
/**
* 重發交換機
*/
private String reExchange = "exchange-name";
/**
* 重發路由key
*/
private String reRoutingKey = "routing-key-name";
/**
* 重發隊列
*/
private String reQueue = "queue-name";
/**
* 聲明死信交換機
*/
@Bean("dlExchange")
public Exchange dlExchange() {
return ExchangeBuilder.directExchange(dlExchange).durable(true).build();
}
/**
* 聲明重發交換機
*/
@Bean("reExchange")
public Exchange reExchange() {
return ExchangeBuilder.directExchange(reExchange).durable(true).build();
}
/**
* 聲明死信隊列,指定死信交換機,死信路由
*/
@Bean("dlQueue")
public Queue dlQueue() {
Map<String, Object> args = new HashMap<>(4);
//指定交換機爲死信交換機
args.put("x-dead-letter-exchange", dlExchange);
//指定路由爲死信路由,值爲重發路由
args.put("x-dead-letter-routing-key", reRoutingKey);
return QueueBuilder.durable(dlQueue).withArguments(args).build();
}
/**
* 聲明重發隊列
*/
@Bean("reQueue")
public Queue reQueue() {
return QueueBuilder.durable(reQueue).build();
}
/**
* 死信隊列綁定 死信交換機 死信路由
*/
@Bean
public Binding deadLetterPushBinding() {
return new Binding(dlQueue, Binding.DestinationType.QUEUE, dlExchange, dlRoutingKey, null);
}
/**
* 重發隊列 綁定死信交換機 死信路由
*/
@Bean
public Binding redirectPushBinding() {
return new Binding(reQueue, Binding.DestinationType.QUEUE, dlExchange, reRoutingKey, null);
}
/**
* 重發隊列 綁定重發交換機 重發路由
*/
@Bean
public Binding reExchangeBinding() {
return new Binding(reQueue, Binding.DestinationType.QUEUE, reExchange, reRoutingKey, null);
}
}
- 生產者
package com.lfg.message.delay.message.demo.delay.dl;
import org.springframework.amqp.core.MessageDeliveryMode;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author lfg
* @version v1.0
*/
@RestController
public class Product {
@Autowired
private RabbitTemplate rabbitTemplate;
@RequestMapping("/product/message")
public String sendMessage() {
rabbitTemplate.convertAndSend("dead-letter-exchange-name",
"dead-letter-routing-key-name", "測試消息:Test message !", message -> {
//設定編碼
message.getMessageProperties().setContentEncoding("utf-8");
//永久有效
message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
//延時時間10s
message.getMessageProperties().setExpiration("10000");
return message;
});
return "已發送延時消息!";
}
}
- 消費者
package com.lfg.message.delay.message.demo.delay.dl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* @author lfg
* @version v1.0
*/
@Component
public class Consumer {
private static final Logger log = LoggerFactory.getLogger(Consumer.class);
@RabbitListener(queues = "queue-name")
public void consume(Message message) {
try {
log.info("正在消費消息---{}", new String(message.getBody()));
} catch (Exception e) {
//注意進行異常處理,默認當出現異常時,消息將會被 再次立即重新消費
}
}
}
(2) 潛在的坑
通過死信隊列來間接實現延時消息,如果延時時間,不是一個固定的時間,則不建議使用這種方式。
例如:一個請求推送一條消息兩次,第一次延時時間1小時,第二次延時時間2小時。
這時候有兩次請求過來:
第一個請求完成後,產生兩條延時消息 A 和 B ,過期時間是 1 和 2 。
第二個請求完成後,產生兩條延時消息 C 和 D ,過期時間是 1 和 2 。
理想情況:當消息過期時,則直接消費。消費順序爲 ACBD
實際情況:根據消息的生成順序延時消費。消費順序爲ABCD
結論:當消息過期時間一致時則沒有問題,但是當過期時間不一致,則會有大坑。
ps: 再消息B-C之間,B過期時間2小時,C過期時間1小時,C先過期,B後過期。
但消費順序像隊列一樣 先進先出,B先消費,C後消費,並且B消費後,立即消費C
通過建多個死信隊列可以解決這個問題,過期時間類別少還好,多的話,非常麻煩。
例如微信JSAPI支付成功,間隔時間回調通知便不適合建多個隊列。
2.延時插件
通過安裝延時插件來實現延時消息,此插件只適用於磁盤節點,內存節點會安裝失敗。
(1) 具體實現
- rabbitmq安裝延時插件,這裏不做過多贅述,百度/google相關教程。
官方插件鏈接
插件下載地址:rabbitmq_delayed_message_exchange
(2) 潛在的坑
此插件只能安裝在硬盤存儲中,無法安裝在內存存儲。
rabbitmq集羣,一個內存存儲,一個硬盤存儲。
對於生產環境不推薦使用此插件。
插件github地址
引用插件官方的一句話:
This plugin was created with disk nodes in mind. RAM nodes are currently unsupported and adding support for them is not a priority (if you aren’t sure what RAM nodes are and whether you need to use them, you almost certainly don’t).
二、Redis 實現延時消息
1.具體實現
推薦使用這種方式,但是需注意redis如果掛了,要保證存儲數據的完整性。
- 生產者
package com.lfg.message.delay.message.demo.delay.redis;
import org.redisson.api.RDelayedQueue;
import org.redisson.api.RQueue;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
/**
* @author lfg
* @version v1.0
*/
@RestController
public class Product3 {
@Autowired
private RedissonClient redissonClient;
/**
* 頻率爲10s/20s/30s
*/
private static final Long[] PUSH_DELAY = {10L, 20L, 30L};
@RequestMapping("/product/message3")
public String sendMessage() {
for (int i = 0; i < PUSH_DELAY.length; i++) {
//推送到隊列
RQueue<String> queue = redissonClient.getBlockingQueue("redis-key-name");
RDelayedQueue<String> delayedQueue = redissonClient.getDelayedQueue(queue);
delayedQueue.offer("測試消息-Test Message-" + PUSH_DELAY[i], PUSH_DELAY[i], TimeUnit.SECONDS);
delayedQueue.destroy();
}
return "已發送延時消息!";
}
}
- 消費者
package com.lfg.message.delay.message.demo.delay.redis;
import org.redisson.api.RBlockingQueue;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* @author lfg
* @version v1.0
*/
@Component
@EnableScheduling
public class Consumer3 {
private static final Logger log = LoggerFactory.getLogger(Consumer3.class);
@Autowired
private RedissonClient redissonClient;
@Scheduled(fixedDelay = 1000)
public void consume() throws InterruptedException {
RBlockingQueue<String> queue = redissonClient.getBlockingQueue("redis-key-name");
String message = queue.take();
log.info("正在消費推送信息:" + message);
//此種方式拋異常也會正常消費掉消息
}
}