Redis延时队列在订单超时未报到场景的应用分享
一、引言
在电商、医疗预约等众多业务场景中,经常会遇到需要处理超时任务的情况。比如医疗预约订单,如果患者在支付成功后,到了预约结束时间还未报到,系统需要自动取消订单。为了实现这样的功能,我们可以利用 Redis 延时队列。本文将详细介绍 Redis 延时队列的使用,对比它与其他消息队列的优缺点,并结合实际的订单超时未报到业务代码进行分享。
二、Redis 延时队列详细介绍
2.1 什么是 Redis 延时队列
Redis 延时队列是一种特殊的队列,它允许元素在指定的时间后才被消费。在 Redis 中,通常可以使用有序集合(Sorted Set)或 Redisson 提供的延迟队列来实现。有序集合的分数可以用来表示元素的过期时间,通过不断轮询有序集合,当分数小于当前时间时,就将元素取出消费。而 Redisson 则提供了更方便的 API 来实现延时队列,它内部封装了很多复杂的操作,让开发者可以更简单地使用。
2.2 工作原理
以 Redisson 实现的延时队列为例,它基于 Redis 的 List 和 ZSet 数据结构。当我们向延时队列中添加元素时,Redisson 会将元素存储在一个 ZSet 中,分数为元素的过期时间。同时,会有一个后台线程不断轮询 ZSet,当发现有元素的分数小于当前时间时,就将元素从 ZSet 移动到 List 中,然后消费者就可以从 List 中获取元素进行消费。
三、Redis 延时队列与其他消息队列的对比
3.1 与 MQ(如 RabbitMQ)对比
- 优点
- 简单易用:Redis 延时队列的实现相对简单,不需要像 RabbitMQ 那样复杂的配置和管理。对于一些简单的业务场景,使用 Redis 延时队列可以快速实现功能。
- 性能高:Redis 是基于内存的数据库,读写速度非常快。在处理大量的延时任务时,Redis 延时队列可以提供更高的性能。
- 缺点
- 功能有限:相比 RabbitMQ,Redis 延时队列的功能相对较少。例如,RabbitMQ 支持多种消息模式(如发布 - 订阅、路由等),而 Redis 延时队列主要用于处理延时任务。
- 可靠性低:Redis 没有像 RabbitMQ 那样完善的消息确认机制和持久化策略。如果 Redis 出现故障,可能会导致部分消息丢失。
- 应用场景
- Redis 延时队列:适用于对性能要求较高、业务逻辑相对简单的延时任务场景,如订单超时未支付、缓存过期等。
- RabbitMQ:适用于对消息可靠性要求较高、业务逻辑复杂的场景,如分布式系统中的消息传递、异步任务处理等。
3.2 与 Kafka 对比
- 优点
- 低延迟:Redis 延时队列的响应速度非常快,可以在短时间内处理大量的延时任务。而 Kafka 主要用于高吞吐量的消息处理,在处理延时任务时可能会有一定的延迟。
- 易于集成:Redis 可以很方便地与各种编程语言和框架集成,对于开发者来说更加友好。
- 缺点
- 吞吐量低:Kafka 具有高吞吐量的特点,可以处理海量的消息。而 Redis 延时队列在处理大规模数据时,吞吐量相对较低。
- 数据持久化弱:Kafka 支持数据的持久化存储,即使服务器重启也不会丢失数据。而 Redis 的数据持久化策略相对较弱,可能会导致数据丢失。
- 应用场景
- Redis 延时队列:适用于对延迟要求较高、数据量较小的延时任务场景。
- Kafka:适用于大数据处理、日志收集等需要高吞吐量的场景。
3.3 为什么订单超时未报到使用延时队列
在订单超时未报到的场景中,我们需要在订单支付成功后,在预约结束时间到达时自动取消订单。这个场景对延迟要求较高,需要在指定的时间点准确地执行任务。Redis 延时队列可以很好地满足这个需求,它可以在指定的时间后将订单信息从队列中取出,然后进行相应的处理。而且,这个业务场景相对简单,不需要像 MQ 或 Kafka 那样复杂的功能,因此使用 Redis 延时队列更加合适。
四、订单超时未报到业务代码分享
4.1 Redis 延时队列工具类
import cn.hutool.core.collection.ConcurrentHashSet;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBlockingDeque;
import org.redisson.api.RDelayedQueue;
import org.redisson.api.RedissonClient;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit; /** * @author * description: reids 延迟队列工具类 */
@Slf4j
@Component
public class RedisDelayQueueUtil<T> implements ApplicationContextAware { public static RedissonClient redissonClient; private T obj; public static Set<String> queueCodeSet = new ConcurrentHashSet<>(); //release发布后开启 @Scheduled(cron = "0 */10 * * * ?") private void keepAlive() { queueCodeSet.forEach((code) -> { //云redis会主动断掉长期未使用的链接,主动激活 addDelayQueue("keepAlive", 1, TimeUnit.SECONDS, code); }); } /** * 添加延迟队列 * * @param value 队列值 * @param delay 延迟时间 * @param timeUnit 时间单位 * @param queueCode 队列键 * @param <T> */ public static <T> void addDelayQueue(T value, long delay, TimeUnit timeUnit, String queueCode) { try { RBlockingDeque<Object> blockingDeque = redissonClient.getBlockingDeque(queueCode); RDelayedQueue<Object> delayedQueue = redissonClient.getDelayedQueue(blockingDeque); delayedQueue.offer(value, delay, timeUnit); log.debug("Redisson 添加延时队列成功 队列键:{},队列值:{},延迟时间:{}", queueCode, value, timeUnit.toSeconds(delay) + "秒"); } catch (Exception e) { log.error("Redisson 添加延时队列失败 {}", e.getMessage()); throw new RuntimeException("Redisson添加延时队列失败"); } } /** * 获取延迟队列 - 会阻塞 * * @param queueCode 队列名称 * @return <T> 数据 * @throws InterruptedException */ public static <T> Optional<T> getDelayQueue(String queueCode) throws InterruptedException { queueCodeSet.add(queueCode); RBlockingDeque<T> blockingDeque = redissonClient.getBlockingDeque(queueCode); try { T t = blockingDeque.take(); if(Objects.equals("keepAlive",t)){ return Optional.empty(); } return Optional.of(t); } catch (Exception e) { return Optional.empty(); } } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { RedisDelayQueueUtil.redissonClient = applicationContext.getBean(RedissonClient.class); }
}
4.2 订单支付成功后入延时队列
ps:只是部分代码,主要展示延时队列的添加
@Slf4j
@Service
public class WechatPayCallbackServiceImpl implements WechatPayCallbackService { @Resource private HisFeign hisFeign; @Resource private HisV2Feign hisV2Feign; @Resource private OrderService orderService; @Resource private UserInfoService userInfoService; @Resource private TenantConfigApi tenantConfigApi; @Resource private MedicalRegOrderApi medicalRegOrderApi; @Resource private OrderAddressService orderAddressService; @Resource private OrderPrescriptionService orderPrescriptionService; //订单超时未报到:支付成功后入延时队列 private final String ORDER_TIMEOUT_WITHOUT_REPORTING = "delayQueue:OrderTimeoutWithoutReportingDelayQueue"; @Override public ProcessDto businessProcess(ProcessParam processParam) { ProcessDto processDto = new ProcessDto(); Order order = orderService.getOne(Wrappers.<Order>lambdaQuery().eq(Order::getOrderSeq, processParam.getBusiTransactionNumber())); if (ObjectUtil.isEmpty(order)) { processDto.setBusiLogicSuccessful(YesNo.NO); processDto.setErrorMsg(" 订单不存在"); return processDto; } //如果微信回调我们的状态不是成功,订单记录异常 if (processParam.getPaymentSuccessful().equals(YesNo.NO)) { order.setExceptionFlag(true); order.setExceptionDesc(" 订单微信回调的支付状态未成功"); order.setOrderStatus(5); order.setCancelReason(CancelType.EXCEPTION_REFUND.getDesc()); orderService.updateById(order); //TODO 异常退费逻辑 processDto.setBusiLogicSuccessful(YesNo.NO); processDto.setErrorMsg(" 微信回调的支付状态未成功"); return processDto; } //获取租户token TenantUserContextVO contextVO = userInfoService.getTenantToken(order.getHospitalId(), order.getPatientId(), order.getUserId()); switch (order.getOrderType()) { case 1: //挂号订单 log.info(" 挂号订单支付"); //如果微信支付回调的状态是成功,那么需要将成功的订单,入到超时未报到延时队列中,供超时未报到业务使用 createOrderCheckTask(order); return registeredOrder(processParam, order, contextVO); case 2: //处方订单 log.info(" 处方订单支付"); return prescriptionOrder(processParam, order, contextVO); default: log.error(" 订单类型错误"); processDto.setBusiLogicSuccessful(YesNo.NO); processDto.setErrorMsg(" 订单类型错误"); return processDto; } } // 超时未报到订单检查延时任务 private void createOrderCheckTask(Order order) { try { log.info(" 开始创建超时未报到订单检查延时任务,订单信息为:{}", JSONUtil.toJsonPrettyStr(order)); Duration duration; //todo 方案一:这里可以根据预约结束时间或者最晚报到时间,作为结束时间。如果超过结束时间就取消订单。【暂时使用方案一】 // 方案二:这里可以根据时间段,比如上午(12:00:00)、下午(17:00:00)、晚上(20:00:00)。全天的话,可以根据全天的结束时间为标准。 LocalDateTime prebookStartTime = order.getPrebookStartTime(); //预约结束时间 LocalDateTime prebookEndTime = order.getPrebookEndTime(); //最晚报到时间 LocalDateTime registerEndDate = order.getRegisterEndDate(); if (Objects.nonNull(prebookEndTime)) { duration = Duration.between(LocalDateTime.now(), prebookEndTime); } else if (Objects.nonNull(registerEndDate)) { duration = Duration.between(LocalDateTime.now(), registerEndDate); } else { duration = Duration.between(prebookStartTime, LocalDateTime.now().plusMinutes(30)); } if (duration.getSeconds() < 10) { //避免时间过短出现问题 duration = Duration.ofSeconds(10L); } log.info(" 创建超时未报到订单检查延时任务,orderId为{},检查时间为{}", order.getId(), LocalDateTime.now().plusSeconds(duration.getSeconds())); RedisDelayQueueUtil.addDelayQueue(order.getId(), duration.getSeconds(), TimeUnit.SECONDS, ORDER_TIMEOUT_WITHOUT_REPORTING); } catch (Exception e) { log.error(" 超时未报到订单检查延时任务创建异常:" + e); GlobalException.throwEx(" 超时未报到订单检查延时任务创建异常"); } }
}
4.3 订单超时业务处理
ps:这里用了线程池,但是搞复杂了,其实可以直接用定时去消费延时队列
/*** 订单重构超时场景校验处理*/
@Component
@Slf4j
public class OrderRefactorPayCheckSchedule implements CommandLineRunner {//订单超时未支付:创建订单的时候入延时队列,和之前共用一个:超过支付限制时间自动取消private final String ORDER_PAY_CHECK_DELAY_QUEUE = "delayQueue:OrderPayCheckDelayQueue";//订单超时未报到:支付成功后入延时队列:超过预约结束时间后,自动取消private final String ORDER_TIMEOUT_WITHOUT_REPORTING = "delayQueue:OrderTimeoutWithoutReportingDelayQueue";//订单超时未接诊:报道后入延时队列:24小时后未接诊自动取消private final String ORDER_TIMEOUT_NO_APPOINTMENT_RECEIVED = "delayQueue:OrderTimeoutNoAppointmentReceivedDelayQueue";//订单自动结束问诊:医生接诊时入延时队列:24小时未结束问诊自动取消private final String ORDER_CONSULTATION_AUTOMATIC_END = "delayQueue:OrderConsultationAutomaticEndDelayQueue";//处方超时未支付 1小时后自动取消private final String PRESCRIPTION_PAY_CHECK_DELAY_QUEUE = "delayQueue:PrescriptionPayCheckDelayQueue";//医生停诊private final String DOCTOR_SUSPEND_DELAY_QUEUE = "delayQueue:DoctorSuspendDelayQueue";// 订单自动结束问诊:24小时未结束@Value("${order.timeout.automatic_end_hours:24}")private Double orderConsultationAutomaticEndHours;@Resourceprivate OrderService orderService;@Resourceprivate SessionApi sessionApi;@Resourceprivate SessionRuntimeApi sessionRuntimeApi;@Resourceprivate ConsultationRecordService consultationRecordService;@Resourceprivate StringRedisTemplate stringRedisTemplate;@Resourceprivate ConsultationRecordApi consultationRecordApi;@Resourceprivate UserApi userApi;@Resourceprivate ShortMessageApi shortMessageApi;@Resourceprivate SuspendService suspendService;@Resourceprivate DoctorConfigApi doctorConfigApi;// 在类变量区新增线程池配置@Value("${schedule.pool.coreSize:5}")private int corePoolSize;@Value("${schedule.pool.maxSize:10}")private int maxPoolSize;@Value("${schedule.pool.queueCapacity:100}")private int queueCapacity;// 新增线程池bean(放在类变量声明之后)private ThreadPoolTaskExecutor taskExecutor;@PostConstructprivate void initThreadPool() {taskExecutor = new ThreadPoolTaskExecutor();taskExecutor.setCorePoolSize(corePoolSize);taskExecutor.setMaxPoolSize(maxPoolSize);taskExecutor.setQueueCapacity(queueCapacity);taskExecutor.setThreadNamePrefix("OrderSchedule-");taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());taskExecutor.initialize();}/*** 开启线程接受阻塞队列内的待同步订单进行同步*/@Overridepublic void run(String... args) {// 为每个队列启动一个独立的线程(超时未支付业务,OrderPayCheckSchedule中已经有了,这里可以注掉)//taskExecutor.execute(() -> wrapTask(this::handleOrderPayCheck));// 使用线程池提交任务taskExecutor.execute(() -> wrapTask(this::handleOrderTimeoutWithoutReporting));taskExecutor.execute(() -> wrapTask(this::handleOrderTimeoutNoAppointmentReceived));taskExecutor.execute(() -> wrapTask(this::handleOrderConsultationAutomaticEnd));taskExecutor.execute(() -> wrapTask(this::handlePrescriptionPayCheckDelayQueue));taskExecutor.execute(() -> wrapTask(this::handleDoctorSuspendDelayQueue));}// 新增任务包装方法private void wrapTask(Runnable task) {while (!Thread.currentThread().isInterrupted()) {try {task.run();} catch (Throwable e) {log.error("定时任务执行异常", e);try {TimeUnit.SECONDS.sleep(5); // 异常后暂停5秒} catch (InterruptedException ex) {Thread.currentThread().interrupt();}}}}/*** 订单超时未支付业务处理*/private void handleOrderPayCheck() {Optional<Long> optional = null;try {optional = RedisDelayQueueUtil.getDelayQueue(ORDER_PAY_CHECK_DELAY_QUEUE);} catch (InterruptedException e) {GlobalException.throwEx("获取延时队列异常:" + e);}log.info("获取延时队列成功,延时队列为:" + optional);if (optional.isPresent()) {Long orderId = optional.get();log.info("开始检查支付状态,订单id:{}", orderId);//调用订单超时未支付逻辑处理//todo 延时任务没有traceId,不方便排查,这里手动设置traceIdMap<String, String> contextMap = new HashMap<>();contextMap.put(Constants.TRACE_ID, "orderId_" + orderId);MDC.setContextMap(contextMap);checkOrderStatus(orderId, CancelTypeEnum.TIMEOUT.getCode());}}/*** 医生停诊业务处理*/private void handleDoctorSuspendDelayQueue() {Optional<DoctorSuspendEvent> optional = null;try {optional = RedisDelayQueueUtil.getDelayQueue(DOCTOR_SUSPEND_DELAY_QUEUE);} catch (InterruptedException e) {GlobalException.throwEx("获取延时队列异常:" + e);}log.info("获取延时队列成功,延时队列为:" + optional);if (optional.isPresent()) {DoctorSuspendEvent doctorSuspendEvent = optional.get();log.info("开始处理停诊业务,医生停诊信息为:{}", doctorSuspendEvent);doctorSuspend(doctorSuspendEvent);}}/*** 订单超时未报到业务处理*/private void handleOrderTimeoutWithoutReporting() {Optional<Long> optional = null;try {optional = RedisDelayQueueUtil.getDelayQueue(ORDER_TIMEOUT_WITHOUT_REPORTING);} catch (InterruptedException e) {GlobalException.throwEx("获取延时队列异常:" + e);}log.info("获取延时队列成功,延时队列为:" + optional);if (optional.isPresent()) {Long orderId = optional.get();log.info("处理超时未报到逻辑,订单id:{}", orderId);//todo 延时任务没有traceId,不方便排查,这里手动设置traceIdMap<String, String> contextMap = new HashMap<>();contextMap.put(Constants.TRACE_ID, "orderId_" + orderId);MDC.setContextMap(contextMap);// 调用超时未报到处理逻辑checkOrderStatus(orderId, CancelTypeEnum.UN_REPORT.getCode());}}/*** 订单超时未接诊业务处理*/private void handleOrderTimeoutNoAppointmentReceived() {Optional<Long> optional = null;try {optional = RedisDelayQueueUtil.getDelayQueue(ORDER_TIMEOUT_NO_APPOINTMENT_RECEIVED);} catch (InterruptedException e) {GlobalException.throwEx("获取延时队列异常:" + e);}log.info("获取延时队列成功,延时队列为:" + optional);if (optional.isPresent()) {Long orderId = optional.get();log.info("处理超时未接诊逻辑,订单id:{}", orderId);//todo 延时任务没有traceId,不方便排查,这里手动设置traceIdMap<String, String> contextMap = new HashMap<>();contextMap.put(Constants.TRACE_ID, "orderId_" + orderId);MDC.setContextMap(contextMap);// 调用超时未接诊处理逻辑checkOrderStatus(orderId, CancelTypeEnum.OVERTIME.getCode());}}/*** 订单自动结束问诊业务处理*/private void handleOrderConsultationAutomaticEnd() {Optional<Long> optional = null;try {optional = RedisDelayQueueUtil.getDelayQueue(ORDER_CONSULTATION_AUTOMATIC_END);} catch (InterruptedException e) {GlobalException.throwEx("获取延时队列异常:" + e);}log.info("获取延时队列成功,延时队列为:" + optional);if (optional.isPresent()) {Long orderId = optional.get();log.info("处理自动结束问诊逻辑,订单id:{}", orderId);//todo 延时任务没有traceId,不方便排查,这里手动设置traceIdMap<String, String> contextMap = new HashMap<>();contextMap.put(Constants.TRACE_ID, "orderId_" + orderId);MDC.setContextMap(contextMap);// 调用自动结束问诊处理逻辑checkOrderStatus(orderId, CancelTypeEnum.AUTO_END.getCode());}}private void handlePrescriptionPayCheckDelayQueue() {Optional<Long> optional = null;try {optional = RedisDelayQueueUtil.getDelayQueue(PRESCRIPTION_PAY_CHECK_DELAY_QUEUE);} catch (InterruptedException e) {GlobalException.throwEx("获取延时队列异常:" + e);}log.info("获取延时队列成功,延时队列为:" + optional);if (optional.isPresent()) {Long orderId = optional.get();log.info("处理处方结束问诊逻辑,订单id:{}", orderId);//todo 延时任务没有traceId,不方便排查,这里手动设置traceIdMap<String, String> contextMap = new HashMap<>();contextMap.put(Constants.TRACE_ID, "orderId_" + orderId);MDC.setContextMap(contextMap);// 调用自动结束问诊处理逻辑checkOrderStatus(orderId, null);}}/*** 订单业务处理** @param orderId*/public void checkOrderStatus(Long orderId, Integer code) {log.info("开始检查订单状态,订单id:{}", orderId);Order order = orderService.lambdaQuery().eq(Order::getId, orderId).one();if (ObjectUtil.isEmpty(order)) {GlobalException.throwEx("获取订单信息为空 订单id:" + orderId);}CancelOrderDTO cancelOrderDTO = new CancelOrderDTO();cancelOrderDTO.setOrderId(order.getId());cancelOrderDTO.setOrderSeq(order.getOrderSeq());cancelOrderDTO.setSendShortMessage(true);switch (Objects.requireNonNull(OrderType.of(order.getOrderType()))) {case VISIT -> {if (Objects.equals(code, CancelTypeEnum.TIMEOUT.getCode()) && Objects.equals(PayStatus.UN_PAY.getCode(), order.getPayStatus()) && !Objects.equals(ConsultationRecordStatus.CANCEL.getCode(), order.getOrderStatus())) {log.info("超时未支付,订单取消,订单为{}", order);} else if (Objects.equals(code, CancelTypeEnum.UN_REPORT.getCode()) && Objects.equals(PayStatus.PAID.getCode(), order.getPayStatus()) && Objects.equals(ConsultationRecordStatus.WAITFOR_CHECKIN.getCode(), order.getOrderStatus())) {log.info("超时未报到,订单取消,订单为{}", order);} else if (Objects.equals(code, CancelTypeEnum.OVERTIME.getCode()) && Objects.equals(PayStatus.PAID.getCode(), order.getPayStatus()) && Objects.equals(ConsultationRecordStatus.WAITFOR_INQUIRING.getCode(), order.getOrderStatus())) {log.info("超时未接诊,订单取消,订单为{}", order);} else if (Objects.equals(code, CancelTypeEnum.AUTO_END.getCode()) && Objects.equals(PayStatus.PAID.getCode(), order.getPayStatus()) && Objects.equals(ConsultationRecordStatus.INQUIRING.getCode(), order.getOrderStatus())) {log.info("自动结束问诊,订单为{}", order);//判断当前时间是否小于问诊结束时间,如果小于就说明延长问诊了,不执行后续业务//获取im会话信息//im诊室关闭时间// 获取当前时间//自动结诊:(1)更改订单与问诊订单状态,为已结束//关闭im诊室//短信通知//获取当前医生结诊时常配置(默认24小时)} else {//其他场景待补充log.info("问诊订单其他场景:订单信息:" + order);}}case PRESCRIPTION -> {// 校验订单状态log.info("处方订单开始校验是否支付", JSONUtil.toJsonPrettyStr(order));if (Objects.equals(RpStatus.UN_PAY.getCode(), order.getOrderStatus()) || Objects.equals(RpStatus.EXTERNAL_RP.getCode(), order.getOrderStatus())) {// 作废处方订单orderService.invalidPrescriptionByOrderId(orderId);}}}}
}
ps:后续会有超时场景的应用补充说明:Redis延时队列在订单超时未报到场景的应用补充说明-CSDN博客
相关文章:
Redis延时队列在订单超时未报到场景的应用分享
一、引言 在电商、医疗预约等众多业务场景中,经常会遇到需要处理超时任务的情况。比如医疗预约订单,如果患者在支付成功后,到了预约结束时间还未报到,系统需要自动取消订单。为了实现这样的功能,我们可以利用 Redis 延…...
vue前端代码作业——待办事项
美化样式示意图: 后端IDEA代码示意图: 代码解释: 1. isAllChecked 计算属性的作用 isAllChecked 用于实现 “全选 / 全不选” 功能,它是一个 双向绑定 的计算属性(因为 v-model 需要同时支持读取和设置值)…...
docker镜像拉取失败
hub.docker.com中提供的docker pull命令在服务器拉取镜像时报错Error response from daemon: Get https://registry-1.docker.io/v2/: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers) 这个错误通常表明Docker客户…...
Ruby 简介
Ruby 简介 引言 Ruby 是一种广泛使用的动态、开源的编程语言,自 1995 年由日本程序员 Yukihiro Matsumoto(通称 Matz)设计以来,它以其优雅的语法、强大的库支持和跨平台特性赢得了全球开发者的青睐。本文将详细介绍 Ruby 的起源、特点、应用领域以及它在现代软件开发中的…...
解决 FFmpeg 使用 C/C++ 接口时,解码没有 shell 快的问题(使用多线程)
一、问题 硬件设备为香橙派 5Plus,最近需要使用硬件视频解码来加速 YOLO 的检测,shell 窗口的FFmpeg已经调通,详见文章: 编译支持 RKmpp 和 RGA 的 ffmpeg 源码_rk3588 ffmpeg mpp-CSDN博客https://blog.csdn.net/plmm__/article…...
sqlalchemy:将mysql切换到OpenGauss
说明 之前python的项目使用的mysql,近期要切换到国产数据库OpenGauss。 之前的方案是fastapisqlalchemy,测试下来发现不用改代码,只要改下配置即可。 切换方案 安装openGauss-connector-python-psycopg2 其代码工程在:https:…...
缓存使用纪要
一、本地缓存:Caffeine 1、简介 Caffeine是一种高性能、高命中率、内存占用低的本地缓存库,简单来说它是 Guava Cache 的优化加强版,是当下最流行、最佳(最优)缓存框架。 Spring5 即将放弃掉 Guava Cache 作为缓存机…...
Qt之Service开发
一、概述 基于Qt的用于开发系统服务(守护进程)和后台服务,有以下几个优秀的开源 QtService 框架和库。 1. QtService (官方解决方案) GitHub: https://github.com/qtproject/qt-solutions/tree/master/qtservice 特点: 官方提供的服务框架 支持 Windows 服务和 Linux 守护…...
ssm框架之Spring
Spring框架介绍 Spring框架是一个轻量级的企业级应用框架 通过它可以贯穿表现层、业务层、持久层。集成方便,简单易用,具有如下特点: Spring框架特色 Spring设计理念 是面向Bean的编程 Spring两大核心技术 控制反转(IoC:Inver…...
Flutter 开发环境配置--宇宙级教学!
目录 一、安装环境(Windows)二、Android 创建Flutter项目三、VSCode 搭建环境四、补充 一、安装环境(Windows) Flutter SDK 下载 推荐使用中国镜像站点下载 Flutter SDK,速度更快:中国环境 或者从官网下载…...
音视频 YUV格式详解
前言 本文介绍YUV色彩模型,YUV的分类和常见格式。 RGB色彩模型 在RGB颜色空间中,任意色光F都可以使用R、G、B三色不同的分量混合相加而成即: F = R + G + B.。即我们熟悉的三原色模型。 RGB色彩空间根据每个分量在计算机中占用的存储字节数可以分为以下几种类型,字节数…...
力扣 第 153 场双周赛 讲题
文章目录 Q1.字符串的反转度Q2.操作后最大活跃区段数I3500.将数组分割为子数组的最小代价 Q1.字符串的反转度 签到题,直接建立一个映射表即可 class Solution:def reverseDegree(self, s: str) -> int:# 先建立映射表ss "abcdefghijklmnopqrstuvwxyz"store {}i…...
grafana 配置页面告警
添加告警规则 1.登录grafana 点击 Alerting > Alert rules 点击 New alert rule 2.填写告警规则名字 3.配置告警规则 选择数据源为 Loki 单机 Builder 单机Label brower 单机 node_name 标签,选择一个主机,选好后单机 Show logs 这时候查询语…...
Cent OS7+Docker+Dify
由于我之前安装了Dify v1.0.0,出现了一些问题:无法删除,包括:知识库中的文件、应用、智能体、工作流,都无法删除。现在把服务器初始化,一步步重新安装,从0到有。 目录 1、服务器重装系统和配置…...
【自学笔记】PHP语言基础知识点总览-持续更新
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 1. PHP 简介2. PHP 环境搭建3. 基本语法变量与常量数据类型运算符 4. 控制结构条件语句循环语句 5. 函数函数定义与调用作用域 6. 数组7. 字符串8. 表单处理9. 会话…...
Android Gradle 下载插件或依赖太慢
问题与处理策略 问题描述 Android 项目中,settings.gradle 文件中,有如下配置,Gradle 插件或依赖下载速度慢 pluginManagement {repositories {gradlePluginPortal()google()mavenCentral()} }dependencyResolutionManagement {repositori…...
python-59-基于python内置库解析html获取标签关键信息
文章目录 1 html.parser1.1 初始化和基础使用1.1.1 handle_starttag(self, tag, attrs)1.1.2 handle_endtag(self, tag)1.1.3 handle_startendtag(self, tag, attrs)1.1.4 handle_data(self, data)1.1.5 handle_comment(self, data)1.2 解析HTML文档的流程2 百度搜索关键词链接…...
elementplus的el-tabs路由式
在使用 Element Plus 的 el-tabs 组件,实现路由式的切换(即点击标签页来切换不同的路由页面)。下面是一个基于 Vue 3 和 Element Plus 实现路由式 el-tabs 的基本步骤和示例。 步骤 1: 安装必要的库 在vue3项目安装 Vue Router 和 Element …...
ArcGIS地理信息系统空间分析实验教程学习
ArcGIS 作为地理信息系统领域的经典软件,以其强大的功能和广泛的应用场景,成为了众多学者、研究人员和专业人士的首选工具。它不仅可以高效地处理和可视化地理空间数据,还能通过复杂的空间分析模型,揭示地理现象背后的规律和趋势。…...
mac部署CAT监控服务
在 Mac 上部署美团点评开源的 CAT 监控服务端,可以按照以下步骤操作: 1. 环境准备 1.1 安装依赖 确保已安装以下工具: JDK 8(建议 OpenJDK 11) MySQL 5.7(存储监控数据)(8.0不支持…...
鸿蒙OS 5 架构设计探秘:从分层设计到多端部署
文章目录 鸿蒙OS架构设计探秘:从分层设计到多端部署一、鸿蒙的分层架构设计二、模块化设计的精髓三、智慧分发设计:资源的动态调度四、一次开发,多端部署的实践总结与思考 鸿蒙OS架构设计探秘:从分层设计到多端部署 最近两年来&a…...
深入解析:ElasticSearch Query 查询方式
全文目录: 开篇语前言摘要概述ElasticSearch Query 查询方式详解1. Match 查询(全文搜索)1.1 Match 查询示例1.2 Match 查询参数扩展 2. Term 查询(精准查询)2.1 Term 查询示例2.2 Terms 查询 3. Bool 查询(…...
HTML5贪吃蛇游戏开发经验分享
HTML5贪吃蛇游戏开发经验分享 这里写目录标题 HTML5贪吃蛇游戏开发经验分享项目介绍技术栈核心功能实现1. 游戏初始化2. 蛇的移动控制3. 碰撞检测4. 食物生成 开发心得项目收获后续优化方向结语 项目介绍 在这个项目中,我使用HTML5 Canvas和原生JavaScript实现了一…...
桥接模式_结构型_GOF23
桥接模式 桥接模式(Bridge Pattern)是一种结构型设计模式,核心思想是将抽象与实现分离,使两者能独立变化。它像一座连接两岸的桥梁,让“抽象层”和“实现层”自由组合,避免因多维度变化导致的“类爆炸”问…...
卡尔曼滤波入门(二)
核心思想 卡尔曼滤波的核心就是在不确定中寻找最优,那么怎么定义最优呢?答案是均方误差最小的,便是最优。 卡尔曼滤波本质上是一种动态系统状态估计器,它回答了这样一个问题: 如何从充满噪声的观测数据中,…...
有关pip与conda的介绍
Conda vs. Pip vs. Virtualenv 命令对比 任务Conda 命令Pip 命令Virtualenv 命令安装包conda install $PACKAGE_NAMEpip install $PACKAGE_NAMEX更新包conda update --name $ENVIRONMENT_NAME $PACKAGE_NAMEpip install --upgrade $PACKAGE_NAMEX更新包管理器conda update con…...
【Portainer】Docker可视化组件安装
Portainer Portainer 是用于管理容器化环境的一体化平台工程解决方案,提供广泛的定制功能,以满足个人开发人员和企业团队的需求。 官方地址: https://www.portainer.io/ 安装 在 WSL / Docker Desktop 上使用 Docker 安装 Portainer CE 通过命令或UI页…...
基于深度神经网络的图像防篡改检测方法研究
标题:基于深度神经网络的图像防篡改检测方法研究 内容:1.摘要 随着数字化时代的发展,图像篡改现象日益普遍,严重影响了图像信息的真实性和可靠性。本文旨在研究基于深度神经网络的图像防篡改检测方法,以有效识别被篡改的图像。通过收集大量真…...
MATLAB导入Excel数据
假如Excel中存在三列数据需要导入Matlab中。 保证该Excel文件与Matlab程序在同一目录下。 function [time, voltage, current] test(filename)% 读取Excel文件并提取时间、电压、电流数据% 输入参数:% filename: Excel文件名(需包含路径,如C:\data\…...
华为GaussDB数据库的手动备份与还原操作介绍
数据库的备份以A机上的操作为例。 1、使用linux的root用户登录到GaussDB服务器。 2、用以下命令切换到 GaussDB 管理员用户,其中,omm 为当前数据库的linux账号。 su - omm 3、执行gs_dump命令进行数据库备份: 这里使用gs_dump命令进行备…...
MySQL数据库BUG导致查询不到本该查到的数据
在数据库的日常使用中,我们常常会遇到一些看似匪夷所思的查询问。最近就看到一个因为MySQL BUG导致无法查到本该查询到数据的案例。 1. 问题背 数据库版本:MySQL8.0.40 假设我们创建了一个名为 product_info 的表,用于存储产品的相关信息。该…...
Dubbo(25)如何配置Dubbo的协议和端口?
配置Dubbo的协议和端口是设置分布式服务通信的基础步骤。Dubbo支持多种协议(如Dubbo、RMI、HTTP等),你可以根据需求选择合适的协议并配置相应的端口。下面以一个完整的Spring Boot项目为例,详细介绍如何配置Dubbo的协议和端口。 …...
服务器磁盘卷组缓存cache设置介绍
工具1: storcli a. 确认软件包是否安装 [rootlocalhost ~]#rpm -qa | grep storcli storcli-1.21.06-1.noarch 备注:若检索结果为空,需要安装对应的软件安装包。安装命令如下: #rpm -ivh storcli-xx-xx-1.noarch.rpm b. 查看逻辑…...
StarVector:开启多模态SVG生成的新纪元——开源AI模型的革新之作
在AI技术蓬勃发展的今天,图像生成模型已不再局限于像素级的输出。StarVector作为一款开源的多模态SVG生成模型,凭借其独特的代码与视觉融合能力,正在重新定义矢量图形的创作方式。它不仅让图像生成更灵活、更轻量化,还为设计师、开…...
MySQL日期时间函数
函数分类 函数名 功能描述 语法示例 获取当前日期和时间 NOW() 返回包含年、月、日、时、分、秒的完整时间戳,格式为 YYYY-MM-DD HH:MM:SS SELECT NOW(); CURDATE() / CURRENT_DATE() 获取当前日期,格式为 YYYY-MM-DD SELECT CURDATE(); 或 SE…...
WinSCP使用教程:(SFTP、SCP、FTP 和 WebDAV)
WinSCP 是一款免费开源的 Windows 环境下的 SFTP、SCP、FTP 和 WebDAV 客户端,主要用于在本地计算机与远程服务器之间安全地传输文件,并提供基本的文件管理功能。 WinSCP是Windows环境下使用SSH的开源图形化的SFTP的客户端 SSH 的全称是 Secure Shell&…...
备份是个好习惯
##解题思路 首先看到题目说备份是个好习惯,说明可能存在备份文件泄露 用dirsearch或者其他的目录扫描工具扫一扫,发现两个网址状态码正常,其中一个刚好是.bak的备份文件 至于flag文件,无法读取源码,都是空的 下载备份…...
centos 7 LVM管理命令
物理卷(PV)管理命令 pvcreate:用于将物理磁盘分区或整个磁盘创建为物理卷。 示例:sudo pvcreate /dev/sdb1 解释:将 /dev/sdb1 分区创建为物理卷。 pvdisplay:显示物理卷的详细信息,如大小、所属…...
使用 Spring Boot 3.2 集成 MinIO 8.5:实现高效对象存储
摘要 MinIO 是一款高性能的分布式对象存储服务,与云原生应用完美契合。本文将手把手教你如何在 Spring Boot 3.2 项目中集成 MinIO 8.5 版本,实现文件上传、下载和删除等核心功能,并提供完整代码示例和常见问题解决方案。 一、环境准备 JDK …...
【Qt】数据库管理
数据库查询工具开发学习笔记 一、项目背景与目标 背景:频繁编写数据库查询语句,希望通过工具简化操作,提升效率。 二、总体设计思路 1. 架构设计 MVC模式:通过Qt控件实现视图(UI),业务逻辑…...
C#:Time.deltaTime
目录 第一性原理:从最基本的问题开始 什么是Time.deltaTime? 1. 什么是“帧”? 2. 什么是“帧率”? 为什么需要它? 一个生活化的例子 更通俗的类比 在Unity中的特殊性 第一性原理:从最基本的问题开…...
鸿蒙富文本实践
01 鸿蒙中的文本展示-Text组件 Text 组件的普通用法和其他语言一样,可以直接使用字符串Text(我是一段文本) 通过点语法设置文本样式: Text(我是超长文本,超出的部分显示省略号。I am an extra long text, with ellipses displayed for any ex…...
【字符设备驱动开发–IMX6ULL】(二)Linux 设备号
【字符设备驱动开发–IMX6ULL】(二)Linux 设备号 文章目录 【字符设备驱动开发–IMX6ULL】(二)Linux 设备号1 设备号的组成2.设备号的分配 1 设备号的组成 为了方便管理,Linux 中每个设备都有一个设备号,设…...
Elasticsearch-实战案例
一、没有使用Elasticsearch的查询速度698ms 1.数据库模糊查询不走索引,在数据量较大的时候,查询性能很差。需要注意的是,数据库模糊查询随着表数据量的增多,查询性能的下降会非常明显,而搜索引擎的性能则不会随着数据增…...
电子文档安全管理系统V6.0接口backup存在任意文件下载漏洞
免责声明:本号提供的网络安全信息仅供参考,不构成专业建议。作者不对任何由于使用本文信息而导致的直接或间接损害承担责任。如涉及侵权,请及时与我联系,我将尽快处理并删除相关内容。 漏洞描述 电子文档安全管理系统 V6.0 reso…...
jmeter web压力测试 压测
下载地址 Apache JMeter - Download Apache JMeter 1. 设置线程组 2. 设置http请求头 3. 设置http请求体 4. 设置结果条目 常用函数 ${__RandomString(8, abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789)}${__javaScript( ${__Random(1000, 10000)} /…...
FPGA学习篇——Verilog学习之寄存器的实现
1 寄存器理论 这里在常见的寄存器种加了一个复位信号sys_rst_n。(_n后缀表示复位信号低电平有效,无这个后缀的则表示高电平有效) 这里规定在时钟的上升沿有效,只有当时钟的上升沿来临时,输出out 才会改变,…...
CXL UIO Direct P2P学习
前言: 在CXL协议中,UIO(Unordered Input/Output) 是一种支持设备间直接通信(Peer-to-Peer, P2P)的机制,旨在绕过主机CPU或内存的干预,降低延迟并提升效率。以下是UIO的核心概念及UI…...
一键实现:谷歌表单转word(formtoword)
一键将 Google Forms 转换为 Word,最简单的方法 有些繁琐的工作让人倍感挫败,明明 应该 可以自动化。你精心制作了一份 Google Forms,收集了数据,现在需要在 Word 文档中分享其结构或内容。于是,你只能手动复制粘贴问…...
QT第六课------QT界面优化------QSS
作者前言 🎂 ✨✨✨✨✨✨🍧🍧🍧🍧🍧🍧🍧🎂 🎂 作者介绍: 🎂🎂 🎂 🎉🎉🎉…...