一、應用場景
首先 我先引入一個大家熟知的觀點:Redis可以作爲消息隊列來使用。
我們在開發過程中,redis用的並不少,但是我想大部分我們都只把redis當作緩存使用,涉及到的數據結構也不會太多,其實redis的數據結構是十分精妙的,而所說的基於redis來寫一個消息隊列,就是基於redis的list(列表結構)。redis作爲消息隊列有兩種模式,一種是發佈-訂閱模式,一種是生產者-消費者模式,本文主要講的是後者。
二、關於redis的list
如下圖所示,redis的list底層結構其實是一個雙向鏈表,每個listNode節點都保存有prev和next指針,來指向他的前驅和後繼節點,所以這個數據結構的功能是十分強大的:左邊進右邊出就是一個隊列,左邊進左邊出就是一個棧......而redis的消息隊列其實就是前一種情況。
正在上傳…重新上傳取消正在上傳…重新上傳取消正在上傳…重新上傳取消
三、代碼結構
首先來看一下生產者的實現:produce方法就是push消息的核心方法,實現十分簡單,不作贅述。值得一提的是,每次有消息推入隊列時這邊都會將對應消費者喚醒,這也算是做的一個優化點,具體的下文會講。
public class DefaultProducer<T> implements Producer<T> {
@Autowired
private RedisTemplate redisTemplate;
/**
* 最大消息堆積數,默認10000條,暫時沒用,後期用於消息數量限制,防止消息無限堆積
*/
private Integer MAX_MESSAGE_SIZE;
private static Integer DEFAULT_MAX_MESSAGE_SIZE = 10000;
//當前消費者線程
private static volatile ConcurrentHashMap<String, Thread> currentConsumeThreads = new ConcurrentHashMap<>();
public DefaultProducer(){
this(DEFAULT_MAX_MESSAGE_SIZE);
}
public DefaultProducer(Integer maxMessageSize){
this.MAX_MESSAGE_SIZE = maxMessageSize;
}
@Override
public void produce(String key, T message) {
Assert.notNull(key,"queue's name can not be null!");
try{
redisTemplate.opsForList().leftPush(key, JSON.toJSONString(message));
if(currentConsumeThreads.containsKey("消費者線程"+key)){
LockSupport.unpark(currentConsumeThreads.get("消費者線程"+key));
log.info("消費者線程:{} -> 喚醒",currentConsumeThreads.get("消費者線程"+key).getName());
}
}catch (Exception e){
log.error("消息推送失敗,路由key:{},message:{}",key,JSON.toJSONString(message));
}
}
/**
* 設置當前消費者線程
* @param t 當前消費者線程
*/
public static void setCurrentConsumerThread(Thread t){
synchronized (DefaultProducer.class){
currentConsumeThreads.put(t.getName(),t);
}
}
}
來看一下消費者的實現:首先我這邊有提供一個默認的消費者基類,來支撐核心功能,所有自定義的消費者必須實現此類而不必關心細節。從下面代碼可以看到,基類的隊列路由key是由他的子類所提供的,只要在子類上面加上@MessageHandler註解,註解裏提供隊列名即可,要注意的是,子類必須是一個被spring管理的類。
再來看一下消費者的執行流程,基類會爲每個自定義的消費者創建一個線程,這個線程用while循環無限監聽消息,但是我們知道死循環是一個很耗性能的操作,尤其是沒有消息的時候就完全是做無用功,所以這裏在取不到消息的時候會將當前線程掛起,然後前面所提到的生產者對這個隊列推送消息時,消費者線程會被喚醒,大大提高了性能。
public abstract class BaseConsumer implements Consumer{
/**
* 消息key
*/
private String key;
private Thread worker;
@Autowired
private RedisTemplate redisTemplate;
public BaseConsumer(){
this.key = this.getClass().getAnnotation(MessageHandler.class).key();
}
@PostConstruct
public void start(){
init();
}
@Override
public void consume(Object message){
throw new UnsupportedOperationException();
}
public final void init(){
if(worker == null){
worker = new Thread(()->{
//暫時先寫成死循環 ,但是在沒有消息取的時候會造成空轉,後期優化(以優化)
while(true){
Object message = redisTemplate.opsForList().rightPop(key);
if (message == null){
//沒有消息則將當前線程掛起,避免循環空轉
log.info("當前消費者線程:{},未取到消息",Thread.currentThread().getName());
DefaultProducer.setCurrentConsumerThread(worker);
LockSupport.park();
}
consume(message);
}
});
worker.start();
}
}
}
四、關於使用
第一步:注入生產者,推送自定義消息
正在上傳…重新上傳取消正在上傳…重新上傳取消正在上傳…重新上傳取消正在上傳…重新上傳取消
第二步:實現對應key的自定義消費者,註解裏的就是你要監聽的key
正在上傳…重新上傳取消正在上傳…重新上傳取消正在上傳…重新上傳取消正在上傳…重新上傳取消
第三步:用postman工具測試