redis实现队列

313 阅读3分钟

起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第5天,点击查看活动详情

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/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)