Redis实现延时队列

373 阅读2分钟

概要

通过redis zset实现延时队列

实现

package com.funstar.redisdemo.delay.queue;

import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Service;
import com.google.common.util.concurrent.ThreadFactoryBuilder;


import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Set;
import java.util.concurrent.*;

/**
 * @author funstar
 * @date 2019/11/24
 */
@Service
public class DelayQueue implements InitializingBean {

    //延时队列在redis中的key
    private static final String QUEUE_KEY = "delayQueue";

    private static final int poolSize = 3;

    private static final DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    private static final ThreadFactory consumerThreadFactory = new ThreadFactoryBuilder().setNameFormat("consumer-pool-%d").build();

    private static final ExecutorService consumerExecutor = new ThreadPoolExecutor(poolSize, poolSize, 0L,
            TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), consumerThreadFactory);

    //消费者开关
    private boolean consumerSwitch = false;


    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    public void setConsumerSwitch(boolean consumerSwitch) {
        this.consumerSwitch = consumerSwitch;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        //poolSize个消费者线程
        for (int i = 0; i < poolSize; i++) {
            consumerExecutor.execute(new ConsumerJob());
        }

    }

    /**
     * 将msg加入到延时队列
     *
     * @param msg
     * @return
     */
    public boolean add(String msg) {
        LocalDateTime now = LocalDateTime.now();
        System.out.println("producer: " + dtf.format(now) + " " + msg);
        long score = now.toEpochSecond(ZoneOffset.of("+8")) * 1000 + 5000;//延迟5s
        return redisTemplate.opsForZSet().add(QUEUE_KEY, msg, score);
    }

    class ConsumerJob implements Runnable {

        @Override
        public void run() {
            while (true && !Thread.interrupted()) {
                try {
                    if (!consumerSwitch) {
                        Thread.sleep(2000);
                    } else {
                        Set<Object> values = redisTemplate.opsForZSet().rangeByScore(QUEUE_KEY, 0, System.currentTimeMillis(), 0, 1);
                        if (CollectionUtils.isEmpty(values)) {
                            Thread.sleep(1000);
                            continue;
                        }
                        String msg = (String)values.iterator().next();
                        //如果remove失败,说明其他线程已经删除了这个key,进行下一次查询,保证了线程安全
                        if (redisTemplate.opsForZSet().remove(QUEUE_KEY, msg) > 0) {
                            this.handle(msg);
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

        private void handle(String msg) {
            LocalDateTime now = LocalDateTime.now();
            System.out.println("consumer: " + dtf.format(now) + " " + msg + " " + Thread.currentThread().getName());
        }
    }
}


说明: 生产者:DelayQueue中add操作将消息加入到延时队列,队列数据结构为Redis zset,score为当前时间+5s,即延迟5s 消费者ConsumerJob:consumerSwitch=true时才进行消费,每隔1s查询zset,查询范围是(0,System.currentTimeMillis()),以此来保证不会提前消费,每次获取一个值并从zset中删除,完成一次消费。

package com.funstar.redisdemo.rest;

import com.funstar.redisdemo.delay.queue.DelayQueue;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author funstar
 * @date 2019/11/24
 */
@RestController
@RequestMapping("/delayQueue")
public class DelayQueueController {

    @Autowired
    private DelayQueue delayQueue;

    @RequestMapping("/add")
    public boolean add(String msg) {
        return delayQueue.add(msg);
    }

    @RequestMapping("/consumerSwitch")
    public boolean consumerSwitch(boolean start) {
        delayQueue.setConsumerSwitch(start);
        return true;
    }

}

/add:生产元素 /consumerSwitch:控制消费者开关

结果

producer: 2019-11-25 21:10:16 1
producer: 2019-11-25 21:10:18 2
consumer: 2019-11-25 21:10:21 1 consumer-pool-0
producer: 2019-11-25 21:10:21 3
consumer: 2019-11-25 21:10:23 2 consumer-pool-1
producer: 2019-11-25 21:10:23 4
producer: 2019-11-25 21:10:26 5
consumer: 2019-11-25 21:10:26 3 consumer-pool-2
consumer: 2019-11-25 21:10:28 4 consumer-pool-0
producer: 2019-11-25 21:10:29 6
consumer: 2019-11-25 21:10:31 5 consumer-pool-0
consumer: 2019-11-25 21:10:34 6 consumer-pool-1

参考资料

juejin.cn/book/684473…