redis实现任务队列(三)

这个三主要是对前面的代码进行一些补充说明。

补充

首先补充二里面的一些重要方法的说明,便于理解。

 //未来数据定时刷新
    @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 + "下");
                }
            }

        }
    }

scan方法:

        scan是基于游标搜索的方法, 使用keys的模糊匹配却发现redis的CPU使用率极高,redis是单线程,会被堵塞。scan是一个基于游标的迭代器,命令每次被调用之后, 都会向返回一个新的游标, 在下次迭代时需要使用这个新游标作为scan命令的游标参数, 以此来延续之前的迭代过程。就是累加的。

这个具体的方法在文章redis(1)里有具体的代码实现。

refreshWithPipeline

connection那些方法主要作用是获取一个Redis连接(RedisConnection)对象。

这个方法是同步操作,但是是将一堆命令同时传递过去处理。具体地个人里接在代码注释里,仍然是一个阻塞方法,我也想过采用异步调用listenfuture去实现,不过觉得好像没什么必要。

    //同步操作,允许一次性发送多个命令给Redis服务器,doInRedis方法中执行了一系列Redis命令,包括rPush和zRem,
    // 这些命令会一起被打包发送到Redis服务器,而不是在每个命令之间等待返回。
    //减少网络延迟,但它仍然是同步的
    public List<Object> refreshWithPipeline(String future_key,String topic_key,Collection<String> values){

        List<Object> objects = stringRedisTemplate.executePipelined(new RedisCallback<Object>() {
            @Nullable
            @Override
            public Object doInRedis(RedisConnection redisConnection) throws DataAccessException {
                StringRedisConnection stringRedisConnection = (StringRedisConnection)redisConnection;
                //将任务转成一个字符串数组
                String[] strings = values.toArray(new String[values.size()]);
                //把当前的任务放到connection中
                stringRedisConnection.rPush(topic_key,strings);
                //把当前的任务从zset中删除
                stringRedisConnection.zRem(future_key,strings);
                return null;
            }
        });
        return objects;
    }

采用setnx实现分布式锁

代码在之前防止redis缓存三件套的时候采用过redisson分布式锁方式实现过,这次采用setnx来实现一下。

个人理解,setnx其实也是一种乐观锁的感觉,他会给你生成一个不存在的key,然后看服务实例是否持有该锁进而允不允许他执行,也是比悲观锁效率更高。

 一些具体的注释也在代码里给出了,锁设置成功了就会返回token。

  //基于setnx实现分布式锁
    /**
     * 加锁
     *
     * @param name
     * @param expire
     * @return
     */
    public String tryLock(String name, long expire) {
        //expire过期时间为毫秒值
        name = name + "_lock";
        String token = UUID.randomUUID().toString();
        RedisConnectionFactory factory = stringRedisTemplate.getConnectionFactory();
        RedisConnection conn = factory.getConnection();
        try {

            //参考redis命令:
            //set key value [EX seconds] [PX milliseconds] [NX|XX]
            //NX:表示key不存在才设置成功。 EX:设置过期时间
            Boolean result = conn.set(
                    name.getBytes(),//key的名称
                    token.getBytes(),//值为uuid
                    Expiration.from(expire, TimeUnit.MILLISECONDS),//过期时间
                    RedisStringCommands.SetOption.SET_IF_ABSENT //NX
            );
            if (result != null && result)
                return token;
        } finally {
            RedisConnectionUtils.releaseConnection(conn, factory,false);
        }
        return null;
    }

reloadData

 @PostConstruct//初始化方法,微服务启动了,就会做同步操作

值得说明的是spring注解@Scheduled,存在这一些问题 :

  • 做集群任务的重复执行问题

  • cron表达式定义在代码之中,修改不方便

  • 定时任务失败了,无法重试也没有统计

  • 如果任务量过大,不能有效的分片执行

这里综合实际任务并且任务量,选择这个注解实现。

