使用redis实现延时队列的一个简单方案(延时队列的设计方案,源码分享)

一、背景,开发场景


        之前负责开发过一个会议室系统,这个系统整体不难,唯一有一个技术难点,就是需要延时任务,会议的开始、结束需要系统自动更新状态(进行中、已结束)以及开始前多分钟、快结束后多少分钟需要发送邮件提醒、短信提醒,以及超时待审批的会议室申请自动审核不通过。这些操作对我们系统来说,不是固定时刻的定时任务轮询就可以的了,因为每一时刻(粒度可能会小到秒)都有可能有执行的。这种开发场景,就需要使用到延时任务了。

        不使用延时任务不可以吗?使用定时任务每隔一段很短的时间轮询去数据库查询数据?当然可以,但是如果数据很大,数据库的处理能力却十分有限,导致系统有性能问题。
        使用延时队列,可以事先每个一段时间,把接下来一段时间内要执行的任务查询出来,放到延时队列。然后每次只需查询延时队列即可,不需要再查询数据库,可以减缓数据库的压力

二、分析方案

1、使用redis的zset数据结构开发一个延时队列

        ZSet数据结构类似于Set结构,只是ZSet结构中,每个元素都会有一个分值,然后所有元素按照分值的大小进行排列,相当于是一个进行了排序的链表。Redis中的ZSet是一个有序的Set,内部使用HashMap和跳表(SkipList)来保证数据的存储和有序,HashMap里放的是成员到score的映射,而跳跃表里存放的是所有的成员,排序依据是HashMap里存的score,使用跳跃表的结构可以获得比较高的查找效率,并且在实现上比较简单。

2、JDK ScheduledExecutorService

3、时间轮

4、redis的key过期回调

        由上分析,使用ScheduledExecutorService虽然简单,但是不支持分布式的部署,没有高可用,最终采取了 使用redis的zset数据结构开发一个延时队列,开发一个支持分布式、轻量简单、低延时、消息可靠的延时队列

三、 代码解析


这个使用redis的延时队列的设计方案分为四部分

  • 延时队列(使用redis的zset结构开发延时队列)
  • 延时任务的具体策略类(使用策略模式,不同延时任务有不同的类获取和执行延时任务)
  • 消费延时任务的监听器,消费到期的消息任务(也是定时器)
  • 增加延时任务的定时器,每隔一段时间执行,加入接下来一段时间内要执行的定时任务

在这里插入图片描述

延时队列

封装延时任务的bean

/**
 * @author zhangxinlin
 * @Description: 消息 封装
 * @date 2020/4/13 23:37
 */
@Data
public class DelayMessage {
    /**
     * 申请单id
     */
    private Integer applyId;
    /**
     * 执行的时间
     */
    private Date executeTime;

    /**
     * 任务类型
     */
    private String jobType;

    /**
     * 等级,默认0
     * 当超时没有执行成功后重新加入执行的队列时会+1,直到超出最大重试次数则丢弃这个消息
     */
    private int level;

    public DelayMessage(){

    }
    public DelayMessage(Integer applyId, Date executeTime, String jobType) {
        this.applyId = applyId;
        this.executeTime = executeTime;
        this.jobType = StringUtils.uncapitalize(jobType);
        this.level = 0;
    }
}

 根据redis的zset结构封装一个延时队列的类
(这个延时队列主要由增加任务到延时队列,从延时队列删除任务)
核心方法是take():从延时队列获取到期需要执行的消息

/**
 * @author zhangxinlin
 * @Description: redis延时队列
 * @date 2020/4/13 23:48
 */
@Component
@Slf4j
public class DelayQueue {

    /**
     * 延时队列的key
     */
    @Value("${queue.key}")
    private String MEETING_QUEUE_KEY;

    /**
     * 延时队列任务丢失后的最大重试次数,-1表示无限次
     */
    @Value("${queue.job.max.retires}")
    private int jobMaxRetires;

    /**
     * 延时队列任务最大的消费时间,超过时间未完成任务即为丢失,单位:秒
     */
    @Value("${queue.job.max.ms}")
    private int jobMaxMs;

    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private RedisLock redisLock;

    /**
     * 增加任务到延时队列
     * @param message
     */
    public void add(DelayMessage message){
        //序列化
        String value =JSONObject.toJSONString(message);
        //加入redis
        redisTemplate.opsForZSet().add(MEETING_QUEUE_KEY,value,message.getExecuteTime().getTime());
    }

    /**
     * 增加任务到延时队列
     * @param applyId
     * @param executeTime
     * @param jobType
     */
    public void add(Integer applyId, Date executeTime,String jobType){
        DelayMessage message = new DelayMessage(applyId,executeTime,jobType);
        this.add(message);
    }

    public void addAll(List<DelayMessage> messages){
        if(!CollectionUtils.isEmpty(messages)){
            messages.stream().forEach(message->{
                this.add(message);
            });
        }
    }

