延时任务定时发布,基于 Redis 与 DB 实现

news2024/9/24 17:17:07

目录

1、什么是延时任务,分别可以使用哪些技术实现?

1.2 使用  Redis 和 DB 相结合的思路图以及分析

2、实现添加任务、删除任务、拉取任务

3、实现未来数据的定时更新

4、将数据库中的任务数据,同步到 Redis 中


1、什么是延时任务,分别可以使用哪些技术实现?

延时任务:有固定周期的,有明确的触发时间

延迟队列:没有固定的开始时间,它常常是由一个事件触发的,而在这个事件触发之后的一段时间内触发另一个事件,任务可以立即执行,也可以延迟

使用场景:

场景一:订单下单之后30分钟后,如果用户没有付钱,则系统自动取消订单;如果期间下单成功,则任务取消

场景二:接口对接出现网络问题,1分钟后重试,如果失败,2分钟重试,直到出现阈值终止

常用的技术方案:

DelayQueue(JDK自带):是一个支持延时获取元素的阻塞队列, 内部采用优先队列 PriorityQueue 存储元素,同时元素必须实现 Delayed 接口;在创建元素时可以指定多久才可以从队列中获取当前元素,只有在延迟期满时才能从队列中提取元素

弊端:使用线程池或者原生 DelayQueue 程序挂掉之后,任务都是放在内存,需要考虑未处理消息的丢失带来的影响,如何保证数据不丢失,需要持久化(磁盘)

RabbitMQ(消息中间件):允许不同应用之间通过消息传递进行通信,提供了可靠的消息传递机制(将消息保存在磁盘中),支持多种消息模式,包括点对点和发布/订阅。RabbitMQ基于AMQP(高级消息队列协议)设计,具有高度的可扩展性和灵活性

使用 Redis 结合 DB 实现:能够充分利用Redis的高性能特性和灵活的数据结构,同时结合数据库的持久化和数据管理能力(存在磁盘,不易丢失),为系统提供高效、实时、可靠的延时任务处理机制

这里我们选用的是 Redis 结合DB进行实现 

【问题】

为什么选用 Redis + DB ,而不选用 RabbitMQ ?

1、Redis 相对于 RabbitMQ 更加轻量级,对于简单的延时任务队列,可能更倾向于使用轻量级的Redis而不是引入RabbitMQ等消息中间件的复杂性

2、Redis通常更容易集成和维护,因为它是一个简单的键值存储系统,而RabbitMQ是一个完整的消息中间件系统。对于一些小型项目或者对于消息中间件功能的需求不是很大的情况下,选择Redis可能更为经济实惠

1.2 使用  Redis 和 DB 相结合的思路图以及分析

【整体流程图】

【分析问题】

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

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

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

结合场景,考虑效率问题以及算法的时间复杂度

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

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


2、实现添加任务、删除任务、拉取任务

【数据库表结构信息】

Taskinfo

TaskinfoLog

【添加任务】

将任务添加到数据库中

这里 TaskinfoLog 内置了 version 版本号,即乐观锁,保证同一时刻只有一个线程执行成功;其中,Task 是 DTO 数据,Taskinfo(任务) 与 TaskinfoLog(任务日志)是DB数据

private boolean addTackToDB(Task task) {

        boolean loop = false;

        try {
            //1.保存任务表
            Taskinfo taskinfo = new Taskinfo();
            BeanUtils.copyProperties(task, taskinfo);
            taskinfo.setExecuteTime(new Date(task.getExecuteTime()));
            taskinfoMapper.insert(taskinfo);

            task.setTaskId(taskinfo.getTaskId()); //将 任务ID 传给前端

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

            loop = true;
        }catch (Exception exception){
            exception.printStackTrace();
        }
        return loop;
    }

将任务添加到 Redis 中

这里调用 Calender.getInstance() 获得任务预设时间(这里是当前时间5min后);将小于等于 LocalTime 的任务放入 List 中,否则,则将预设任务放入 Zset 进行暂存

