redis实现队列_json

Redis设计用来做缓存的,但是由于它自身的某种特性使得它可以用来做消息队列。

它有几个阻塞式的API可以使用,正是这些阻塞式的API让其有能力做消息队列;

另外,做消息队列的其他特性例如FIFO(先入先出)也很容易实现,只需要一个list对象从头取数据,从尾部塞数据即可。

Redis能做消息队列还得益于其list对象blpop brpop接口以及Pub/Sub(发布/订阅)的某些接口,它们都是阻塞版的,所以可以用来做消息队列。(List : lpush / rpop)

一.生产者消费者模式

1.简介

1.使用list结构作为队列,rpush生产消息,lpop消费消息,当lpop没有消息的时候,要适当sleep一会再重试。

或者,不用sleep,直接用blpop指令,在没有消息的时候,它会阻塞住直到消息到来。但是redis没有akc功能

2.代码

@Component
@RequestMapping("/RedisApplication")
public class RedisApplication {

@Autowired
private RedisTemplate redisTemplate;

@RequestMapping(value = "/testFIFO")
public void testFIFO() throws InterruptedException {
System.out.println("---------------开始放入队列--------------");
for (int i = 0; i < 5; i++) {
String arg = "key" + i;
redisTemplate.opsForList().leftPush("FIFOKEY", arg);
}
System.out.println("----------------放入队列停止------------");
while (true) {
Object outKey = redisTemplate.opsForList().rightPop("FIFOKEY");
if (outKey != null) {
System.out.println(outKey);
} else {
Thread.sleep(500);
}
}
}
}


二.发布订阅者模式

1.简介

使用pub/pub/sub主题订阅者模式,可以实现1:N的消息队列。

缺点:在消费者下线的情况下,生产的消息会丢失。此场景,建议用MQ。

2.代码

被请求的接口(被订阅者),使用redis队列,放于队列中。

package com.airboot.bootdemo.controller;

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


@RestController
@RequestMapping("/SentRedisController")
public class SentRedisController {

@Autowired
private RedisTemplate redisTemplate;

/**
* redis生产者测试
* @param data
* @return
*/
@GetMapping("/send1")
String send1(String data) {
redisTemplate.convertAndSend("testkafka", data);
return "success";
}
/**
* redis生产者测试
* @param data
* @return
*/
@GetMapping("/send2")
String send2(String data) {
redisTemplate.convertAndSend("testkafka1", data);
return "success";
}
}

配置监听器。监听队列。

package com.airboot.bootdemo.config;

import com.airboot.bootdemo.controller.RedisSubscriber;
import com.airboot.bootdemo.controller.RedisSubscriberTwo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;


@Configuration
public class RedisConfig {

@Autowired
private RedisTemplate redisTemplate;

//序列化通用设置
@Bean
public RedisTemplate redisTemplateInit() {
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
// 设置序列化Key的实例化对象
redisTemplate.setKeySerializer(new StringRedisSerializer());
// 设置序列化Value的实例化对象
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
return redisTemplate;
}

//配置监听 配置使类RedisSubscriber RedisSubscriberTwo 去监听testkafka1,
//testkafka这两个队列
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
RedisSubscriber listenerAdapter,
RedisSubscriberTwo listenerAdapter2){
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
//订阅了一个叫chat 的通道
container.addMessageListener(listenerAdapter, new PatternTopic("testkafka"));
container.addMessageListener(listenerAdapter, new PatternTopic("testkafka1"));//配置要订阅的订阅项
container.addMessageListener(listenerAdapter2, new PatternTopic("testkafka"));//配置要订阅的订阅项
//这个container 可以添加多个 messageListener
return container;
}

}

可以多个消费者通过上面配置多个类的监听就可以。

package com.airboot.bootdemo.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.stereotype.Component;

@Component
public class RedisSubscriberTwo extends MessageListenerAdapter {

@Autowired
private RedisTemplate<String, String> redisTemplate;

@Override
public void onMessage(Message message, byte[] bytes) {
System.out.println(message);
byte[] body = message.getBody();
byte[] channel = message.getChannel();
String msg = redisTemplate.getStringSerializer().deserialize(body);
String topic = redisTemplate.getStringSerializer().deserialize(channel);
System.out.println("监听到topic为2" + topic + "的消息:" + msg);
}
}