    /**
     * 从延时队列中移除任务
     * @param message
     */
    public void remove(DelayMessage message){
        //序列化
        String value =JSONObject.toJSONString(message);
        redisTemplate.opsForZSet().remove(MEETING_QUEUE_KEY,value);
    }

    public void remove(Integer applyId,Date executeTime,String jobType){
        DelayMessage message = new DelayMessage(applyId,executeTime,jobType);
        this.remove(message);
    }

//    public void removeRange(List<String> delValues){
//        if(!CollectionUtils.isEmpty(delValues)){
//            redisTemplate.opsForZSet().remove(MEETING_QUEUE_KEY,delValues.toArray());
//        }
//    }

    /**
     * 删除处理中队列的消息
     * @param message
     */
    public void removeHandingMessage(DelayMessage message){
        //序列化
        String value =JSONObject.toJSONString(message);
        redisTemplate.opsForZSet().remove(MEETING_QUEUE_KEY+"_HANDING",value);
    }

    /**
     * 从队列获取到期需要执行的消息
     * @return
     */
    public List<DelayMessage> take(){

        List<DelayMessage> delayMessageList = Collections.emptyList();
        try{
            //加锁,一次只能由一个线程读消息队列,避免同一个消息被重复读
            boolean success = this.redisLock.lock(MEETING_QUEUE_KEY);
            if (!success) {
                return delayMessageList;
            }
            //获取待处理的任务消息
            List<String> toDoMessageList = this.getMessageByQueue(MEETING_QUEUE_KEY);
            //获取处理中超时的任务消息
            List<String> overTimeMessageList = this.getMessageByQueue(MEETING_QUEUE_KEY+"_HANDING");

            //待处理的任务处理,更新待处理队列和进行中队列
            this.handToDoMessage(toDoMessageList);
            //超时的任务处理,更新待处理队列和进行中队列
            this.handOvertimeMessage(overTimeMessageList);

            if(!CollectionUtils.isEmpty(toDoMessageList)){
                delayMessageList = toDoMessageList.stream().map(value->{
                    DelayMessage message = JSONObject.parseObject(value,DelayMessage.class);
                    return message;
                }).collect(Collectors.toList());
            }

        }finally {
            this.redisLock.delete(MEETING_QUEUE_KEY);
        }

        return delayMessageList;
    }

    /**
     * 根据key获取需要执行的任务消息
     * @param queueKey
     * @return
     */
    private List<String> getMessageByQueue(String queueKey){
        Set<ZSetOperations.TypedTuple<Object>> tuples = redisTemplate.opsForZSet().rangeWithScores(queueKey,0,-1);
        if(queueKey.equals(MEETING_QUEUE_KEY)){
            log.info("当前延时队列 待处理任务数量:{}",tuples.size());
        }
        Iterator<ZSetOperations.TypedTuple<Object>> iterator = tuples.iterator();
        List<String> messageList = null;
        while (iterator.hasNext())
        {

            ZSetOperations.TypedTuple<Object> typedTuple = iterator.next();
            long nowScore = System.currentTimeMillis();
            double score = typedTuple.getScore();
            if(nowScore > score){
                if(messageList == null){
                    messageList = new ArrayList<>();
                }
                String value = (String)typedTuple.getValue();
                messageList.add(value);
            }else{
                break;
            }

        }
        return messageList;
    }

    /**
     * 待处理的任务处理,更新待处理队列和进行中队列
     * @param toDoMessageList
     */
    private void handToDoMessage(List<String> toDoMessageList){
        if(!CollectionUtils.isEmpty(toDoMessageList)){

            List<String> delValues = toDoMessageList;
            //redis事物
            redisTemplate.execute(new SessionCallback<Object>(){
                @Override
                public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
                    //开启事务
                    operations.multi();
                    //从待处理的延迟队列中移除
                    redisTemplate.opsForZSet().remove(MEETING_QUEUE_KEY,delValues.toArray());
                    //加入进行中的延时队列
                    for(String value:delValues){
                        long score = DateUtils.addSecond(new Date(),jobMaxMs).getTime();
                        redisTemplate.opsForZSet().add(MEETING_QUEUE_KEY+"_HANDING",value,score);
                    }
                    //执行事务
                    operations.exec();
                    return null;
                }
            });
        }
    }

    /**
     * 超时的任务处理,更新待处理队列和进行中队列
     * @param overTimeMessageList
     */
    private void handOvertimeMessage(List<String> overTimeMessageList){
        //判断进行中的队列中是否有超时的,如果有则重新加入待处理队列中等待重新消费
        if(!CollectionUtils.isEmpty(overTimeMessageList)){
            List<String> delValues = overTimeMessageList;
            //redis事物
            redisTemplate.execute(new SessionCallback<Object>(){
                @Override
                public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
                    //开启事务
                    operations.multi();
                    //从处理中的队列移除
                    redisTemplate.opsForZSet().remove(MEETING_QUEUE_KEY+"_HANDING",delValues.toArray());
                    //加入待处理的延时队列
                    for(String value:delValues){
                        DelayMessage message = JSONObject.parseObject(value,DelayMessage.class);
                        //小于最大重试次数加入待处理队列,否则丢掉这个任务
                        if(jobMaxRetires == -1 || message.getLevel() < jobMaxRetires){
                            message.setLevel(message.getLevel()+1);
                            String nowValue = JSONObject.toJSONString(message);
                            redisTemplate.opsForZSet().add(MEETING_QUEUE_KEY,nowValue,message.getExecuteTime().getTime());
                        }
                    }
                    //执行事务
                    operations.exec();
                    return null;
                }
            });
        }
    }
}

