手寫一個基於redis的消息隊列

一、應用場景

首先 我先引入一個大家熟知的觀點:Redis可以作爲消息隊列來使用。

我們在開發過​程中,redis用的並不少,但是我想大部分我們都只把redis當作緩存使用,涉及到的數據結構也不會太多,其實redis的數據結構是十分精妙的,而所說的基於redis來寫一個消息隊列,就是基於redis的list(列表結構)。redis作爲消息隊列有兩種模式,一種是發佈-訂閱模式,一種是生產者-消費者模式,本文主要講的是後者。

 

二、關於redis的list

如下圖所示,redis的list底層結構其實是一個雙向鏈表,每個listNode節點都保存有prev和next指針,來指向他的前驅和後繼節點,所以這個數據結構的功能是十分強大的:左邊進右邊出就是一個隊列,左邊進左邊出就是一個棧......而redis的消息隊列其實就是前一種情況。

1576583630525-02cd0d2f-5131-4794-a939-91c03c73e4dc.pnguploading.gif正在上傳…重新上傳取消1576583630525-02cd0d2f-5131-4794-a939-91c03c73e4dc.pnguploading.gif正在上傳…重新上傳取消1576583630525-02cd0d2f-5131-4794-a939-91c03c73e4dc.pnguploading.gif正在上傳…重新上傳取消image.png

 

三、代碼結構

 

首先來看一下生產者的實現: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();
        }
    }

}

四、關於使用

第一步:注入生產者,推送自定義消息

1576585107592-da2a9b54-380c-4b05-8966-2a6ce96fb8a9.pnguploading.gif正在上傳…重新上傳取消1576585107592-da2a9b54-380c-4b05-8966-2a6ce96fb8a9.pnguploading.gif正在上傳…重新上傳取消1576585107592-da2a9b54-380c-4b05-8966-2a6ce96fb8a9.pnguploading.gif正在上傳…重新上傳取消1576585107592-da2a9b54-380c-4b05-8966-2a6ce96fb8a9.pnguploading.gif正在上傳…重新上傳取消image.png

 

第二步:實現對應key的自定義消費者,註解裏的就是你要監聽的key

1576585173916-5b59253e-d99e-43fb-9fab-a86772e53253.pnguploading.gif正在上傳…重新上傳取消1576585173916-5b59253e-d99e-43fb-9fab-a86772e53253.pnguploading.gif正在上傳…重新上傳取消1576585173916-5b59253e-d99e-43fb-9fab-a86772e53253.pnguploading.gif正在上傳…重新上傳取消1576585173916-5b59253e-d99e-43fb-9fab-a86772e53253.pnguploading.gif正在上傳…重新上傳取消image.png

 

第三步:用postman工具測試

image.png

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章