redis实现延迟任务(二)

实现思路

我们实现文章地定时发布主要是利用zset地score属性。我们可以在score里存入要发布地时间戳地值,然后在定时刷新任务方法里,通过获取本地时间与score里的时间进行对比,因为本地时间是在不断变大的,如果大于等于的话那么就将他放到立即执行的list列表任务里。

除了定时任务还有立即发布的,立即发布的话就不需要存储score了,我们就存在list属性里面就可以了。

redis里list与set

这里为什么要存储在两个不同的数据类型里面呢,可以分析一下:

list:是有序的,并且可以允许key的重复,插入和删除数据快但是查询一般般,list为双向链表,当需要立即发布的时候,数据量特别大的时候,list的效率要高于zset,普遍情况下也都是立即发布。

set:是无序的,value是不可以重复的,并且查找很快。

数据存储

由于redis最好不要存放大量的数据,因为redis是基于缓存实现的,虽然最后也会有持久化aof等,但是大量的数据还是存储在数据库中比较好,同时为了防止Zset里面的数据量太大了发生阻塞,所以我们只往zset放未来五分钟内要实现的任务。其他的先放在数据库里,当然这就涉及到了数据同步的问题。

数据同步

我们需要将符合时间条件的数据从数据库当中读取到redis里,又需要将符合条件即将执行的数据从redis里的set集合里读取到list集合里面。

下面给出具体的实现代码

代码实现

model

task类:

这里的参数是需要放置将来的任务对象的,序列化成byte就好了。也可以序列化成json,这个是根据数据协议来的。用fastjson序列化也是可以的。

java 复制代码
package com.neu.schedule.model.po;

import lombok.Data;

import java.io.Serializable;

/**
 * @BelongsProject: llyz-neu-project
 * @BelongsPackage: com.neu.schedule.model.po
 * @Author: zzm
 * @CreateTime: 2024-01-04  21:29
 * @Description: TODO
 * @Version: 1.0
 */
@Data
public class Task implements Serializable {

    /**
     * 任务id
     */
    private Long taskId;
    /**
     * 类型
     */
    private Integer taskType;

    /**
     * 优先级
     */
    private Integer priority;

    /**
     * 执行id long类型时间戳
     */
    private long executeTime;

    /**
     * task参数
     */
    private byte[] parameters;

}

taskinfo:

java 复制代码
package com.neu.schedule.model.po;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.io.Serializable;
import java.util.Date;

/**
 * <p>
 *
 * </p>
 *
 * @author zzm
 */
@Data
@TableName("taskinfo")
public class Taskinfo implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 任务id
     */
    @TableId(type = IdType.ID_WORKER)
    private Long taskId;

    /**
     * 执行时间
     */
    @TableField("execute_time")
    private Date executeTime;

    /**
     * 参数
     */
    @TableField("parameters")
    private byte[] parameters;

    /**
     * 优先级
     */
    @TableField("priority")
    private Integer priority;

    /**
     * 任务类型
     */
    @TableField("task_type")
    private Integer taskType;


}

taskInfoLog:用来记录task任务运行结果的

java 复制代码
package com.neu.schedule.model.po;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;

import java.io.Serializable;
import java.util.Date;

/**
 * <p>
 *
 * </p>
 *
 * @author zzm
 */
@Data
@TableName("taskinfo_logs")
public class TaskinfoLogs implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 任务id
     */
    @TableId(type = IdType.ID_WORKER)
    private Long taskId;

    /**
     * 执行时间
     */
    @TableField("execute_time")
    private Date executeTime;

    /**
     * 参数
     */
    @TableField("parameters")
    private byte[] parameters;

    /**
     * 优先级
     */
    @TableField("priority")
    private Integer priority;

    /**
     * 任务类型
     */
    @TableField("task_type")
    private Integer taskType;

    /**
     * 版本号,用乐观锁
     */
    @Version//乐观锁,mybatis支持的
    private Integer version;

    /**
     * 状态 0=int 1=EXECUTED 2=CANCELLED
     */
    @TableField("status")
    private Integer status;


}