延时任务
上面有了延时队列了,但是延时任务要怎么加入延时队列呢?以及延时任务要如何获取?
不同延时任务有不同的延时任务的获取和执行任务的具体实现是不一样,例如修改会议为进行中、已结束和发送邮件提醒等任务,这些任务获取逻辑是不一样的,我们使用策略模式,把不同的延时任务封装为不同的策略,一个策略类对应一个延时任务的获取和执行。

延时任务的策略类的接口
 

public interface DelayJob {

    /**
     * 获取任务名称
     * @return
     */
    String getJobName();

    /**
     * 获取当前时间的一定范围时间内需要执行的延时消息
     * @param curDate 当前时间(可指定,不指定时默认当前系统时间)
     * @return
     */
    List<DelayMessage> getJob(Date curDate);

    /**
     * 延时任务到期后的执行任务
     * @param message
     * @throws Exception
     */
    void executeJob(DelayMessage message) throws Exception;
}

  • 4
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Redis可以通过两种方式实现延时队列: 1. 使用ZSET实现延时队列 在ZSET中,每一个元素都有一个score值,代表了元素的权重。我们可以把元素的score值设置成到期时间,然后使用Redis的ZREVRANGEBYSCORE命令获取到期时间在当前时间之前的元素,这些元素就是需要被执行的任务。 具体实现流程如下: 1. 将任务添加到延时队列中,设置到期时间为任务的执行时间,score为到期时间的时间戳。 2. 定期轮询延时队列,获取到期时间在当前时间之前的任务,将这些任务从延时队列中移除,并执行相应的操作。 代码示例: ``` from redis import StrictRedis import time redis = StrictRedis(host='localhost', port=6379, db=0) def add_task(task_id, execute_time): redis.zadd('delay_queue', {task_id: execute_time}) def handle_task(): while True: # 获取当前时间戳 current_time = time.time() # 获取到期时间在当前时间之前的任务 tasks = redis.zrangebyscore('delay_queue', 0, current_time) if not tasks: time.sleep(1) continue # 处理任务 for task_id in tasks: # 执行相应的操作 print('Handle task:', task_id) # 从延时队列中移除任务 redis.zrem('delay_queue', task_id) if __name__ == '__main__': # 添加任务 add_task('task1', time.time() + 10) # 处理任务 handle_task() ``` 2. 使用LIST和BLPOP实现延时队列 在LIST中,每一个元素都代表了一个任务。我们可以使用Redis的LPUSH命令将任务添加到LIST中,然后使用Redis的BLPOP命令阻塞获取LIST的最后一个元素,当获取到的元素的score值小于当前时间时,执行相应的操作。 具体实现流程如下: 1. 将任务添加到延时队列中,设置到期时间为任务的执行时间,score为到期时间的时间戳。 2. 定期轮询延时队列使用BLPOP命令获取LIST的最后一个元素,并判断是否需要执行相应的操作。 代码示例: ``` from redis import StrictRedis import time import threading redis = StrictRedis(host='localhost', port=6379, db=0) def add_task(task_id, execute_time): # 将任务添加到延时队列中,score为到期时间的时间戳 redis.zadd('delay_queue', {task_id: execute_time}) def handle_task(): while True: # 获取当前时间戳 current_time = time.time() # 获取到期时间在当前时间之前的任务 tasks = redis.zrangebyscore('delay_queue', 0, current_time) if not tasks: time.sleep(1) continue # 处理任务 for task_id in tasks: # 执行相应的操作 print('Handle task:', task_id) # 从延时队列中移除任务 redis.zrem('delay_queue', task_id) def push_task(): # 添加任务 add_task('task1', time.time() + 10) add_task('task2', time.time() + 20) add_task('task3', time.time() + 30) # 使用BLPOP命令获取LIST的最后一个元素 while True: value = redis.blpop('task_list', timeout=1) if not value: continue # 判断是否需要执行相应的操作 task_id, score = value if float(score) <= time.time(): print('Handle task:', task_id) if __name__ == '__main__': # 启动处理任务的线程 handle_thread = threading.Thread(target=handle_task) handle_thread.start() # 启动添加任务的线程 push_thread = threading.Thread(target=push_task) push_thread.start() ``` 以上两种方式均可以实现延时队列,具体选择哪种方式取决于实际需求和场景。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值