使用Redis实现延迟队列

延迟队列的使用场景

1、 当分布式锁加锁失败时,将消息放入到延迟队列中处理

2、订餐通知:下单成功后60s之后给用户发送短信通知

3、在订单系统中,一个用户某个时刻下单之后通常有 30 分钟的时间进行支付,如果 30 分钟之内没有支付成功,那么这个订单需要关闭

Redis实现延迟队列的基本原理

生产者将数据放入到list队列中(调用delay函数),最关键的是利用redis的zset数据结构

jedis.zadd(queueKey,System.currentTimeMillis()+10000,s);

zadd的第二个参数是score值,这里是用当前的时间戳加上想要延迟的秒数来作为score值,第三个参数是value值

消费者多线程轮询 zset 获取到期的任务进行处理

Set values=jedis.zrangeByScore(queueKey,0,System.currentTimeMillis(),0,1);

zrangeByScore(String key, double min, double max, int offset, int count)这个函数可以看出是获取0秒到当前时间戳的数据的一条数据进行处理,这里offset是0,count 是1,表示按照分数大小从小到大进行消费

这里我突然想到数据结构中的队列,是按照入队的顺序进行消费的,如果说想要按照逆序进行消费呢?虽然不知道项目项目场景中有没有这样的情况。。。。。

获取到数据之后再调用zrem函数将消费的数据进行移除

jedis.zrem(queueKey,s)>0

代码测试

import cn.hutool.core.lang.TypeReference;
import com.alibaba.fastjson.JSON;
import redis.clients.jedis.Jedis;


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

public class RedisDelayingQueue<T> {
    static class TaskItem<T>{
        public String id;
        public T msg;
    }
    // fastjson 序列化对象中存在 generic 类型时,需要使用 TypeReference
    private Type TaskType=new TypeReference<TaskItem<T>>() {}.getType();
    private Jedis jedis;
    private String queueKey;
    public RedisDelayingQueue(Jedis jedis, String queueKey){
        this.jedis=jedis;
        this.queueKey=queueKey;
    }
    public void delay(T msg){
        TaskItem taskItem=new TaskItem();
        taskItem.id= UUID.randomUUID().toString();
        taskItem.msg=msg;
        System.out.println("放入的顺序"+taskItem.msg);
        //将消息序列化成一个字符串作为 zset 的 value,这个消息的到期处理时间作为 score
        String s= JSON.toJSONString(taskItem);
        jedis.zadd(queueKey,System.currentTimeMillis()+10000,s);
    }
    //然后用多个线程轮询 zset 获取到期的任务进行处理,多个线程是为了保障可用性,万一挂了一个线程还有其它线程可以继续处
    public void loop(){
        while (!Thread.interrupted()){
            Set values=jedis.zrangeByScore(queueKey,0,System.currentTimeMillis(),0,1);
            if(values.isEmpty()){
                try{
                    Thread.sleep(500);
                }catch(InterruptedException e){
                    break;
                }
                continue;
            }
            String s= (String) values.iterator().next();
            //zrem 方法是多线程多进程争抢任务的关键,通过 zrem来决定唯一的属主
            if(jedis.zrem(queueKey,s)>0){
                TaskItem taskItem=JSON.parseObject(s,TaskType);
                System.out.println("消费的顺序"+taskItem.msg);
                this.handleMsg((T) taskItem.msg);
            }
        }
    }
    //要对 handle_msg 进行异常捕获,避免因为个别任务处理问题导致循环异常退出
    public void handleMsg(T msg){
        System.out.println(msg);
    }

    public static void main(String[] args) {
        System.out.println("程序开始运行");
        Jedis jedis=new Jedis("127.0.0.1",6379);
        RedisDelayingQueue queue=new RedisDelayingQueue<>(jedis,"q-demo");
        Thread produce=new Thread(){
            public void run(){
                for(int i=0;i<10;i++){
                    queue.delay("codehole"+i);
                }
            }
        };
        Thread consumer=new Thread(){
            public void run(){
                for(int i=0;i<10;i++){
                    queue.loop();
                }
            }
        };
        produce.start();
        consumer.start();
        try{
            produce.join();
//            Thread.sleep(6000);
//            consumer.interrupt();
            consumer.join();
        }catch (InterruptedException e){
        }
    }


}
复制代码
分类:
后端