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

目录

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.mfbz.cn/a/182234.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

单链表实现【队列】

目录 队列的概念及其结构 队列的实现 数组队列 链式队列 队列的常见接口的实现 主函数Test.c 头文件&函数声明Queue.h 头文件 函数声明 函数实现Queue.c 初始化QueueInit 创建节点Createnode 空间释放QueueDestroy 入队列QueuePush 出队列QueuePop 队头元…

Samsung下origen中uboot的配置与编译

uboot的特点&#xff1a; n代码结构清晰 n 支持丰富的处理器与开发板&#xff0c;易于移植 n 支持丰富的用户命令 n 支持丰富的网络协议 n 支持丰富的文件系统 n 支持丰富的设备驱动 n 更新活跃、用户较多、资料丰富 n 开放源代码 n 较高的稳定性 n 不具有通用性&#xff08;不…

【前端】必学知识ES6 1小时学会

1.ES6概述 2.let和const的认识 3.let、const、var的区别 4.模板字符串 5.函数默认参数 6.箭头函数【重点】 ​编辑7.对象初始化简写以及案例分析 【重点】 8.对象解构 8.对象传播操作符 9.对象传播操作符案例分析 ​编辑 10.数组Map 11.数组Reduce 12.NodeJS小结 …

【Redisson】基于自定义注解的Redisson分布式锁实现

前言 在项目中&#xff0c;经常需要使用Redisson分布式锁来保证并发操作的安全性。在未引入基于注解的分布式锁之前&#xff0c;我们需要手动编写获取锁、判断锁、释放锁的逻辑&#xff0c;导致代码重复且冗长。为了简化这一过程&#xff0c;我们引入了基于注解的分布式锁&…

【JavaSE】不允许你不会使用String类

&#x1f3a5; 个人主页&#xff1a;深鱼~&#x1f525;收录专栏&#xff1a;JavaSE&#x1f304;欢迎 &#x1f44d;点赞✍评论⭐收藏 目录 前言&#xff1a; 一、常用方法 1.1 字符串构造 1.2 String对象的比较 &#xff08;1&#xff09;比较是否引用同一个对象 注意…

【目标检测】保姆级别教程从零开始实现基于Yolov8的一次性筷子计数

前言 一&#xff0c;环境配置 一&#xff0c;虚拟环境创建 二&#xff0c;安装资源包 前言 最近事情比较少&#xff0c;无意间刷到群聊里分享的基于百度飞浆平台的一次性筷子检测&#xff0c;感觉很有意思&#xff0c;恰巧自己最近在学习Yolov8&#xff0c;于是看看能不能复…

ActiveMQ消息中间件应用场景

一、ActiveMQ简介 ActiveMQ是Apache出品&#xff0c;最流行的&#xff0c;能力强劲的开源消息总线。ActiveMQ是一个完全支持JMS1.1和J2EE1.4规范的JMS Provide实现。尽管JMS规范出台已经是很久的事情了&#xff0c;但是JMS在当今的J2EE应用中仍然扮演这特殊的地位。 二、Active…

C++每日选择题—Day1

第一题 以下C代码会输出什么? #include <iostream> using namespace std; class A { public:A() {}~A() {} private:static int a; }; int main() {cout << sizeof(A) << endl;return 0; } A&#xff1a;0 B&#xff1a;1 C&#xff1a;4 D&#xff1a;8 答…

6.2.SDP协议

那今天呢&#xff1f;我们来介绍一下sdp协议&#xff0c;那实际上呢&#xff1f;sdp协议非常的简单。我们如果拿到一个stp的文档去看的话&#xff0c;那你要分阅里边的所有的内容会觉得很枯燥&#xff0c;但实际上呢&#xff0c;如果我们按照这张图所展示的结构去看stp的话。你…

Vue3+vite 处理静态资源,解决服务器不显示动态循环img问题

注意&#xff1a; vue2webpack中&#xff0c;通常使用require来动态渲染静态资源。但在vue3vite中&#xff0c;不支持require语法&#xff0c;因此使用require会报undefined&#xff0c;所以官方推荐使用import来动态渲染静态资源。 实现方式动态渲染静态资源 vue2webpack 使…

基于Springboot+Vue选课系统