private void addTaskToRedis(Task task) {

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

        //1.获取未来 5 分钟之后的预设时间
        Calendar calendar = Calendar.getInstance();  //获取当前日期和时间的日历实例
        calendar.add(Calendar.MINUTE,5);
        long calendarTimeInMillis = calendar.getTimeInMillis(); //获取其毫秒值

        //2.1 若任务执行的时间小于当前时间,则直接放入 list 数据结构中
        if(task.getExecuteTime() <= System.currentTimeMillis()){
            cacheService.lLeftPush(ScheduleConstants.TOPIC+key, JSON.toJSONString(task));
        }else if(task.getExecuteTime() <= calendarTimeInMillis){
            //2.2 若任务执行的时间大于当前时间 并且 小于等于预设时间(未来5分钟),则直接放入 zset 中按照分值排序进行存储
            cacheService.zAdd(ScheduleConstants.FUTURE+ key,JSON.toJSONString(task),task.getExecuteTime());
        }
    }

 调用以上方法

public long addTask(Task task) {

        //1.添加任务到 DB 中,保证任务的持久化
        boolean res = addTackToDB(task);

        if(res) {
            //2.将任务添加到 redis 中
            addTaskToRedis(task);
        }
        return task.getTaskId();
    }

【删除任务】

删除数据库中的任务,并更新对应任务的任务日志

private Task deleteTask_UpdateTaskLog(long taskId, int status) {

        Task task =null;

        try {
            //1.删除任务
            taskinfoMapper.deleteById(taskId);

            //2.更新任务日志
            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("任务处理失败,异常任务ID:{}",taskId);
            e.printStackTrace();
        }
        return task;
    }

 根据任务的时间类型,删除 Redis 中 List 与 Zset 中保存的任务信息

private void removeTaskFromRedis(Task task) {

        String key = task.getTaskType() + "_" +task.getPriority();
        //1. 执行时间小于当前时间,则进行删除任务
        if(task.getExecuteTime() <= System.currentTimeMillis()){

            cacheService.lRemove(ScheduleConstants.TOPIC+key,0,JSON.toJSONString(task)); //list
        }else{
            cacheService.zRemove(ScheduleConstants.FUTURE+key,JSON.toJSONString(task)); //zset
        }
    }

调用以上方法

public boolean cancelTask(long taskId) {

        boolean loop = false;

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

        //2.删除 redis 中的数据
        if(task!=null){
            removeTaskFromRedis(task);
            loop = true;
        }
        return loop;
    }

【拉取任务】

 由于 List 中存储的任务是以 JSON 的形式进行存储的,所以需要将其进行 parseObj 序列化

  使用 lRightPop() 将需要立即执行的任务从 List 中拉取出来,并更新任务日志的状态

public Task pullTask(int type, int priority) {

        Task task = null;

        try {
            String key = type + "_" +priority;

            //1.从 list 中使用 pop 拉取任务
            String taskJSON = cacheService.lRightPop(key);  //解析出来的信息是 JSON 字段
            if(StringUtils.isNotBlank(taskJSON)){
                task = JSON.parseObject(taskJSON, Task.class);

                //1.1.在数据库中删除任务,更新任务日志
                deleteTask_UpdateTaskLog(task.getTaskId(), ScheduleConstants.EXECUTED);  //已执行
            }
        }catch (Exception e){
            e.printStackTrace();
            log.error("拉取任务异常!");
        }
        return task;
    }


3、实现未来数据的定时更新

将任务根据执行的时间,分别存入 Redis 中的 List 与 Zset 中后

还需要判断 Zset 中进行预设时间的任务,是否到了需要执行的时间,到了的话需要进行任务消费

所以,需要设定一个时间,定时的将 Zset 中的数据推送到 List 中,避免任务的堆积与消费延时

【分析问题】

  在任务推送时,需要将 Redis 中所有的 future 任务提取出来进行遍历判断(通过 key 获取)

  在进行全局模糊匹配 Key 值获取的时候,一般有两种方法:Keys  和  Scan

  Keys:keys的模糊匹配功能很方便也很强大,但是在生产环境需要慎用;开发中使用 keys的模糊匹配却发现 Redis 的 CPU 使用率极高,Redis是单线程,会被堵塞

  Scan:SCAN 命令是一个基于游标的迭代器,SCAN 命令每次被调用之后, 都会向用户返回一个新的游标, 用户在下次迭代时需要使用这个新游标作为 SCAN 命令的游标参数, 以此来延续之前的迭代过程

这里,我们使用 Scan 技术进行模糊匹配

根据模糊匹配获取对应的任务后,需要进行消息的推送,Redis 中一般存在两种消息交互的方法:

