Reids實現消息隊列的思路與方式

 

目錄

前言

1 隊列的特點

2 使用List實現簡單隊列

2.1 思路

2.2 實現簡單隊列的相關命令

2.3 RedisTemplate操作List實現消息隊列

3 使用SortedSet實現延時隊列

3.1 延時隊列應用場景

3.2 思路

3.3 實現延時隊列相關命令

3.4 RedisTemplate操作SortedSet實現延時隊列

4 Spring Boot環境中連接Redis實現發佈/訂閱

4.1 兩個消費者

4.2 消息監聽配置

4.3 生產者

4.4 測試結果


前言

       Redis是現在最流行的key-value數據庫,有諸多應用場景,包括緩存、分佈式鎖、計數器等,本文介紹的是用Redis實現消息隊列。消息隊列有專門的中間件,包括RabbitMQ、RocketMQ、kafka等,它們都屬於重量級。在項目中,如果需求沒必要用到重量級的消息中間件,可以採用Redis來實現消息隊列。

1 隊列的特點

       隊列是一個線性的數據結構,有兩個基本操作,入隊出隊——入隊是將數據寫入隊尾,出隊是取出隊頭的一個數據,也就是常說的FIFO(First Input First Out),先進先出

 

2 使用List實現簡單隊列

2.1 思路

        Redis的List類型可以從列表的表頭(最左邊)或者表尾(最右邊)插入元素,當然同樣也可以從表頭或者表尾刪除元素。基於這個特性,假設List的最左邊元素是隊頭最右邊元素是隊尾,那麼往List的右邊插入元素就是入隊,往List的最左邊刪除元素就是出隊

2.2 實現簡單隊列的相關命令

        剛好,Redis的List類型就支持這樣的操作,會用到的命令包括Rpush 、Lpop、Blpop。其中,Rpush命令用於將一個或多個值插入到列表的尾部(最右邊),這裏可以用來入隊。Lpop與Blpop命令都可用於出隊,其中Lpop命令用於移除並返回列表的第一個元素,當指定的key不存在時,返回nil;Blpop也是移除並返回列表的第一個元素,與Lpop命令不同的是,Blpop在列表中沒有元素時會阻塞直到等待超時或發現可彈出元素爲止。

2.3 RedisTemplate操作List實現消息隊列

        這裏用SpringBoot + RedisTemplate實現簡單消息隊列,代碼包含兩部分,生產者(入隊)與消費者(出隊)。

        首先是入隊(生產者)代碼,這裏相當於是使用Rpush在key爲"simpleQueue"的List中寫入了三個值"Java"、"C++"與"Python"。

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.junit4.SpringRunner;


@RunWith(SpringRunner.class)
@SpringBootTest
public class MessageSender {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Test
    public void sendMessage() {
        // 往隊列的最右邊添加元素
        redisTemplate.opsForList().rightPushAll("simpleQueue", "Java", "C++", "Python");
    }
}

       然後是出隊(消費者)代碼,redisTemplate.opsForList().leftPop有多個重載方法,這裏的寫法相當於是調用了Blpop命令,並設置了60秒的超時時間,以阻塞的方式彈出元素。

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.dao.QueryTimeoutException;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.concurrent.TimeUnit;

@RunWith(SpringRunner.class)
@SpringBootTest
public class MessageListener {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Test
    public void listenMessage() {
        while(true) {
            try {
                String message = redisTemplate.opsForList().leftPop("simpleQueue", 60, TimeUnit.SECONDS);
                System.out.println("已消費隊列" + message);
            } catch (QueryTimeoutException e) {
                System.out.println("60秒內沒有新的消息,繼續。。。");
            }
        }
    }
}

 

3 使用SortedSet實現延時隊列

3.1 延時隊列應用場景

  • 創建的所有訂單如果15分鐘內未操作,自動取消;
  • 發送短信,11點整發送短信,2分鐘後再次執行;
  • 用戶下了外賣的單,商家在5分鐘內未接單,需要自動取消訂單。        

