目录
前言
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来实现消息队列。
队列是一个线性的数据结构,有两个基本操作,入队与出队——入队是将数据写入队尾,出队是取出队头的一个数据,也就是常说的FIFO(First Input First Out),先进先出。
Redis的List类型可以从列表的表头(最左边)或者表尾(最右边)插入元素,当然同样也可以从表头或者表尾删除元素。基于这个特性,假设List的最左边元素是队头,最右边元素是队尾,那么往List的右边插入元素就是入队,往List的最左边删除元素就是出队。
刚好,Redis的List类型就支持这样的操作,会用到的命令包括Rpush 、Lpop、Blpop。其中,Rpush命令用于将一个或多个值插入到列表的尾部(最右边),这里可以用来入队。Lpop与Blpop命令都可用于出队,其中Lpop命令用于移除并返回列表的第一个元素,当指定的key不存在时,返回nil;Blpop也是移除并返回列表的第一个元素,与Lpop命令不同的是,Blpop在列表中没有元素时会阻塞直到等待超时或发现可弹出元素为止。
这里用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 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 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秒内没有新的消息,继续。。。");
}
}
}
}
这里使用Redis的SortedSet类型实现延时队列。首先,SortedSet与Set一样都是String类型元素的集合,并且元素不允许有重复,与Set不同的地方在于SortedSet每一个元素会关联一个double类型的分数,redis通过这一个分数为SortedSet的成员排序,另外这一个分数值是可以重复的。
现在有这样的需求,将创建时间超过15分钟的订单关闭,基于SortedSet的特性可以这样做。
在生产者这边我们以一个SortedSet集合为延时队列,key为常量,这里定义为orderId。在创建订单时,将订单号写入这个key为orderId的SortedSet,也就是value存订单号的值,score存订单创建时往后推十五分钟的时间戳。
在消费者这边根据key获取集合中的所有元素,遍历这些元素,将score小于当前时间戳的value值删除,并消费这些消息。
实现延时队列会用到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 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 redisTemplate;
@Test
public void listenMessage() {
while(true){
Set> orderIdSet = redisTemplate.opsForZSet().rangeWithScores("orderId", 0, -1);
if(orderIdSet == null || orderIdSet.isEmpty()){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
continue;
}
Iterator> iterator = orderIdSet.iterator();
ZSetOperations.TypedTuple 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);
}
}
}
}
发布/订阅即Publish/Subscribe,是一种消息通信模式。在这种模式下可以有多个消费者订阅任意数量的频道,生产者往频道发送消息,订阅了该频道的所有消费者会收到消息。
Redis本身就支持发布/订阅,订阅频道的命令为SUBSCRIBE,发布消息的命令为PUBLISH,下面就以两个消费者订阅同一个频道,另有一个生产者发布消息为例,用Spring Boot + RedisTemplate实现功能并测试。
定义两个消费者,两个消费者中各有一个方法消费消息。
/**
* 消费者一
*/
public class SubscribeOne {
public void receive(String message) {
System.out.println("这里是一号订阅客户端,接收到信息:" + message);
}
}
/**
* 消费者二
*/
public class SubscribeTwo {
public void receive(String message) {
System.out.println("这里是二号订阅客户端,接收到信息:" + message);
}
}
在这一个配置中,定义了消息监听者容器与两个消息监听适配器。
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;
}
}
这里的生产者发布消息给“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 + "条消息");
}
}
}
启动这一个Spring Boot应用,直接在浏览器中通过URL访问,控制台打印结果如下。
这里是一号订阅客户端,接收到信息:发布的第1条消息
这里是一号订阅客户端,接收到信息:发布的第2条消息
这里是二号订阅客户端,接收到信息:发布的第2条消息
这里是二号订阅客户端,接收到信息:发布的第1条消息
这里是一号订阅客户端,接收到信息:发布的第3条消息
这里是二号订阅客户端,接收到信息:发布的第3条消息
这里是二号订阅客户端,接收到信息:发布的第4条消息
这里是一号订阅客户端,接收到信息:发布的第4条消息
这里是二号订阅客户端,接收到信息:发布的第5条消息
这里是一号订阅客户端,接收到信息:发布的第5条消息
这里是一号订阅客户端,接收到信息:发布的第6条消息
这里是二号订阅客户端,接收到信息:发布的第6条消息
这里是一号订阅客户端,接收到信息:发布的第7条消息
这里是二号订阅客户端,接收到信息:发布的第7条消息
这里是一号订阅客户端,接收到信息:发布的第8条消息
这里是二号订阅客户端,接收到信息:发布的第8条消息
这里是一号订阅客户端,接收到信息:发布的第9条消息
这里是二号订阅客户端,接收到信息:发布的第9条消息
这里是二号订阅客户端,接收到信息:发布的第10条消息
这里是一号订阅客户端,接收到信息:发布的第10条消息