乐观锁支持:

java 复制代码
/**
     * mybatis-plus乐观锁支持
     * @return
     */
@Bean
public MybatisPlusInterceptor optimisticLockerInterceptor(){
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
    return interceptor;
}

这个肯定不能任务重复执行,其实乐观锁的思想多采用cas,当有多个实例对同一个任务执行的时候,看谁先将version字段设置成功了,谁就可以执行该任务,它比悲观锁的效率高。

service

这里就不一一解释每个方法的内容了,只说点重要代码和大概思路。

任务队列,添加任务,删除任务和实现任务。

添加任务:首先将task添加到数据库,然后看看任务的执行时间,如果超过五分钟,就不放入redis里,等着后期同步,如果是小于五分钟大于当前时间就放到set里,如果小于等于当前时间就放到redis的list里。

删除任务:从数据库里面删除,再去redis里删除,有就删除。

拉取任务:poll。由于最后都是从list结合里执行数据,所以只从list里面弹就可以了。

执行任务:这个方法是需要定时刷新的,用的是schedule注解,这个需要在启动类上加注解:

复制代码
@EnableScheduling//开启定时任务

所有的任务执行都需要在taskinfolog里面记录status的执行结果,结果的状态大家可以自行定义。

java 复制代码
package com.neu.schedule.service.impl;

import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.neu.base.constants.ScheduleConstants;
import com.neu.base.redis.CacheService;
import com.neu.schedule.mapper.TaskinfoLogsMapper;
import com.neu.schedule.mapper.TaskinfoMapper;
import com.neu.schedule.model.po.Task;
import com.neu.schedule.model.po.Taskinfo;
import com.neu.schedule.model.po.TaskinfoLogs;
import com.neu.schedule.service.TaskService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.PostConstruct;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Set;

/**
 * @BelongsProject: llyz-neu-project
 * @BelongsPackage: com.neu.schedule.service.impl
 * @Author: zzm
 * @CreateTime: 2024-01-04  21:30
 * @Description: TODO
 * @Version: 1.0
 */
@Service
@Transactional
@Slf4j
public class TaskServiceImpl implements TaskService {

    /**
     * 添加延迟任务
     *
     * @param task
     * @return
     */
    @Override
    public long addTask(Task task) {
        //1.添加任务到数据库中

        boolean success = addTaskToDb(task);

        if (success) {
            //2.添加任务到redis
            addTaskToCache(task);
        }


        return task.getTaskId();
    }
    @Autowired
    private CacheService cacheService;

    /**
     * 把任务添加到redis中
     *
     * @param task
     */
    private void addTaskToCache(Task task) {
        String key = task.getTaskType() + "_" + task.getPriority();

        //获取5分钟之后的时间  毫秒值
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.MINUTE, 5);
        long nextScheduleTime = calendar.getTimeInMillis();

