什么是延迟队列?基于Springboot redis实现延时队列

2,395 阅读3分钟

什么是延迟队列?

首先,队列这种数据结构相信大家都不陌生,它是一种先进先出的数据结构。普通队列中的元素是有序的,先进入队列中的元素会被优先取出进行消费;

延时队列相比于普通队列最大的区别就体现在其延时的属性上,普通队列的元素是先进先出,按入队顺序进行处理,而延时队列中的元素在入队时会指定一个延迟时间,表示其希望能够在经过该指定时间后处理。从某种意义上来讲,延迟队列的结构并不像一个队列,而更像是一种以时间为权重的有序堆结构。

应用场景

  • 下单成功后,X分钟内没有支付,自动取消订单
  • 外卖场景,快要超时时给外卖小哥发送提醒通知
  • 预定的会议开始前X分钟提醒
  • 等等

方案

  • JDK中DelayQueue相关API
  • Quartz
  • Redis Zset(本文)
  • MQ
  • 等等

实现

本文使用Redis Zset来实现延迟队列。

zset 是 Redis 提供的最具特色的数据类型之一,首先它是一个 set,这保证了内部 value 值的唯一性,其次它给每个 value 添加了一个 score(分值)属性,通过对分值的排序实现了有序化。比如用 zset 结构来存储学生的成绩,value 值代表学生的 ID,score 则是的考试成绩。我们可以对成绩按分数进行排序从而得到学生的的名次。

\

1、pom.xml

 <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

application.yml

spring:
  redis:
    host: localhost
    port: 6379
    password: xxx
    database: 1

\

2、延迟任务对象定义

public interface RedisDelayTask {
    /**
     *  任务ID
     * @return
     */
    String getId();

    /**
     *  队列中的值
     * @return
     */
    String getValue();

    /**
     *  延迟时间(单位:s)
     * @return
     */
    long getDelayTime();

    /**
     *  任务执行
     */
    void execute();
}
/**
 * 抽象任务
 */
public abstract class AbstractRedisDelayTask implements RedisDelayTask {

    protected String id;
    protected String value;
    private long delayTime;

    public AbstractRedisDelayTask(String id, String value, long delayTime) {
        this.id = id;
        this.value = value;
        this.delayTime = delayTime;
    }

    @Override
    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    @Override
    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }

    @Override
    public long getDelayTime() {
        return delayTime;
    }

    public void setDelayTime(long delayTime) {
        this.delayTime = delayTime;
    }

    @Override
    public String toString() {
        return "RedisDelayTask{" +
                "id='" + id + ''' +
                ", value='" + value + ''' +
                ", delayTime=" + delayTime +
                '}';
    }
}

3、通知类任务定义

public class NoticeTask extends AbstractRedisDelayTask {

    private final static Logger LOGGER = LoggerFactory.getLogger(NoticeTask.class);

    public NoticeTask(String id, String value, long delayTime) {
        super(id, value, delayTime);
    }

    @Override
    public void execute() {
        LOGGER.info("task execute, {}", this);
    }
}

4、任务管理

@Component
public class RedisDelayQueueManager implements InitializingBean {

    @Autowired
    private StringRedisTemplate redisTemplate;
    /**
     *  任务列表
     */
    private Map<String, RedisDelayTask> tasks = new ConcurrentHashMap<>();


    /**
     *  添加延迟任务到队列
     * @param task
     */
    public void addTask(RedisDelayTask task) {
        long delayedTime = System.currentTimeMillis() + TimeUnit.MILLISECONDS.convert(task.getDelayTime(), TimeUnit.SECONDS);
        boolean r = redisTemplate.opsForZSet().add(task.getId(), task.getValue(), delayedTime);
        if (r) {
            tasks.put(task.getId(), task);
        }
    }

    /**
     *  检查并执行任务
     */
    private void checkAndExecuteTask() {
        while (true) {
            Set<String> taskIds = tasks.keySet();
            for (String taskId : taskIds) {
                // score就是任务要执行的时间点,如果<=当前时间,说明任务该执行了
                Set<ZSetOperations.TypedTuple<String>> tuples = redisTemplate.opsForZSet().rangeByScoreWithScores(taskId, 0, System.currentTimeMillis());
                if (!CollectionUtils.isEmpty(tuples)) {
                    for (ZSetOperations.TypedTuple<String> tuple : tuples) {
                        // 移除并执行任务
                        RedisDelayTask task = tasks.remove(taskId);
                        if (task != null) {
                            task.execute();
                            // 从队列中删除
                            redisTemplate.opsForZSet().remove(taskId, tuple.getValue());
                        }
                    }
                }
            }
        }
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        // 新起一个线程执行任务
        new Thread(() -> {
            checkAndExecuteTask();
        }, "redis-delay-task").start();
    }
}

5、测试

@RunWith(SpringRunner.class)
@SpringBootTest(classes = RedisApplication.class)
public class RedisDelayTaskTest {
    @Autowired
    private RedisDelayQueueManager redisDelayQueueManager;

    @Test
    public void addTask() throws IOException {
        NoticeTask task = new NoticeTask("notice-task", "notice-task-value", 5);
        redisDelayQueueManager.addTask(task);
        NoticeTask task2 = new NoticeTask("notice-task2", "notice-task-value2", 10);
        redisDelayQueueManager.addTask(task2);
        System.in.read();
    }
}

执行结果如下:

2022-01-22 17:27:53.428 INFO 86506 --- [ main] io.lettuce.core.KqueueProvider : Starting without optional kqueue library

2022-01-22 17:27:58.140 INFO 86506 --- [edis-delay-task] c.springboot.demo.redis.task.NoticeTask : task execute, RedisDelayTask{id='notice-task', value='notice-task-value', delayTime=5}

2022-01-22 17:28:03.925 INFO 86506 --- [edis-delay-task] c.springboot.demo.redis.task.NoticeTask : task execute, RedisDelayTask{id='notice-task2', value='notice-task-value2', delayTime=10}