普通 Redis 客户端和服务器交互模式

Pipeline 消息管道的请求模型

根据场景以及考虑到效率的问题,这里我们使用管道技术进行消息的推送

  以上代码实现: 

 //1.查询所有未来数值的 key
            Set<String> future_keys = cacheService.scan(ScheduleConstants.FUTURE + "*");

            future_keys.forEach(new Consumer<String>() {    //future_100_20
                @Override
                public void accept(String future_key) {

                    //以 future 进行分组 =》  future + 100_20 ,然后以 topic 前缀进行拼接
                    String topic_Key = ScheduleConstants.TOPIC + future_key.split(ScheduleConstants.FUTURE)[1];

                    //1.1 根据 key 查询符合条件的信息(即判断执行的时间是否大于当前时间,若小于或等于,则符合条件)
                    Set<String> tasks = cacheService.zRangeByScore(future_key, 0, System.currentTimeMillis());

                    //2. 进行同步数据
                    if (!tasks.isEmpty()) {
                        //2.1 使用管道技术,将任务数据批量同步到 list 中,等待消费
                        cacheService.refreshWithPipeline(future_key, topic_Key, tasks);
                        log.info("将定时任务 " + future_key + " 刷新到了 " + topic_Key);
                    }
                }
            });

【分析问题】

  这是在单服务下进行消息的推送,若在多服务下进行,由于多个 Tomcat 中对应着不同的 JVM ,所以所控制的锁也不一样,这样,就又会出现线程同步问题

【解决问题】

   对于这种情况,使用分布式锁可能是最好的选择;而实现分布式锁的方法多种多样,而 Redis 中所提供的 SetNX 正好可以解决

  SetNX 分布式锁代码如下:

   /**
     * 使用 setnx 实现分布式锁
     */
    public String tryLock(String name, long 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]
            Boolean result = conn.set(
                    name.getBytes(),
                    token.getBytes(),
                    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;
    }

完整代码如下:

@Scheduled(cron = "0 */1 * * * ?")  //定时,每分钟刷新一次
    public void refreshTask(){

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

        if(StringUtils.isNotBlank(token) && token.length()!=0) {    //进行 NX 加锁操作,使不同服务下同一时刻只能有一个抢占当前任务
            //1.查询所有未来数值的 key
            Set<String> future_keys = cacheService.scan(ScheduleConstants.FUTURE + "*");

            future_keys.forEach(new Consumer<String>() {    //future_100_20
                @Override
                public void accept(String future_key) {

                    //以 future 进行分组 =》  future + 100_20 ,然后以 topic 前缀进行拼接
                    String topic_Key = ScheduleConstants.TOPIC + future_key.split(ScheduleConstants.FUTURE)[1];

                    //1.1 根据 key 查询符合条件的信息(即判断执行的时间是否大于当前时间,若小于或等于,则符合条件)
                    Set<String> tasks = cacheService.zRangeByScore(future_key, 0, System.currentTimeMillis());

                    //2. 进行同步数据
                    if (!tasks.isEmpty()) {
                        //2.1 使用管道技术,将任务数据批量同步到 list 中,等待消费
                        cacheService.refreshWithPipeline(future_key, topic_Key, tasks);
                        log.info("将定时任务 " + future_key + " 刷新到了 " + topic_Key);
                    }
                }
            });

        }
    }

  


4、将数据库中的任务数据,同步到 Redis 中

由于时间是流动的,任务的执行时间是死的,所以需要进行动态的数据更新,保证数据的有效性

流程图如下所示:

  为了数据同步的时候,避免数据库中的数据,与 Redis 中未消费的任务的重复;所以,需要清除 Redis 中所有任务的缓存数据,以确保同步到 Redis 中的数据是最新的

public void clearCacheByRedis(){

        Set<String> topic_keys = cacheService.scan(ScheduleConstants.TOPIC + "*");  //list 中的所有任务的 key
        Set<String> future_keys = cacheService.scan(ScheduleConstants.FUTURE + "*"); //zset 中所有任务中的 key

        cacheService.delete(topic_keys);
        cacheService.delete(future_keys);
    }

任务同步的代码如下:

