使用redis实现延时任务

        

1.为什么任务需要存储在数据库中?

延迟任务是一个通用的服务,任何需要延迟得任务都可以调用该服务,需要考虑数据持久化的问题,存储数据库中是一种数据安全的考虑。

2.为什么redis中使用两种数据类型,list和zset?

效率问题,算法的时间复杂度

3.在添加zset数据的时候,为什么不需要预加载?

任务模块是一个通用的模块,项目中任何需要延迟队列的地方,都可以调用这个接口,要考虑到数据量的问题,如果数据量特别大,为了防止阻塞,只需要把未来几分钟要执行的数据存入缓存即可。

搭建一个模块,用于管理任何类型的延迟任务

        配置好他的nacos服务

server:
  port: 
spring:
  application:
    name: 
  cloud:
    nacos:
      discovery:
        server-addr: 
      config:
        server-addr: 
        file-extension: 

nacos的yml配置

spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql:
    username: 
    password: 
# 设置Mapper接口所对应的XML文件位置,如果你在Mapper接口中有自定义方法,需要进行该配置
mybatis-plus:
  mapper-locations: classpath*:mapper/*.xml
  # 设置别名包扫描路径,通过该属性可以给包中的类注册别名
  type-aliases-package: 

准备好数据库

里面有两张表:taskinfo任务信息表和taskinfologs任务信息日志表

相对于java也要创建两个实体类

安装redis

安装在docker或者window,然后用笔记本redis的连接客户端连接

接着就要使用idea连接redis了,先导入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- redis依赖commons-pool 这个依赖一定要添加 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

nacos里面的yml文件也要配置redis的地址
spring:
  redis:
    host: 
    password: 
    port: 6379

可以先创建一个测试类,测试一下redis



@SpringBootTest(classes = ScheduleApplication.class)
@RunWith(SpringRunner.class)
public class RedisTest {

    @Autowired
    private CacheService cacheService;

    @Test
    public void testList(){

        //在list的左边添加元素
//        cacheService.lLeftPush("list_001","hello,redis");

        //在list的右边获取元素,并删除
        String list_001 = cacheService.lRightPop("list_001");
        System.out.println(list_001);
    }

    @Test
    public void testZset(){
        //添加数据到zset中  分值
        /*cacheService.zAdd("zset_key_001","hello zset 001",1000);
        cacheService.zAdd("zset_key_001","hello zset 002",8888);
        cacheService.zAdd("zset_key_001","hello zset 003",7777);
        cacheService.zAdd("zset_key_001","hello zset 004",999999);*/

        //按照分值获取数据
        Set<String> zset_key_001 = cacheService.zRangeByScore("zset_key_001", 0, 8888);
        System.out.println(zset_key_001);


    }
}

然后创建一个用于接收的类task类,用来传参

然后开始写代码

先实现任务的持久化,传参是的task要要有任务id,类型,优先级 ,executeTime,parameters

,放在数据库中,刚开始存任务里面,我们只需要放数据库的任务信息表和日志表中,日志表的状态就是初始化。

然后就开始放redis中,判断task:

task.getExecuteTime() <= System.currentTimeMillis()或者是否小于当前时间的五分钟之后

前者放入list中,后者放入zset中。

@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
        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);
            taskinfo.setExecuteTime(new Date(task.getExecuteTime()));
            taskinfoMapper.insert(taskinfo);

            //设置taskID
            task.setTaskId(taskinfo.getTaskId());

            //保存任务日志数据
            TaskinfoLogs taskinfoLogs = new TaskinfoLogs();
            BeanUtils.copyProperties(taskinfo, taskinfoLogs);
            taskinfoLogs.setVersion(1);
            taskinfoLogs.setStatus(ScheduleConstants.SCHEDULED);
            taskinfoLogsMapper.insert(taskinfoLogs);

            flag = true;
        } catch (Exception e) {
            e.printStackTrace();
        }

        return flag;
    }
}

然后数据存储工作准备好了

接着就是消费任务

当消费者过来时,把list任务取出来,

 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 异常");
        }

        return task;
    }

因为list会消费完,使用zset里面的任务时间过期时,我们就可以把他们的任务同步到list中

这里使用spring的

Scheduled,一分钟执行一次,当部署两台服务时,都去执行refresh定时任务方法 这里不需要,只需要一台执行即可,底层就是使用redis的分布式锁,setnx

然后下面的逻辑就是将每一个符合的zset数据传到list中

 @Scheduled(cron = "0 */1 * * * ?")
    public void refresh(){

        String token = cacheService.tryLock("FUTURE_TASK_SYNC", 1000 * 30);

        if (StringUtils.isNotBlank(token)){
            log.info("未来数据定时刷新");

            Set<String> futureKeys = cacheService.scan(ScheduleConstants.FUTURE + "*");
            for (String futureKey : futureKeys) {

                String topicKey =ScheduleConstants.TOPIC+futureKey.split(ScheduleConstants.FUTURE)[1];

                Set<String> tasks = cacheService.zRangeByScore(futureKey, 0, System.currentTimeMillis());
                if (tasks.isEmpty()){
                    cacheService.refreshWithPipeline(futureKey,topicKey,tasks);
                    log.info("未来数据刷新");
                }

            }

        }

trylock方法

redis的管道传输

接着写一个openfeign接口来对应上这个计划模块

在计划模块实现openfeign,来接收openfeign的远程调用

 

然后,我们需要在我们想要实现延时任务的地方,整理好参数,传给openfeign将任务添加到数据库等等,openfeign传入计划模块,实现插入数据库,插入redis中。这样,我们的任务就上传到redis中了,业务就完成了,剩下的任务怎么消费,就交给后台

这里我们用

y@Scheduled(fixedRate = 1000)这个每秒执行一次来执行任务

因为这个模块时负责定时审核文章,查询任务时,只需要按照list的任务名称来查询,查询出来的值里放着参数时文章的id,然后调用审核文章的接口就完成了定时审核

后面还有一个,因为如果执行时间大于五分钟之后,就不放入zset了,防止阻塞。所以我们只需要弄个定时上传即可

先清理redis中的所有数据,简单粗暴,然后在查询任务信息表,插入redis就同步了redis和数据库

@Scheduled(cron = "0 */5 * * * ?")
@PostConstruct//spring启动时就会执行一次这个方法
public void reloadData() {
    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);
        }
    }
}

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

总之,redis延时任务业务流程就是,把你需要延时的任务放入redis中,然后写一个消费方法定时处理redis的list中的方法

我学习的项目是在发布文章的流程里,把审核文章删掉,也就是实现一个定时发布,,接着调用新开的一个方法,用来特意整理数据来调用openfeign,来调用计划模块,将任务(任务里有我们的文章id,当用户点击发布时,我们会将文章保存在自媒体的文章表中,前台时看不到的)放入数据库和redis中,这样只要定时文章的时间小于当前时间,就会立马被处理,因为消费任务的逻辑时间是写了每一秒执行

  • 16
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值