IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 大数据 -> 使用redis实现延时队列的一个简单方案(延时队列的设计方案,源码分享)(草稿) -> 正文阅读

[大数据]使用redis实现延时队列的一个简单方案(延时队列的设计方案,源码分享)(草稿)

一、背景,开发场景

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

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

二、分析方案

  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;
}

zrangebyscore myzset 0 100 withscores

  大数据 最新文章
实现Kafka至少消费一次
亚马逊云科技:还在苦于ETL?Zero ETL的时代
初探MapReduce
【SpringBoot框架篇】32.基于注解+redis实现
Elasticsearch:如何减少 Elasticsearch 集
Go redis操作
Redis面试题
专题五 Redis高并发场景
基于GBase8s和Calcite的多数据源查询
Redis——底层数据结构原理
上一篇文章      下一篇文章      查看所有文章
加:2022-01-01 13:58:47  更:2022-01-01 14:01:31 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 13:45:29-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码