这里使用 @PostConstruct 注解 进行方法的初始化操作(根据实际情况定义)

    @PostConstruct  //进行初始化操作,每当启动微服务时,当前方法就会执行一次
    @Scheduled(cron = "0 */5 * * * ?")  //每五分钟执行一次
    public void renewDBTasks_To_Redis(){

        //1.清除 redis 中的缓存
        clearCacheByRedis();

        //2.查询 DB 中执行时间小于预设时间的任务
        //2.1.获取未来 5 分钟之后的预设时间
        Calendar calendar = Calendar.getInstance();  //获取当前日期和时间的日历实例
        calendar.add(Calendar.MINUTE,5);
        long calendarTimeInMillis = calendar.getTimeInMillis(); //获取其毫秒值

        LambdaQueryWrapper<Taskinfo> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.lt(Taskinfo::getExecuteTime,calendarTimeInMillis);

        List<Taskinfo> taskInfos = taskinfoMapper.selectList(queryWrapper);

        //3.将数据库中数据同步保存到 redis 中
        if(taskInfos!=null && taskInfos.size()>0) {
            taskInfos.forEach(new Consumer<Taskinfo>() {
                @Override
                public void accept(Taskinfo taskinfo) {
                    Task task = new Task();
                    BeanUtils.copyProperties(taskinfo,task);
                    task.setExecuteTime(taskinfo.getExecuteTime().getTime());
                    //3.1 由它内部判断,是存储在 list 中还是 zset 中
                    addTaskToRedis(task);
                }
            });
        }
        log.info("成功将数据库中的数据更新同步到了 redis 中");
    }

所有方法的完整代码:

@Slf4j
@Service
@Transactional
public class TaskServiceImpl implements TaskService {

    @Resource
    private TaskinfoMapper taskinfoMapper;

    @Resource
    private TaskinfoLogsMapper taskinfoLogsMapper;

    @Resource
    private CacheService cacheService;

    /**
     * 添加任务
     * @param task   任务对象
     * @return 任务ID
     */
    @Override
    public long addTask(Task task) {

        //1.添加任务到 DB 中,保证任务的持久化
        boolean res = addTackToDB(task);

        if(res) {
            //2.将任务添加到 redis 中
            addTaskToRedis(task);
        }
        return task.getTaskId();
    }


    /**
     * 将已完成的任务删除
     */
    @Override
    public boolean cancelTask(long taskId) {

        boolean loop = false;

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

        //2.删除 redis 中的数据
        if(task!=null){
            removeTaskFromRedis(task);
            loop = true;
        }
        return loop;
    }


    /**
     * 按照类型和优先级进行拉取 list 中的任务
     */
    @Override
    public Task pullTask(int type, int priority) {

        Task task = null;

        try {
            String key = type + "_" +priority;

            //1.从 list 中使用 pop 拉取任务
            String taskJSON = cacheService.lRightPop(key);  //解析出来的信息是 JSON 字段
            if(StringUtils.isNotBlank(taskJSON)){
                task = JSON.parseObject(taskJSON, Task.class);

                //1.1.在数据库中删除任务,更新任务日志
                deleteTask_UpdateTaskLog(task.getTaskId(), ScheduleConstants.EXECUTED);  //已执行
            }
        }catch (Exception e){
            e.printStackTrace();
            log.error("拉取任务异常!");
        }
        return task;
    }


    /**
     * 未来数据的更新,将 zset 中的任务推送到 list 中
     */
    @Scheduled(cron = "0 */1 * * * ?")  //定时,每分钟刷新一次
    public void refreshTask(){

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

        if(StringUtils.isNotBlank(token) && token.length()!=0) {    //进行 NX 加锁操作,使不同服务下同一时刻只能有一个抢占当前任务
            //1.查询所有未来数值的 key
            Set<String> future_keys = cacheService.scan(ScheduleConstants.FUTURE + "*");

            future_keys.forEach(new Consumer<String>() {    //future_100_20
                @Override
                public void accept(String future_key) {

                    //以 future 进行分组 =》  future + 100_20 ,然后以 topic 前缀进行拼接
                    String topic_Key = ScheduleConstants.TOPIC + future_key.split(ScheduleConstants.FUTURE)[1];

                    //1.1 根据 key 查询符合条件的信息(即判断执行的时间是否大于当前时间,若小于或等于,则符合条件)
                    Set<String> tasks = cacheService.zRangeByScore(future_key, 0, System.currentTimeMillis());

                    //2. 进行同步数据
                    if (!tasks.isEmpty()) {
                        //2.1 使用管道技术,将任务数据批量同步到 list 中,等待消费
                        cacheService.refreshWithPipeline(future_key, topic_Key, tasks);
                        log.info("将定时任务 " + future_key + " 刷新到了 " + topic_Key);
                    }
                }
            });

        }
    }