选课系统要求 (1)数据库表&#xff1a;教师信息表、学生信息表、课程表、选课表 其中&#xff0c;教师信息表、学生信息表和选课表的数据需要提前设置&#xff0c;本题主要操作课程表 (2) 技术架构&#xff1a; 后台使用springboot 前端使用vue-admin-template (3) 考试时间&…

双12电视盒子什么牌子好?数码小编力荐目前最强的电视盒子

最近想买电视盒子的网友非常多&#xff0c;小编收到了很多关于电视盒子方面的咨询&#xff0c;因此我特意整理了今年测评过的电视盒子&#xff0c;总结了五款目前最强的电视盒子&#xff0c;想知道双十二买电视盒子什么牌子好就赶紧收藏起来吧。 推荐一&#xff1a;泰捷WEBOX新…

如何将本地websocket发布至公网并实现远程访问?

本地websocket服务端暴露至公网访问【cpolar内网穿透】 文章目录 本地websocket服务端暴露至公网访问【cpolar内网穿透】1. Java 服务端demo环境2. 在pom文件引入第三包封装的netty框架maven坐标3. 创建服务端,以接口模式调用,方便外部调用4. 启动服务,出现以下信息表示启动成功…

阿里云 E-MapReduce 全面开启 Serverless 时代

作者&#xff1a;李钰 - 阿里云资深技术专家、EMR 负责人 EMR 2.0 平台 阿里云正式发布云原生开源大数据平台EMR 2.0已历经一年时间&#xff0c;如今EMR 2.0全新平台在生产上已经全面落地&#xff0c;资源占比超过60%。EMR 2.0平台之所以在生产上这么快落地&#xff0c;源于其…

Jenkins Ansible 参数构建

首先在Jenkins中创建自由项目 在web端配置完成后在另一台机子上下载nginx 在gitlab端创建项目并创建文件配置代码 在有Jenkins的机器上下载Ansible [rootslave1 ~]# yum -y install epel-release [rootslave1 ~]# yum -y install ansible再进入下载nginx机器中克隆gitlab项目…

生成式AI:SEO的末日?

由于在搜索结果中引入生成式AI (GAI)&#xff0c;以 SEO 为主导的内容的未来成为最近的热门话题&#xff0c;这是有充分理由的。 对于出版商和网站所有者&#xff08;从现在开始我们将他们称为内容创建者&#xff09;的影响可能是毁灭性的。 如下图所示&#xff0c;谷歌新的搜…

spring 是如何开启事务的, 核心原理是什么

文章目录 spring 是如何开启事务的核心原理1 基于注解开启事务2 基于代码来开启事务 spring 是如何开启事务的 核心原理 Spring事务管理的实现有许多细节&#xff0c;如果对整个接口框架有个大体了解会非常有利于我们理解事务&#xff0c;下面通过讲解Spring的事务接口来了解…

使用Wireshark提取流量中图片方法

0.前言 记得一次CTF当中有一题是给了一个pcapng格式的流量包&#xff0c;flag好像在某个响应中的图片里。比较简单&#xff0c;后来也遇到过类似的情况&#xff0c;所以总结和记录一下使用Wireshark提取图片的方法。 提取的前提是HTTP协议&#xff0c;至于HTTPS的协议需要导入服…

QMI8658A(6轴)-EVB 评估板-使用说明书

QMI8658A6<6轴>-EVB 评估板-使用说明书 0.前言 1.硬件准备 1.1 I2C 接口 1.2 USART 接口 1.3 引脚序号功能定义 2.程序运行 0.前言 【相关博文】 【QMI8658 - 姿态传感器学习笔记 - Ⅰ】 【QMI8658 - 姿态传感器学习笔记 - Ⅱ】 【QMI8658 - 姿态传感器学习…

批量创建表空间数据文件(DM8:达梦数据库)

DM8:达梦数据库 - - 批量创建表空间数据文件 环境介绍1 批量创建表空间SQL2 达梦数据库学习使用列表 环境介绍 在某些场景(分区表子表)需要批量创建表空间,给不同的表使用,以下代码是批量创建表空间的SQL语句; 1 批量创建表空间SQL --创建 24个数据表空间,每个表空间有3个数…
最新文章