目錄
3.4 RedisTemplate操作SortedSet實現延時隊列
4 Spring Boot環境中連接Redis實現發佈/訂閱
前言
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條消息