    /**
     * 数据库中的任务同步到 redis 中,保证数据的一致性
     */
    @PostConstruct  //进行初始化操作,每当启动微服务时,当前方法就会执行一次
    @Scheduled(cron = "0 */5 * * * ?")  //每五分钟执行一次
    public void renewDBTasks_To_Redis(){

        //1.清除 redis 中的缓存
        clearCacheByRedis();

        //2.查询 DB 中执行时间小于预设时间的任务
        //2.1.获取未来 5 分钟之后的预设时间
        Calendar calendar = Calendar.getInstance();  //获取当前日期和时间的日历实例
        calendar.add(Calendar.MINUTE,5);
        long calendarTimeInMillis = calendar.getTimeInMillis(); //获取其毫秒值

        LambdaQueryWrapper<Taskinfo> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.lt(Taskinfo::getExecuteTime,calendarTimeInMillis);

        List<Taskinfo> taskInfos = taskinfoMapper.selectList(queryWrapper);

        //3.将数据库中数据同步保存到 redis 中
        if(taskInfos!=null && taskInfos.size()>0) {
            taskInfos.forEach(new Consumer<Taskinfo>() {
                @Override
                public void accept(Taskinfo taskinfo) {
                    Task task = new Task();
                    BeanUtils.copyProperties(taskinfo,task);
                    task.setExecuteTime(taskinfo.getExecuteTime().getTime());
                    //3.1 由它内部判断,是存储在 list 中还是 zset 中
                    addTaskToRedis(task);
                }
            });
        }
        log.info("成功将数据库中的数据更新同步到了 redis 中");
    }


    /*******************************************************************************************************************
     * 删除 redis 中对应的任务
     */
    private void removeTaskFromRedis(Task task) {

        String key = task.getTaskType() + "_" +task.getPriority();
        //1. 执行时间小于当前时间,则进行删除任务
        if(task.getExecuteTime() <= System.currentTimeMillis()){

            cacheService.lRemove(ScheduleConstants.TOPIC+key,0,JSON.toJSONString(task)); //list
        }else{
            cacheService.zRemove(ScheduleConstants.FUTURE+key,JSON.toJSONString(task)); //zset
        }
    }


    /**
     * 删除 redis 中所有的缓存数据
     */
    public void clearCacheByRedis(){

        Set<String> topic_keys = cacheService.scan(ScheduleConstants.TOPIC + "*");  //list 中的所有任务的 key
        Set<String> future_keys = cacheService.scan(ScheduleConstants.FUTURE + "*"); //zset 中所有任务中的 key

        cacheService.delete(topic_keys);
        cacheService.delete(future_keys);
    }



    /**
     * 在数据库中删除任务,更新任务日志
     */
    private Task deleteTask_UpdateTaskLog(long taskId, int status) {

        Task task =null;

        try {
            //1.删除任务
            taskinfoMapper.deleteById(taskId);

            //2.更新任务日志
            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("任务处理失败,异常任务ID:{}",taskId);
            e.printStackTrace();
        }
        return task;
    }


    /**
     * 将任务存到 redis 中
     */
    private void addTaskToRedis(Task task) {

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

        //1.获取未来 5 分钟之后的预设时间
        Calendar calendar = Calendar.getInstance();  //获取当前日期和时间的日历实例
        calendar.add(Calendar.MINUTE,5);
        long calendarTimeInMillis = calendar.getTimeInMillis(); //获取其毫秒值

        //2.1 若任务执行的时间小于当前时间,则直接放入 list 数据结构中
        if(task.getExecuteTime() <= System.currentTimeMillis()){
            cacheService.lLeftPush(ScheduleConstants.TOPIC+key, JSON.toJSONString(task));
        }else if(task.getExecuteTime() <= calendarTimeInMillis){
            //2.2 若任务执行的时间大于当前时间 并且 小于等于预设时间(未来5分钟),则直接放入 zset 中按照分值排序进行存储
            cacheService.zAdd(ScheduleConstants.FUTURE+ key,JSON.toJSONString(task),task.getExecuteTime());
        }
    }