3.2 思路

       這裏使用Redis的SortedSet類型實現延時隊列。首先,SortedSet與Set一樣都是String類型元素的集合,並且元素不允許有重複,與Set不同的地方在於SortedSet每一個元素會關聯一個double類型的分數,redis通過這一個分數爲SortedSet的成員排序,另外這一個分數值是可以重複的。

       現在有這樣的需求,將創建時間超過15分鐘的訂單關閉,基於SortedSet的特性可以這樣做。

       在生產者這邊我們以一個SortedSet集合爲延時隊列,key爲常量,這裏定義爲orderId。在創建訂單時,將訂單號寫入這個key爲orderId的SortedSet,也就是value存訂單號的值,score存訂單創建時往後推十五分鐘的時間戳。

       在消費者這邊根據key獲取集合中的所有元素,遍歷這些元素,將score小於當前時間戳的value值刪除,並消費這些消息。

3.3 實現延時隊列相關命令

       實現延時隊列會用到SortedSet相關的命令有:

  • Zadd:用於將一個或多個元素及其分數值加入到有序集中;
  • Zrangebyscore:用於返回指定分數區間的成員元素列表,結果按分數由小到大排列;
  • Zrem:移除集合中一個或多個元素。

3.4 RedisTemplate操作SortedSet實現延時隊列

        依然是用SpringBoot + RedisTemplate,包含了生產者與消費者的代碼,當然RedisTemplate對命令作了封裝,API的名稱與redis本身的命令命名是不同的。

        生產者:

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.Calendar;
import java.util.Random;

@RunWith(SpringRunner.class)
@SpringBootTest
public class MessageSender {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Test
    public void sendMessage() {
        // 時間戳取當前時間往後推15分鐘,存入sorted-set的score值
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.MINUTE, 15);
        double millisecond = calendar.getTimeInMillis();
        // 以簡單的方式模擬訂單號
        Random random = new Random();
        int orderId = Math.abs(random.nextInt());
        redisTemplate.opsForZSet().add("orderId", String.valueOf(orderId), millisecond );
        System.out.println("發送訂單任務,訂單ID爲===============" + orderId);
    }
}

        消費者:

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.Iterator;
import java.util.Set;

@RunWith(SpringRunner.class)
@SpringBootTest
public class MessageListener {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Test
    public void listenMessage() {
        while(true){
            Set<ZSetOperations.TypedTuple<String>> orderIdSet = redisTemplate.opsForZSet().rangeWithScores("orderId", 0, -1);
            if(orderIdSet == null || orderIdSet.isEmpty()){
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    break;
                }
                continue;
            }
            Iterator<ZSetOperations.TypedTuple<String>> iterator = orderIdSet.iterator();
            ZSetOperations.TypedTuple<String> next = iterator.next();
            Double score = next.getScore();
            if(score == null) {
                continue;
            }
            double nowTime = System.currentTimeMillis();
            if(nowTime >= score) {
                String value = next.getValue();
                redisTemplate.opsForZSet().remove("orderId", value);
                System.out.println("已成功處理一條訂單,訂單id爲" + value);
            }
        }
    }
}

 

4 Spring Boot環境中連接Redis實現發佈/訂閱

        發佈/訂閱即Publish/Subscribe,是一種消息通信模式。在這種模式下可以有多個消費者訂閱任意數量的頻道,生產者往頻道發送消息,訂閱了該頻道的所有消費者會收到消息。

        Redis本身就支持發佈/訂閱,訂閱頻道的命令爲SUBSCRIBE,發佈消息的命令爲PUBLISH,下面就以兩個消費者訂閱同一個頻道,另有一個生產者發佈消息爲例,用Spring Boot + RedisTemplate實現功能並測試。

4.1 兩個消費者

        定義兩個消費者,兩個消費者中各有一個方法消費消息。

/**
 * 消費者一
 */
public class SubscribeOne {
    public void receive(String message) {
        System.out.println("這裏是一號訂閱客戶端,接收到信息:" + message);
    }
}
/**
 * 消費者二
 */
public class SubscribeTwo {
    public void receive(String message) {
        System.out.println("這裏是二號訂閱客戶端,接收到信息:" + message);
    }
}