所有的task在数据库中都能找到,同步数据前记得清理一下缓存数据,防止重复,但要执行的肯定不会丢失。

 //数据库任务定时同步到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");
    }

  • 10
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 延时队列是指队列中的任务需要在一定延时后才能被执行,可以有效地解决某些任务需要延时处理的问题。Java Redis是一个基于Redis的Java客户端,它可以实现Java语言对Redis数据结构的操作,因此可以很方便地实现延时队列的功能。 Java Redis实现延时队列的主要思路是利用Redis的Sorted Set数据结构,将任务按照延时时间作为Score,任务内容作为Value,插入Sorted Set中,并设置过期时间,过期后将任务弹出并执行。具体实现步骤如下: 1. 创建一个Sorted Set,将任务插入其中,Score为任务的延时时间,Value为任务的内容。 2. 使用Redis的zremrangebyscore命令扫描Sorted Set中Score小于等于当前时间的任务并弹出,并将任务内容推送到执行队列中。 3. 设置延时任务的过期时间,过期时间为延时时间加当前时间,可以使用Redis的zadd命令添加任务时同时设置Score和过期时间。 4. 执行队列按顺序执行任务任务执行完成后从执行队列中删除。 通过以上步骤,可以实现一个高效可靠的延时队列,可以优化系统任务调度、异步处理、消息通知等场景下的问题。 ### 回答2: Java Redis延时队列是一种常用的消息队列模式,在很多应用场景中都有应用。Java Redis延时队列通过将消息发送到Redis进行存储,在指定的时间后再将消息取出来进行处理。这个过程中,通过Redis的Sorted Set类型进行排序来保证队列中的消息有序。下面来详细介绍Java Redis延时队列实现方式。 一、Redis数据结构 Java Redis延时队列的关键在于Redis数据结构的设计。在Redis中,Sorted Set就是用来解决排序问题的。所以我们需要借助Sorted Set实现延时队列。具体来说,可以使用Redis中的zadd命令将消息发送到Sorted Set中,并按照时间顺序进行排序。Sorted Set中的元素包含值和权重,我们可以根据权重(即时间戳)来实现有序存储。 二、消息入队 消息的入队过程如下: 1. 获取消息的过期时间TTL。 2. 计算出当前的时间戳now。 3. 将消息写入到Sorted Set中,权重为now+TTL。 ```redis-cli> ZADD delay-queue (now + TTL) message``` 、消息出队 消息出队过程如下: 1. 获取当前时间戳now。 2. 使用zrangebyscore命令从Sorted Set中获取所有权重小于等于now的元素,即过期的元素。 3. 遍历查询结果,对每个元素执行出队操作(移除元素)。 ```redis-cli> ZRANGEBYSCORE delay-queue -inf now``` 四、多线程处理 为避免在出队过程中同时处理多个过期元素时出现问题,可以使用多线程处理消息。Java的并发包中提供了Executor框架,这里可以使用ThreadPoolExecutor线程池。 五、消息重试 有时候由于网络波动等原因,在执行消息处理时可能会失败,所以需要将失败的消息重新入队。此时,可以加入重试机制,重新入队时TTL加上重试时间,即可实现延时次数的控制。 六、总结 Java Redis延时队列利用Redis的Sorted Set实现有序存储,使用多线程和重试机制解决了消息处理时的并发和失败问题,保障了消息的可靠性。在实际应用中,可以根据业务需求进行调优和扩展,如设置合理的时间间隔、增加监控和报警等。 ### 回答3: Java Redis实现延时队列可以分为以下几步: 1.将任务加入到延时队列中:首先需要将任务和对应的过期时间放入Redis的有序集合中,以过期时间为score值,任务为value。这样可以保证按照过期时间顺序进行排序,具有先进先出的特点。代码实现如下: ```java //添加任务到延时队列 public void addToDelayQueue(String taskId, long delayTime) { //计算过期时间 long expireTime = System.currentTimeMillis() + delayTime; //添加到有序集合中,score为过期时间 jedis.zadd(DELAY_QUEUE_KEY, expireTime, taskId); } ``` 2.从延时队列中取出任务:需要循环从有序集合中取出第一个score小于等于当前时间的任务,并将其从有序集合中删除。代码实现如下: ```java //获取延时队列中的任务 public void getFromDelayQueue() { while (true) { //获取第一个score小于等于当前时间的任务 Set<String> set = jedis.zrangeByScore(DELAY_QUEUE_KEY, 0, System.currentTimeMillis(), 0, 1); if (set == null || set.isEmpty()) { try { Thread.sleep(1000);//如果没有取到任务,则等待1秒 } catch (InterruptedException e) { e.printStackTrace(); } continue; } String taskId = set.iterator().next(); //删除任务 jedis.zrem(DELAY_QUEUE_KEY, taskId); //处理任务 handleTask(taskId); } } //处理任务 public void handleTask(String taskId) { //TODO: 根据taskId执行对应的任务 } ``` 3.使用Redis发布订阅机制以及线程池来处理任务:在处理任务时,可以使用Redis发布订阅机制将任务相关的信息发布到指定的频道,由相应的消费者线程池来进行任务处理,可以降低单线程处理任务的压力。代码实现如下: ```java //处理任务 public void handleTask(String taskId) { //通过发布订阅机制将任务信息发布到指定频道 jedis.publish(TASK_CHANNEL, taskId); } //消费者线程池处理任务 public void consumeTask() { JedisPubSub jedisPubSub = new JedisPubSub() { public void onMessage(String channel, String message) { //TODO: 根据message信息执行对应的任务 } }; jedis.subscribe(jedisPubSub, TASK_CHANNEL); executorService.execute(jedisPubSub::quit); } ``` 通过以上步骤,我们就可以实现Java Redis延时队列的功能,实现任务的延迟执行。延时队列实现可以实现具有任务按照时间顺序执行,且任务可取消、可重试等特点,应用场景广泛。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值