    /**
     * 将任务添加到数据库中
     */
    private boolean addTackToDB(Task task) {

        boolean loop = false;

        try {
            //1.保存任务表
            Taskinfo taskinfo = new Taskinfo();
            BeanUtils.copyProperties(task, taskinfo);
            taskinfo.setExecuteTime(new Date(task.getExecuteTime()));
            taskinfoMapper.insert(taskinfo);

            task.setTaskId(taskinfo.getTaskId()); //将 任务ID 传给前端

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

            loop = true;
        }catch (Exception exception){
            exception.printStackTrace();
        }
        return loop;
    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1243949.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

multiset和multimap(map和set的可重复版)

multiset和multimap multisetmultiset的使用 multimapmultimap的使用 这里有讲解map和set的详细使用&#xff1a; C中的map和set使用详解 multiset multiset的文档介绍 翻译&#xff1a; multiset是按照特定顺序存储元素的容器&#xff0c;其中元素是可以重复的。在multiset…

Vue学习笔记-Object.defineproperty函数

文章目录 前文提要Object.defineProperty作用Object.defineProperty参数使用例图getter&#xff0c;也就是get函数setter&#xff0c;也就是set函数 前文提要 本人仅做个人学习记录&#xff0c;如有错误&#xff0c;请多包涵 Object.defineProperty作用 当在js中声明了一个变…

深入理解JVM 类加载机制

深入理解JVM 类加载机制 虚拟机如何加载Class文件&#xff1f; Class文件中的信息进入到虚拟机后会发生什么变化&#xff1f; 类加载机制就是Java虚拟机把描述类的数据从Class文件加载到内存&#xff0c;并对数据进行校验、转换解析和初始化&#xff0c;最终形成可以被虚拟机…

成为独立开发者有多难

首先自我介绍&#xff1a;我是一名前端开发工程师&#xff0c;7年的前端开发经验。CSDN 九段刀客_js,vue,ReactNative-CSDN博客,80多万的访问量&#xff0c;1万多的粉丝。 相信80%的程序员的终极梦想都是成为一名独立开发者&#xff0c;不用找工作有自己的产品可以有睡后收入。…

函数与数组

一.函数 1、函数的作用 定义较为复杂的但是需要重复使用的内容&#xff0c;以便再次使用&#xff0c;可以直接调用&#xff0c;节约时间&#xff0c;提高效率。 语句块定义成函数约等于别名&#xff0c;定义函数&#xff0c;再引用函数。 封装的可重复利用的具有特定功能的…

【机器学习 | ARIMA】经典时间序列模型ARIMA定阶最佳实践,确定不来看看?

&#x1f935;‍♂️ 个人主页: AI_magician &#x1f4e1;主页地址&#xff1a; 作者简介&#xff1a;CSDN内容合伙人&#xff0c;全栈领域优质创作者。 &#x1f468;‍&#x1f4bb;景愿&#xff1a;旨在于能和更多的热爱计算机的伙伴一起成长&#xff01;&#xff01;&…

宣传技能培训1——《新闻摄影技巧》

新闻摄影技巧 写在最前面摘要 构图与拍摄角度景别人物表情与叙事远景与特写 构图与拍摄角度案例 主体、陪体、前景、背景强调主体利用前景和背景层次感的创造 探索新闻摄影中的构图技巧基本构图技巧构图技巧的应用实例实例分析1. 黄金分割和九宫格2. 三角型构图3. 引导线构图4.…

JSP过滤器和监听器

什么是过滤器 Servlet过滤器与Servlet十分相似&#xff0c;但它具有拦截客户端&#xff08;浏览器&#xff09;请求的功能&#xff0c;Servlet过滤器可以改变请求中的内容&#xff0c;来满足实际开发中的需要。 对于程序开发人员而言&#xff0c;过滤器实质就是在Web应用服务器…

Matrix

Matrix 如下是四种变换对应的控制参数&#xff1a; Rect 常用的一个“绘画相关的工具类”&#xff0c;常用来描述长方形/正方形&#xff0c;他只有4个属性&#xff1a; public int left; public int top; public int right; public int bottom; 这4个属性描述着这一个“方块…

IAR为恩智浦S32M2提供全面支持,提升电机控制能力

IAR Embedded Workbench for Arm已全面支持恩智浦最新的S32系列&#xff0c;可加速软件定义汽车的车身和舒适性应用的开发 瑞典乌普萨拉&#xff0c;2023年11月22日 – 嵌入式开发软件和服务的全球领导者IAR现已全面支持恩智浦半导体&#xff08;NXP Semiconductors&#xff0…

数字化时代,数据资产交易困境的治理

数据作为五大生产要素之一&#xff0c;正与行业应用深度结合&#xff0c;成为推动产业、企业数字化转型升级的重要驱动力量。数据作为基础的战略性资源和新的生产要素&#xff0c;如果不能充分流通和交易&#xff0c;就丧失了价值和作用。因此&#xff0c;数据资产的交易关系着…

8.HTML中表单标签

8. 表单标签 8.1 为什么需要表单 使用表单的目的是为了收集用户信息&#xff0c; 在我们网页中&#xff0c;我们也需要跟用户进行交互&#xff0c;收集用户资料&#xff0c;此时就需要表单 8.2 表单的组成 在HTML中&#xff0c;一个完整的表单通常由表单域&#xff0c;表单控件…

微信小程序 - 云开发

1、小程序云开发 1.1、云开发简介 小程序云开发是微信团队联合腾讯云推出的专业的小程序开发服务。开发者可以使用云开发快速开发小程序、小游戏、公众号网页等&#xff0c;并且原生打通微信开放能力。开发者无需搭建服务器&#xff0c;可免鉴权直接使用平台提供的API进行业务…

港口大型设备状态监测及预测性维护策略

在现代港口运营中&#xff0c;大型设备的正常运行对于保障港口作业的高效性至关重要。为了实现设备的可靠性和持续性&#xff0c;港口管理者需要采取一系列状态监测和预测性维护策略。 推进自动化和智能化是提高港口大型设备状态监测和维护管理效率的重要途径。通过应用先进的…

JavaWeb——感谢尚硅谷官方文档

JavaWeb——感谢尚硅谷官方文档 XML一、xml简介二、xml的语法1、文档申明2、xml注释3、xml元素4、xml属性5、xml语法规则 三、xml解析技术1、使用dom4j解析xml Tomcat一、JavaWeb的概念二、web资源的分类三、常见的web服务器四、Tomcat的使用1、安装2、Tomcat的目录介绍3 启动T…

【Python】np.unique() 介绍与使用

简述 numpy.unique&#xff1a;用于去除数组中重复元素&#xff0c;并从小到大排序&#xff08;找到唯一元素并排序&#xff09;。 def unique(ar, return_indexFalse, return_inverseFalse,return_countsFalse, axisNone):ar: 这是输入的数组或类数组对象。return_index: 如…

6.1.webrc媒体协商

那今天呢&#xff1f;我们来看一下y8 rtc的媒体协商&#xff0c;那实际上在我们之前的课程中呢&#xff1f;我已经向你介绍过y8 rtc的媒体协商了。只不过呢&#xff0c;角度是不一样的&#xff0c;在之前介绍外边tc媒体协商的时候呢&#xff0c;我们是从应用的角度来看。那web …

【Vue】插值表达式

作用&#xff1a;利用表达式进行插值渲染 语法&#xff1a;{ { 表达式 } } 目录 案例一&#xff1a; 案例二&#xff1a; 案例三&#xff1a; ​编辑 注意&#xff1a; 案例一&#xff1a; <!DOCTYPE html> <html lang"en"> <head><me…

基于SSM的校园奶茶点单管理系统

基于SSM的校园奶茶点单管理系统的设计与实现~ 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;SpringMyBatisSpringMVC工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 系统展示 主页 奶茶列表 登录界面 管理员界面 用户界面 摘要 随着社会的发展和科技的进…

YOLOv8训练自己的目标检测数据集

YOLOv8训练自己的目标检测数据集 目录标题 源码下载环境配置安装包训练自己的数据集数据集文件格式数据集文件配置超参数文件配置训练数据集命令行训练脚本.py文件训练 进行detect显示detect的效果 源码下载 YOLOv8官方的GitHub代码&#xff0c;同时上面也有基础环境的配置要…