4.2 消息監聽配置

        在這一個配置中,定義了消息監聽者容器與兩個消息監聽適配器。

import com.bigsea.Controller.ChatController;
import com.bigsea.subscribe.SubscribeOne;
import com.bigsea.subscribe.SubscribeTwo;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.stereotype.Component;

@Component
public class subscribeConfig {
    private static final String RECEIVE_NAME = "receive";
    /**
     * 消息監聽適配器一
     * @return MessageListenerAdapter
     */
    @Bean
    public MessageListenerAdapter listenerAdapterOne() {
        return new MessageListenerAdapter(new SubscribeOne(), RECEIVE_NAME);
    }

    /**
     * 消息監聽適配器二
     * @return MessageListenerAdapter
     */
    @Bean
    public MessageListenerAdapter listenerAdapterTwo() {
        return new MessageListenerAdapter(new SubscribeTwo(), RECEIVE_NAME);
    }

    /**
     * 定義消息監聽者容器
     * @param connectionFactory 連接工廠
     * @param listenerAdapterOne MessageListenerAdapter
     * @param listenerAdapterTwo MessageListenerAdapter
     * @return RedisMessageListenerContainer
     */
    @Bean
    public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
                                                   MessageListenerAdapter listenerAdapterOne,
                                                   MessageListenerAdapter listenerAdapterTwo) {
        RedisMessageListenerContainer listenerContainer = new RedisMessageListenerContainer();
        listenerContainer.setConnectionFactory(connectionFactory);
        listenerContainer.addMessageListener(listenerAdapterOne, new PatternTopic(ChatController.CHAT_NAME));
        listenerContainer.addMessageListener(listenerAdapterTwo, new PatternTopic(ChatController.CHAT_NAME));
        return listenerContainer;
    }
}

4.3 生產者

        這裏的生產者發佈消息給“myMessage”頻道,兩個消費者同樣也是監聽的這一頻道。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/chat")
public class ChatController {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    public static final String CHAT_NAME = "myMessage";

    private static final int MESSAGE_COUNT = 10;

    @GetMapping("/pub")
    public void publish() {
        for(int i = 1; i <= MESSAGE_COUNT; i++) {
            stringRedisTemplate.convertAndSend(CHAT_NAME, "發佈的第" + i + "條消息");
        }
    }
}

4.4 測試結果

        啓動這一個Spring Boot應用,直接在瀏覽器中通過URL訪問,控制檯打印結果如下。

這裏是一號訂閱客戶端,接收到信息:發佈的第1條消息
這裏是一號訂閱客戶端,接收到信息:發佈的第2條消息
這裏是二號訂閱客戶端,接收到信息:發佈的第2條消息
這裏是二號訂閱客戶端,接收到信息:發佈的第1條消息
這裏是一號訂閱客戶端,接收到信息:發佈的第3條消息
這裏是二號訂閱客戶端,接收到信息:發佈的第3條消息
這裏是二號訂閱客戶端,接收到信息:發佈的第4條消息
這裏是一號訂閱客戶端,接收到信息:發佈的第4條消息
這裏是二號訂閱客戶端,接收到信息:發佈的第5條消息
這裏是一號訂閱客戶端,接收到信息:發佈的第5條消息
這裏是一號訂閱客戶端,接收到信息:發佈的第6條消息
這裏是二號訂閱客戶端,接收到信息:發佈的第6條消息
這裏是一號訂閱客戶端,接收到信息:發佈的第7條消息
這裏是二號訂閱客戶端,接收到信息:發佈的第7條消息
這裏是一號訂閱客戶端,接收到信息:發佈的第8條消息
這裏是二號訂閱客戶端,接收到信息:發佈的第8條消息
這裏是一號訂閱客戶端,接收到信息:發佈的第9條消息
這裏是二號訂閱客戶端,接收到信息:發佈的第9條消息
這裏是二號訂閱客戶端,接收到信息:發佈的第10條消息
這裏是一號訂閱客戶端,接收到信息:發佈的第10條消息

 

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