package com.airboot.bootdemo.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.stereotype.Component;

@Component
public class RedisSubscriber extends MessageListenerAdapter {
//
@Autowired
private RedisTemplate<String, String> redisTemplate;

@Override
public void onMessage(Message message, byte[] bytes) {
System.out.println(message);
byte[] body = message.getBody();
byte[] channel = message.getChannel();
String msg = redisTemplate.getStringSerializer().deserialize(body);
String topic = redisTemplate.getStringSerializer().deserialize(channel);
System.out.println("监听到topic为" + topic + "的消息:" + msg);
}
}

三.延时队列

1.简介

上面的例子我们已经了一个简易的消息队列。我们继续思考一个现实的场景,假定这些是一些游戏商品,它需要添加"延迟销售"特性,在未来某个时候才可以开始处理这些游戏商品数据。 那么要实现这个延迟的特性,我们需要修改现有队列的实现。


  1. 在消息数据的信息中包含延迟处理消息的执行时间,如果工作进程发现消息的执行时间还没到,那么它将会在短暂的等待之后重新把消息数据推入队列中。(延迟发送消息)
  2. 使用有序集合来存储这些需要延时消费的消息数据,将任务的执行时间设置为分值,在开启一个工作进程查找有序集合里面是否有可以立刻执行的任务,如果有的话就从有序集合中移除消息并且消费。

2.代码

package com.airboot.bootdemo.utils;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;

import java.lang.reflect.Type;
import java.util.Set;
import java.util.UUID;


public class RedisDelayQueue<T> {


private String queueKey;

public RedisTemplate redisTemplate;

// fastjson 序列化对象中存在 generic 类型时,需要使用 TypeReference
private Type TaskType = new TypeReference<TaskItem<T>>() {
}.getType();

public RedisDelayQueue(RedisTemplate redisTemplate, String queueKey) {
this.queueKey = queueKey;
this.redisTemplate = redisTemplate;
}

static class TaskItem<T> {
public String id;
public T msg;
}

public void delay(T msg) {
TaskItem<T> item = new TaskItem<T>();
//分配唯一的uuid
item.id = UUID.randomUUID().toString();
item.msg = msg;
//fastjson序列化
String s = JSON.toJSONString(item);
ZSetOperations operations = redisTemplate.opsForZSet();
//塞入延时队列,5s后再试
operations.add(queueKey, s, System.currentTimeMillis() + 5000);
}

public void loop() {
while (!Thread.interrupted()) {
//只取一条
Set<String> values = redisTemplate.opsForZSet().rangeByScore(queueKey, 0, System.currentTimeMillis(), 0, 1);
if (values.isEmpty()) {
try {
//歇会继续
Thread.sleep(500);
} catch (InterruptedException e) {
break;
}
continue;
}
String s = values.iterator().next();
if (redisTemplate.opsForZSet().remove(queueKey, s) > 0) {
//多进程同时调用,只有一个会remove成功
TaskItem<T> task = JSON.parseObject(s, TaskType);
//执行业务逻辑
handleTask(task.msg);
}
}
}

private void handleTask(T msg) {
System.out.println(msg);
}
}

调用。

@RequestMapping("/testDelayQueue")
public void testDelayQueue() {
RedisDelayQueue queue = new RedisDelayQueue(redisTemplate, "DelayQueue");
Thread producer = new Thread() {

@Override
public void run() {
for (int i = 0; i < 10; i++) {
queue.delay("DelayQueue" + i);
}
}

};
Thread consumer = new Thread() {

@Override
public void run() {
queue.loop();
}

};
producer.start();
consumer.start();
try {
producer.join();
Thread.sleep(6000);
consumer.interrupt();
consumer.join();
} catch (InterruptedException e) {
}
}

3.原理

主要就是几个语句:

放入redis中 ,权值为当前时间后5秒 operations.add(queueKey, s, System.currentTimeMillis() + 5000);
//获取时间小于当前时间的 在set中的第一条数据 所以在5秒之后就获取到了上面那条数据
Set<String> values = redisTemplate.opsForZSet().rangeByScore(queueKey, 0, System.currentTimeMillis(), 0, 1);
移除上面查到的数据 模仿数据被消费 redisTemplate.opsForZSet().remove(queueKey, s)