延时任务定时发布,基于 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.查询所有未来数值的 keySet<String> future_keys = cacheService.scan(ScheduleConstants.FUTURE + "*");future_keys.forEach(new Consumer<String>() { //future_100_20@Overridepublic 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.查询所有未来数值的 keySet<String> future_keys = cacheService.scan(ScheduleConstants.FUTURE + "*");future_keys.forEach(new Consumer<String>() { //future_100_20@Overridepublic 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 中的所有任务的 keySet<String> future_keys = cacheService.scan(ScheduleConstants.FUTURE + "*"); //zset 中所有任务中的 keycacheService.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>() {@Overridepublic 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 {@Resourceprivate TaskinfoMapper taskinfoMapper;@Resourceprivate TaskinfoLogsMapper taskinfoLogsMapper;@Resourceprivate CacheService cacheService;/*** 添加任务* @param task 任务对象* @return 任务ID*/@Overridepublic long addTask(Task task) {//1.添加任务到 DB 中,保证任务的持久化boolean res = addTackToDB(task);if(res) {//2.将任务添加到 redis 中addTaskToRedis(task);}return task.getTaskId();}/*** 将已完成的任务删除*/@Overridepublic 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 中的任务*/@Overridepublic 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.查询所有未来数值的 keySet<String> future_keys = cacheService.scan(ScheduleConstants.FUTURE + "*");future_keys.forEach(new Consumer<String>() { //future_100_20@Overridepublic 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>() {@Overridepublic 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 中的所有任务的 keySet<String> future_keys = cacheService.scan(ScheduleConstants.FUTURE + "*"); //zset 中所有任务中的 keycacheService.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;}
相关文章:
延时任务定时发布,基于 Redis 与 DB 实现
目录 1、什么是延时任务,分别可以使用哪些技术实现? 1.2 使用 Redis 和 DB 相结合的思路图以及分析 2、实现添加任务、取消任务、拉取任务 3、实现未来数据的定时更新 4、将数据库中的任务数据,同步到 Redis 中 1、什么是延时任务ÿ…...
Nacos安装使用
Nacos安装使用 官方下载地址: https://github.com/alibaba/nacos/releases 官方文档地址: https://nacos.io/zh-cn/docs/quick-start.html Nacos介绍 Nacos是阿里巴巴开源的一款支持服务注册与发现,配置管理以及微服务管理的组件。用来取代以前常用的注册中心&a…...
了解抽象思维的应用与实践
目录 一、快速了解抽象思维 (一)抽象思维的本质理解 (二)系统架构中的重要性 (三)软件开发中抽象的基本过程思考 意识和手段 抽象的方式 抽象层次的权衡 二、业务中的应用实践 (一&…...
【阿里云】图像识别 智能分类识别 增加垃圾桶开关盖功能点和OLED显示功能点(二)
一、增加垃圾桶开关盖功能 环境准备 二、PWM 频率的公式 三、pthread_detach分离线程,使其在退出时能够自动释放资源 四、具体代码实现 图像识别数据及调试信息wget-log打印日志文件 五、增加OLED显示功能 六、功能点实现语音交互视频 一、增加垃圾桶开关盖功能…...
linux centos上安装python3.11.x详细完整教程
一. 安装步骤 注意: 1、安装python3.11的其他版本替换下面的版本信息即可。(如想安装3.11.5将案例中的3.11.0替换成3.11.5即可) #下载最新的软件安装包 wget https://www.python.org/ftp/python/3.11.0/Python-3.11.0.tgz#解压缩安装包 tar -xzf Python-3.11.0.tg…...
itext - PDF模板套打
目录 环境配置 快速使用 代码实现 添加图片 封装 项目需求:获取列表数据之后直接将数据生成一个pdf。因此需要使用到 itext 对pdf进行直接操作。 环境配置 需要为pdf添加文字域,因此需要安装Adobe Acrobat 准备一个空的PDF文件,如果有现…...
指针的使用和传址调用
1.引入 学习指针的⽬的是使⽤指针解决问题,那什么问题,⾮指针不可呢? 例如:写⼀个函数,交换两个整型变量的值。 ⼀番思考后,我们可能写出这样的代码: #include <stdio.h> void Swap1(int…...
【Leetcode】【实现循环队列】【数据结构】
代码实现: typedef struct {int front;int back;int k;int* a;} MyCircularQueue;bool myCircularQueueIsEmpty(MyCircularQueue* obj) {return obj->frontobj->back; }bool myCircularQueueIsFull(MyCircularQueue* obj) {return (obj->back1)%(obj->…...
电力感知边缘计算网关产品设计方案-软件架构(业务流程)
软件架构(业务流程) 基于前端系统提供的硬件通信平台,后端系统以控制执行单元为核心,协同控制通信管理、驱动适配、存储单元等职能单元完成与前端系统的通信数据交互业务,在经历以下业务流程后,完成设备自适应通信业务功能。 1.外部设备通信前端系统 前端系统连接新的…...
【labelimg打不开】
labelimg打不开 一、 报错1.1 排除错误 **解决方法:** 一、 报错 当运行labelimg程序报错此条时 AssertionError: Missing string id : useDefaultLabel 1.1 排除错误 第一,进入创建的虚拟环境,输入pip list 查看是否安装了labelimg 第二&…...
JAVA之异常详解
1. 异常的概念与体系结构 1.1 异常的概念 在Java中,将程序执行过程中发生的不正常行为称为异常 1. 算术异常 public class Test {public static void main(String[] args) {System.out.println(10/0);} } 因为 0 不能当被除数,所以报出了异常&#…...
模型优化【2】-剪枝[局部剪枝]
模型剪枝是一种常见的模型压缩技术,它可以通过去除模型中不必要的参数和结构来减小模型的大小和计算量,从而提高模型的效率和速度。在 PyTorch 中,我们可以使用一些库和工具来实现模型剪枝。 pytorch实现剪枝的思路是生成一个掩码࿰…...
VMware 系列:ESXI6.7升级7.0
ESXI6.7升级7.0 一、下载补丁二、上传文件三 启用Shell四、登录Shell后台五、删除不兼容驱动六、正常升级最近,将一台使用ESXI6.7的虚拟机升级到了7.0版本,下面记录一下自己的升级过程。 升级条件 首先确保硬件是否能升级到7.0版本,物理网卡驱动为e1000e不能升级,如果是ig…...
4-20mA高精度采集方案
下载链接!https://mp.weixin.qq.com/s?__bizMzU2OTc4ODA4OA&mid2247557466&idx1&snb5a323285c2629a41d2a896764db27eb&chksmfcfaf28dcb8d7b9bb6211030d9bda53db63ab51f765b4165d9fa630e54301f0406efdabff0fb&token976581939&langzh_CN#rd …...
案例022:基于微信小程序的行政复议在线预约系统
文末获取源码 开发语言:Java 框架:SSM JDK版本:JDK1.8 数据库:mysql 5.7 开发软件:eclipse/myeclipse/idea Maven包:Maven3.5.4 小程序框架:uniapp 小程序开发软件:HBuilder X 小程序…...
Go 工具链详解(七):模块缓存清理工具
go mod 缓存 在 Golang 中,模块是对一组版本化的包的集合的描述。Go 1.11 版本引入了模块支持,通过 go mod 命令提供了对模块的管理。Go 模块的一个重要特性是依赖管理,可以清晰地定义项目所依赖的模块及对应的版本,并确保代码使…...
1.7 C语言之函数概述
1.7 C语言之函数概述 一、概述二、练习 一、概述 函数就是把一组计算操作封装起来,供程序员调用,我们只需知道其提供了什么功能,而无需关注具体实现细节(前提是其久经考验,设计没有问题,后续我们自己写的函数大概率还…...
CTA-GAN:基于生成对抗性网络的主动脉和颈动脉非集中CT血管造影 CT到增强CT的合成技术
Generative Adversarial Network–based Noncontrast CT Angiography for Aorta and Carotid Arteries 基于生成对抗性网络的主动脉和颈动脉非集中CT血管造影背景贡献实验方法损失函数Thinking 基于生成对抗性网络的主动脉和颈动脉非集中CT血管造影 https://github.com/ying-f…...
电源控制系统架构(PCSA)之电源管理基础设施组件
目录 6.5 电源管理基础设施组件 6.5.1 电源策略单元 6.5.2 时钟控制器 6.5.3 低功耗Distributor 6.5.4 低功耗Combiner 6.5.5 P-Channel到Q-Channel转换器 6.5 电源管理基础设施组件 6.5.1 电源策略单元 本节介绍电源策略单元(Power Policy Unit, PPU)。PPU的完整细节见…...
影刀RPA_boss直聘翻页(避坑)
boss直聘翻页这里有个坑 问题: 无限循环中,点击下一页按钮,直到不可点击为止。 发现,在点到第5页的时候,再次点击下一页,直接就点击了页码10,导致流程直接就结束了。 在第5页进行校验࿰…...
用MATLAB手把手复现:EKF如何让导弹在三维空间里“看”得更准?(附完整代码与误差分析)
三维制导系统中的EKF实战:从MATLAB代码解析到误差优化 导弹在三维空间中的精确制导一直是航空航天领域的核心挑战。传统方法在面对复杂环境干扰时往往力不从心,而扩展卡尔曼滤波(EKF)技术则为这一难题提供了优雅的解决方案。本文将带您深入EKF在三维制导…...
QMCDecode终极指南:3分钟解锁QQ音乐加密文件,实现音乐自由
QMCDecode终极指南:3分钟解锁QQ音乐加密文件,实现音乐自由 【免费下载链接】QMCDecode QQ音乐QMC格式转换为普通格式(qmcflac转flac,qmc0,qmc3转mp3, mflac,mflac0等转flac),仅支持macOS,可自动识别到QQ音乐下载目录&a…...
为什么你的手写笔记在高分辨率屏幕上总是模糊?Xournal++渲染优化终极指南
为什么你的手写笔记在高分辨率屏幕上总是模糊?Xournal渲染优化终极指南 【免费下载链接】xournalpp Xournal is a handwriting notetaking software with PDF annotation support. Written in C with GTK3, supporting Linux (e.g. Ubuntu, Debian, Arch, SUSE), ma…...
B站视频下载终极指南:3分钟掌握BilibiliDown高效批量下载技巧
B站视频下载终极指南:3分钟掌握BilibiliDown高效批量下载技巧 【免费下载链接】BilibiliDown (GUI-多平台支持) B站 哔哩哔哩 视频下载器。支持稍后再看、收藏夹、UP主视频批量下载|Bilibili Video Downloader 😳 项目地址: https://gitcode.com/gh_mi…...
PyTorch 2.8镜像实战案例:RTX 4090D运行MiniCPM-Llama3-8B多语言问答
PyTorch 2.8镜像实战案例:RTX 4090D运行MiniCPM-Llama3-8B多语言问答 1. 环境准备与快速验证 1.1 镜像基础配置 这个专为RTX 4090D优化的PyTorch 2.8镜像已经预装了深度学习所需的所有关键组件: 核心框架:PyTorch 2.8 (CUDA 12.4编译版)加…...
抖音批量下载工具终极指南:3分钟快速上手,轻松获取无水印内容
抖音批量下载工具终极指南:3分钟快速上手,轻松获取无水印内容 【免费下载链接】douyin-downloader A practical Douyin downloader for both single-item and profile batch downloads, with progress display, retries, SQLite deduplication, and brow…...
WorkshopDL:打破平台壁垒的模组自由之门
WorkshopDL:打破平台壁垒的模组自由之门 【免费下载链接】WorkshopDL WorkshopDL - The Best Steam Workshop Downloader 项目地址: https://gitcode.com/gh_mirrors/wo/WorkshopDL 想象一下,您在Epic Games Store上购买了一款心仪已久的游戏&…...
QMCDecode终极指南:3分钟解锁QQ音乐加密文件,释放你的音乐自由
QMCDecode终极指南:3分钟解锁QQ音乐加密文件,释放你的音乐自由 【免费下载链接】QMCDecode QQ音乐QMC格式转换为普通格式(qmcflac转flac,qmc0,qmc3转mp3, mflac,mflac0等转flac),仅支持macOS,可自动识别到QQ音乐下载目…...
交通灯控制电路里的‘幽灵’:一次完整的竞争与冒险现象排查实录(附波形分析)
交通灯控制电路里的‘幽灵’:一次完整的竞争与冒险现象排查实录(附波形分析) 数字电路设计中最令人头疼的问题之一,莫过于那些看似随机出现的异常现象。上周在实验室调试一个交通灯控制电路时,我们就遇到了这样一个&qu…...
Ostrakon-VL-8B一键部署教程:基于Ubuntu的餐饮视觉分析环境搭建
Ostrakon-VL-8B一键部署教程:基于Ubuntu的餐饮视觉分析环境搭建 你是不是也遇到过这样的场景?面对餐厅后厨监控里堆积如山的食材图片,或者外卖平台上成千上万的菜品照片,想快速分析它们的种类、新鲜度、摆放合规性,却…...
