redis实现任务队列(三)

本文介绍了如何在Java应用中使用Redis进行未来数据定时刷新,包括使用`@Scheduled`注解实现定时任务调度,利用`setnx`实现分布式锁,以及`scan`和`refreshWithPipeline`方法在缓存管理中的应用。还讨论了Spring的`@Scheduled`问题及其局限性。

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

补充

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

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

### 使用 Redis 实现优先级任务队列 #### 数据结构设计 为了实现优先级任务队列,可以选择 Redis 的 **有序集合(Sorted Set)** 数据结构。这种数据结构允许为每个任务分配一个分数(score),该分数可以直接表示任务的优先级。较高的分数代表更高的优先级[^3]。 具体来说,可以将任务 ID 或描述存入有序集合中,而其对应的优先级作为 score 值。通过这种方式,Redis 能够自动按照优先级对任务进行排序,方便后续处理。 --- #### 示例代码 以下是一个基于 Python 和 `redis-py` 库的简单实现: ```python import redis from time import sleep # 初始化 Redis 客户端 r = redis.StrictRedis(host='localhost', port=6379, db=0) def add_task(task_id, priority): """向队列中添加任务""" r.zadd('priority_queue', {task_id: priority}) # 添加任务到有序集合 def get_next_task(): """获取最高优先级的任务""" task = r.zrange('priority_queue', 0, 0, withscores=True) # 获取第一个任务 if task: task_id = task[0][0].decode() # 解码任务 ID r.zrem('priority_queue', task_id) # 移除已完成的任务 return task_id return None def worker(): """模拟工作线程不断拉取高优先级任务""" while True: task = get_next_task() if task: print(f"Processing task: {task}") else: print("No tasks available. Waiting...") sleep(2) ``` 上述代码展示了如何利用 Redis 的有序集合创建一个优先级队列: - `zadd` 方法用于向队列中插入新任务,并指定其优先级。 - `zrange` 方法按顺序返回任务列表,其中索引范围 `(0, 0)` 表示只取出当前最高优先级的一个任务。 - `zrem` 方法移除已经处理完毕的任务。 --- #### 扩展方案:多优先级队列 如果不想使用有序集合,也可以采用多个独立的列表来区分不同优先级的任务。例如,定义个列表分别存储高、中、低优先级任务 (`queue_h`, `queue_m`, `queue_l`)。每次从高优先级队列依次尝试提取任务[^5]。 以下是扩展后的伪代码逻辑: ```python queues = ['queue_h', 'queue_m', 'queue_l'] def get_next_task_from_queues(): for queue_name in queues: task = r.lpop(queue_name) # 尝试从当前队列弹出任务 if task: return task.decode() return None ``` 此方法的优点在于更直观易懂;缺点则是无法动态调整优先级权重。 --- #### 性能优化建议 1. 如果需要频繁更新任务状态或者重新计算优先级,则应考虑引入额外字段记录元信息。 2. 对于大规模并发环境下的竞争条件问题,可借助 Lua 脚本来原子化执行复杂操作[^4]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值