        //2.1 如果任务的执行时间小于等于当前时间,存入list中,list的key是topic+type+priority,可以重复
        if (task.getExecuteTime() <= System.currentTimeMillis()) {
            cacheService.lLeftPush(ScheduleConstants.TOPIC + key, JSON.toJSONString(task));
        } else if (task.getExecuteTime() <= nextScheduleTime) {
            //2.2 如果任务的执行时间大于当前时间 && 小于等于预设时间(未来5分钟) 存入zset中
            cacheService.zAdd(ScheduleConstants.FUTURE + key, JSON.toJSONString(task), task.getExecuteTime());
        }

    }

    @Autowired
    private TaskinfoMapper taskinfoMapper;

    @Autowired
    private TaskinfoLogsMapper taskinfoLogsMapper;

    /**
     * 添加任务到数据库中
     *
     * @param task
     * @return
     */
    private boolean addTaskToDb(Task task) {

        boolean flag = false;

        try {
            //保存任务表
            Taskinfo taskinfo = new Taskinfo();
            BeanUtils.copyProperties(task, taskinfo);
            //task里的执行时间是不一样的,是毫秒值,需要转换成Date类型
            taskinfo.setExecuteTime(new Date(task.getExecuteTime()));
            System.out.println(taskinfo.getTaskId());
            taskinfoMapper.insert(taskinfo);
            //在插入数据库之后,用的是数据库自动生成的雪花id
            System.out.println(taskinfo.getTaskId());
            //设置taskID
            task.setTaskId(taskinfo.getTaskId());
            //保存任务日志数据
            TaskinfoLogs taskinfoLogs = new TaskinfoLogs();
            BeanUtils.copyProperties(taskinfo, taskinfoLogs);
            //乐观锁初始版本号,默认为1
            taskinfoLogs.setVersion(1);
            taskinfoLogs.setStatus(ScheduleConstants.SCHEDULED);
            taskinfoLogsMapper.insert(taskinfoLogs);
            flag = true;
        } catch (Exception e) {
            e.printStackTrace();
        }

        return flag;
    }


    /**
     * 取消任务
     * @param taskId
     * @return
     */
    @Override
    public boolean cancelTask(long taskId) {

        boolean flag = false;

        //删除任务,更新日志
        Task task = updateDb(taskId,ScheduleConstants.CANCELLED);

        //删除redis的数据
        if(task != null){
            removeTaskFromCache(task);
            flag = true;
        }



        return false;
    }

    /**
     * 删除redis中的任务数据
     * @param task
     */
    private void removeTaskFromCache(Task task) {

        String key = task.getTaskType()+"_"+task.getPriority();

        if(task.getExecuteTime()<=System.currentTimeMillis()){
            cacheService.lRemove(ScheduleConstants.TOPIC+key,0,JSON.toJSONString(task));
        }else {
            cacheService.zRemove(ScheduleConstants.FUTURE+key, JSON.toJSONString(task));
        }
    }

    /**
     * 删除任务,更新任务日志状态
     * @param taskId
     * @param status
     * @return
     */
    private Task updateDb(long taskId, int status) {
        Task task = null;
        try {
            //删除任务
            taskinfoMapper.deleteById(taskId);
            TaskinfoLogs taskinfoLogs = taskinfoLogsMapper.selectById(taskId);
            taskinfoLogs.setStatus(status);
            taskinfoLogsMapper.updateById(taskinfoLogs);

            task = new Task();
            BeanUtils.copyProperties(taskinfoLogs,task);
            task.setExecuteTime(taskinfoLogs.getExecuteTime().getTime());
        }catch (Exception e){
            log.error("task cancel exception taskid={}",taskId);
        }

        return task;

    }


    /**
     * 按照类型和优先级拉取任务
     * @return
     */
    @Override
    public Task poll(int type,int priority) {
        Task task = null;
        try {
            String key = type+"_"+priority;
            String task_json = cacheService.lRightPop(ScheduleConstants.TOPIC + key);
            if(StringUtils.isNotBlank(task_json)){
                task = JSON.parseObject(task_json, Task.class);
                //更新数据库信息
                updateDb(task.getTaskId(),ScheduleConstants.EXECUTED);
            }
        }catch (Exception e){
            e.printStackTrace();
            log.error("poll task exception");
        }

        return task;
    }

    //未来数据定时刷新
    @Scheduled(cron = "0 */1 * * * ?")//一分钟调用一次
    public void refresh() {
        //setnx实现分布式锁
        String token = cacheService.tryLock("FUTURE_TASK_SYNC", 1000 * 30);
        if(StringUtils.isNotBlank(token)){
            log.info("未来数据定时刷新");
            System.out.println(System.currentTimeMillis() / 1000 + "执行了定时任务");

            // 获取所有未来数据集合的key值
            Set<String> futureKeys = cacheService.scan(ScheduleConstants.FUTURE + "*");// future_*
            for (String futureKey : futureKeys) { // future_250_250

                String topicKey = ScheduleConstants.TOPIC + futureKey.split(ScheduleConstants.FUTURE)[1];
                //获取该组key下当前需要消费的任务数据
                //参数:0:为从0开始查 0~当前时间的毫秒值
                Set<String> tasks = cacheService.zRangeByScore(futureKey, 0, System.currentTimeMillis());
                if (!tasks.isEmpty()) {
                    //将这些任务数据添加到消费者队列中
                    cacheService.refreshWithPipeline(futureKey, topicKey, tasks);
                    System.out.println("成功的将" + futureKey + "下的当前需要执行的任务数据刷新到" + topicKey + "下");
                    log.info("成功的将" + futureKey + "下的当前需要执行的任务数据刷新到" + topicKey + "下");
                }
            }

        }
    }

    //数据库任务定时同步到redis中,每五分钟执行一次
    @Scheduled(cron = "0 */5 * * * ?")
    @PostConstruct//初始化方法,微服务启动了,就会做同步操作
    public void reloadData() {
        //清理缓存中的数据 list,zset
        clearCache();
        log.info("数据库数据同步到缓存");
        //获取五分钟之后的时间,毫秒值
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.MINUTE, 5);

        //查看小于未来5分钟的所有任务
        List<Taskinfo> allTasks = taskinfoMapper.selectList(Wrappers.<Taskinfo>lambdaQuery().lt(Taskinfo::getExecuteTime,calendar.getTime()));
        if(allTasks != null && allTasks.size() > 0){
            for (Taskinfo taskinfo : allTasks) {
                Task task = new Task();
                BeanUtils.copyProperties(taskinfo,task);
                task.setExecuteTime(taskinfo.getExecuteTime().getTime());
                addTaskToCache(task);
            }
        }
        log.info("数据库任务同步到redis");
    }

    private void clearCache(){
        // 删除缓存中未来数据集合和当前消费者队列的所有key
        Set<String> futurekeys = cacheService.scan(ScheduleConstants.FUTURE + "*");// future_
        Set<String> topickeys = cacheService.scan(ScheduleConstants.TOPIC + "*");// topic_
        cacheService.delete(futurekeys);
        cacheService.delete(topickeys);
    }


}
相关推荐
段一凡-华北理工大学8 分钟前
向量数据库实战:选型、调优与落地~系列文章12:文本分块策略实战:chunk_size 怎么选?重叠多少?
开发语言·数据库·后端·oracle·rust·工业智能体·高炉智能化
二宝哥1 小时前
CentOS 7.9 系统下 Redis 5.0.4 单机安装与源码编译安装指南
redis·centos
oradh2 小时前
Oracle XTTS实现跨版本迁移和升级(Oracle 11g单库升级至19C RAC集群)
数据库·oracle·11g升级19c·xtts跨版本迁移和升级
z123456789863 小时前
2026最新两款AI编程工具深度对比实测
java·数据库·ai编程
程序猿DD3 小时前
一个 API Key,统一调用大模型、生图和联网搜索
数据库·网关
TlSfoward4 小时前
抓包代理链路下的 TLS 指纹变化分析 TLSFOWARD抓包工具
数据库·爬虫·网络协议·搜索引擎·php
数智化管理手记4 小时前
账龄分析手工统计易遗漏?自动账龄分析工具怎么搭建
大数据·网络·数据库·人工智能·数据挖掘
LabVIEW开发5 小时前
NI Package Manager安装LabVIEW时InternalServerError错误
网络·数据库·labview·labview知识·labview功能·labview程序
SelectDB5 小时前
PostgreSQL 实时同步到 Apache Doris:一站式 CDC 解决方案
数据库
欲速不达88205 小时前
Mysql并发DML锁等待的优化
数据库·mysql