当前位置: 首页 > news >正文

消息队列之关于如何实现延时队列

一、延时队列的应用

1.1 什么是延时队列?

顾名思义:首先它要具有队列的特性,再给它附加一个延迟消费队列消息的功能,也就是说可以指定队列中的消息在哪个时间点被消费。

延时队列在项目中的应用还是比较多的,尤其像电商类平台:

  1. 下订单成功后,在30分钟内没有支付,自动取消订单
  2. 外卖平台发送订餐通知,下单成功后60s给用户推送短信。
  3. 如果订单一直处于某一个未完结状态时,及时处理关单,并退还库存
  4. 淘宝新建商户一个月内还没上传商品信息,将冻结商铺等
  5. 用户登录之后5分钟给用户做分类推送;
  6. 用户多少天未登录给用户做召回推送;
  7. 关闭空闲连接。服务器中,有很多客户端的连接,空闲一段时间之后需要关闭之。
  8. 清理过期数据业务上。比如缓存中的对象,超过了空闲时间,需要从缓存中移出。
  9. 任务超时处理。在网络协议滑动窗口请求应答式交互时,处理超时未响应的请求。
  10. 新创建店铺,N天内没有上传商品,系统如何知道该信息,并发送激活短信?
1.2 延迟队列和定时任务的区别
  • 定时任务有明确的触发时间,延时任务没有
  • 定时任务有执行周期,而延时任务在某事件触发后一段时间内执行,没有执行周期
  • 定时任务一般执行的是批处理操作是多个任务,而延时任务一般是单个任务

业界目前也有很多实现方案,单机版的方案就不说了,现在也没有哪个公司还是单机版的服务,今天我们一一探讨各种方案的大致实现。

二、延时队列的实现

个人一直秉承的观点:工作上能用JDK自带API实现的功能,就不要轻易自己重复造轮子,或者引入三方中间件。一方面自己封装很容易出问题(大佬除外),再加上调试验证产生许多不必要的工作量;另一方面一旦接入三方的中间件就会让系统复杂度成倍的增加,维护成本也大大的增加。

2.1 定时轮询(quartz,数据库等)

通过一个线程定时的去扫描数据库,通过下订单时间来判断是否有超时的订单,然后进行update或delete等操作

对于未支付的订单,需要进行定期扫描,判断其产生时间有没有超过固定的时间,如果超过则取消订单。这样做很不好,因为:

  1. 扫描数据库,每次扫描整个表非常耗时,因为一直在阻塞,很耗费资源
  2. 所谓的定期扫描,扫描时间的粒度太小,就会造成资源浪费,扫描的粒度太大就会造成订单没有及时删除
(1)实现

基于主流的定时任务,定时扫描:

常见的定时任务主要有:quartz、xxl-job、spring系列的task、linux自带的 corntab和加工后的 JcronTab等

下面为方便起见,使用spring task实现主要思路

public class JobDelayQueueDemo {// 下单时间Date submitOrder = null;// 取消订单时间:下单30分钟后 检测如果未支付,则自动取消订单,生成订单60s后,给客户发短信Date executeTime = null;/*** Step2: 定时扫描数据库** 每隔五秒*/@Scheduled(cron = "0/5 * * * * ? ")public void process(){System.out.println("我是定时任务!\n 我来看一下延迟时间够了没有!!" );/*** 检测当前时间 是否是取消订单时间* 当前时间 - 执行时间 如果误差小于 2s则取消订单*/if (System.currentTimeMillis() - executeTime.getTime() < 2000) {System.out.println("取消订单逻辑处理");/*** 取消订单逻辑* 此处省略1000万行代码*/System.out.println("发送短信提醒客户,订单已取消");}}/*** Step1: 提交订单,存入数据库*/public void submitOrder(){/*** 下单时间,如果当前时间超过下单时间30分钟未支付 则发送短信提醒将自动取消订单*/submitOrder = DateTools.string2DateTime("2020-07-22 13:00:00");/***  取消订单时间:下单30分钟后 检测如果未支付,则自动取消订单,生成订单60s后,给客户发短信*/executeTime = DateUtils.addMinutes(submitOrder,30);/*** 存入数据库* 此处省略 100000万行代码*/}
}

下面为方便起见,使用spring quartz 实现主要思路:

public class QuartzDelayQueueDemo implements Job {// 下单时间Date submitOrder = null;// 取消订单时间:下单30分钟后 检测如果未支付,则自动取消订单,生成订单60s后,给客户发短信Date executeTime = null;public static void main(String[] args) throws SchedulerException {// 创建任务JobDetail jobDetail = JobBuilder.newJob(QuartzDelayQueueDemo.class).withIdentity("job1", "group1").build();// 创建触发器 每3秒钟执行一次Trigger trigger = TriggerBuilder.newTrigger().withIdentity("trigger1", "group3").withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(3).repeatForever()).build();Scheduler scheduler = new StdSchedulerFactory().getScheduler();// 将任务及其触发器放入调度器scheduler.scheduleJob(jobDetail, trigger);// 调度器开始调度任务scheduler.start();}@Overridepublic void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {System.out.println("我是定时任务!\n 我来看一下延迟时间够了没有!!");/*** 检测当前时间 是否是取消订单时间* 当前时间 - 执行时间 如果误差小于 2s则取消订单*/if (System.currentTimeMillis() - executeTime.getTime() < 2000) {System.out.println("取消订单逻辑处理");/*** 取消订单逻辑* 此处省略1000万行代码*/System.out.println("发送短信提醒客户,订单已取消");}}
}

源码地址: 快速访问

(2)优缺点

优点 :简单易行,支持集群操作
缺点

  • 对服务器内存消耗大
  • 存在延迟,比如你每隔3分钟扫描一次,那最坏的延迟时间就是3分钟
  • 假设你的订单有几千万条,每隔几分钟这样扫描一次,数据库损耗极大
  • 数据量过大时会消耗太多的IO资源,效率太低
2.2 DelayQueue 延时队列(JDK)

JDK 中提供了一组实现延迟队列的API,位于Java.util.concurrent包下DelayQueue

DelayQueue是一个BlockingQueue(无界阻塞)队列,它本质就是封装了一个PriorityQueue(优先队列),PriorityQueue内部使用完全二叉堆(不知道的自行了解哈)来实现队列元素排序,我们在向DelayQueue队列中添加元素时,会给元素一个Delay(延迟时间)作为排序条件,队列中最小的元素会优先放在队首。队列中的元素只有到了Delay时间才允许从队列中取出。队列中可以放基本数据类型或自定义实体类,在存放基本数据类型时,优先队列中元素默认升序排列,自定义实体类就需要我们根据类属性值比较计算了。

先简单实现一下看看效果,添加三个order入队DelayQueue,分别设置订单在当前时间的5秒10秒15秒后取消。
在这里插入图片描述
要实现DelayQueue延时队列,队中元素要implements Delayed 接口,这个接口里只有一个getDelay方法,用于设置延期时间。Order类中compareTo方法负责对队列中的元素进行排序。

其中:

  • poll():获取并移除队列的超时元素,没有则返回空
  • take():获取并移除队列的超时元素,如果没有则wait当前线程,直到有元素满足超时条件,返回结果。
(1)实现
  1. BlockingQueue+PriorityQueue(堆排序)+Delayed
  2. DelayQueue中存放的对象需要实现compareTo()方法和getDelay()方法
  3. getDelay方法返回该元素距离失效还剩余的时间,当<=0时元素就失效了,就可以从队列中获取到
public class DelayOrder implements Delayed {/*** 延迟时间*/@JsonFormat(locale = "zh", timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")private long time;/*** 订单号*/String orderNumber = null;public DelayOrder(String orderNumber, long time, TimeUnit unit) {this.orderNumber = orderNumber;this.time = System.currentTimeMillis() + (time > 0 ? unit.toMillis(time) : 0);}/*** 返回距离你自定义的超时时间还有多少* @param unit* @return*/@Overridepublic long getDelay(TimeUnit unit) {return time - System.currentTimeMillis();}/*** https://www.runoob.com/java/java-string-compareto.html* @param o* @return*/@Overridepublic int compareTo(Delayed o) {DelayOrder Order = (DelayOrder) o;long diff = this.time - Order.time;if (diff <= 0) {return -1;} else {return 1;}}void print() {System.out.println(orderNumber + "编号的订单准备删除,删除时间:"+ DateTools.getLong2YyyyMmDdHhMmSs(System.currentTimeMillis()));}}

DelayQueueput方法是线程安全的,因为put方法内部使用了ReentrantLock锁进行线程同步。

DelayQueue还提供了两种出队的方法 poll()take()

  • poll() 为非阻塞获取,没有到期的元素直接返回null;
  • take() 阻塞方式获取,没有到期的元素线程将会等待。
public class DelayQueueDemo {public static void main(String[] args) throws InterruptedException {DelayOrder orderNumber1 = new DelayOrder("orderNumber1", 5, TimeUnit.SECONDS);DelayOrder orderNumber2 = new DelayOrder("orderNumber2", 10, TimeUnit.SECONDS);DelayOrder orderNumber3 = new DelayOrder("orderNumber3", 15, TimeUnit.SECONDS);DelayQueue<DelayOrder> delayOrders = new DelayQueue<>();delayOrders.put(orderNumber1);delayOrders.put(orderNumber2);delayOrders.put(orderNumber3);System.out.println("订单延迟队列开始时间:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));while (delayOrders.size() != 0){/*** 取队列头部元素是否过去* poll():获取并移除队列的超时元素,没有则返回空* take():获取并移除队列的超时元素,如果没有则wait当前线程,直到有元素满足超时条件,返回结果。*/
//             DelayOrder task = delayOrders.poll();DelayOrder task1 = delayOrders.take();if (task1 != null) {System.out.format("订单:{%s}被取消, 取消时间:{%s}\n", task1.orderNumber, LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));}
//            Thread.sleep(1000);}}
}

上边只是简单的实现入队与出队的操作,实际开发中会有专门的线程,负责消息的入队与消费。

执行后看到结果如下,Order1Order2Order3 分别在 5秒10秒15秒后被执行,至此就用DelayQueue实现了延时队列。

订单延迟队列开始时间:2020-12-14 13:20:48
订单:{orderNumber1}被取消, 取消时间:{2020-12-14 13:20:53}
订单:{orderNumber2}被取消, 取消时间:{2020-12-14 13:20:58}
订单:{orderNumber3}被取消, 取消时间:{2020-12-14 13:21:03}
(2)优缺点

优点 :效率高,任务触发时间延迟低

缺点

  1. 服务器重启后,数据全部消失,怕宕机
  2. 集群扩展相当麻烦
  3. 因为内存条件限制的原因,比如下单未付款的订单数太多,那么很容易就出现OOM异常
  4. 代码复杂度较高,分布式需要额外实现(无法指定绝对的日期或时间)
(3)实现原理

在这里插入图片描述

内部结构

  • 可重入锁
  • 用于根据delay时间排序的优先级队列
  • 用于优化阻塞通知的线程元素leader
  • 用于实现阻塞和通知的Condition对象

delayed和PriorityQueue

在理解delayQueue原理之前我们需要先了解两个东西,delayed和PriorityQueue.

  • delayed是一个具有过期时间的元素
  • PriorityQueue是一个根据队列里元素某些属性排列先后的顺序队列

delayQueue其实就是在每次往优先级队列中添加元素,然后以元素的delay/过期值作为排序的因素,以此来达到先过期的元素会拍在队首,每次从队列里取出来都是最先要过期的元素

offer()方法

  1. 执行加锁操作
  2. 把元素添加到优先级队列中
  3. 查看元素是否为队首
  4. 如果是队首的话,设置leader为空,唤醒所有等待的队列
  5. 释放锁
    public boolean offer(E e) {final ReentrantLock lock = this.lock;lock.lock();try {q.offer(e);if (q.peek() == e) {leader = null;available.signal();}return true;} finally {lock.unlock();}}

take() 方法

  1. 执行加锁操作
  2. 取出优先级队列元素q的队首
  3. 如果元素q的队首/队列为空,阻塞请求
  4. 如果元素q的队首(first)不为空,获得这个元素的delay时间值
  5. 如果first的延迟delay时间值为0的话,说明该元素已经到了可以使用的时间,调用poll方法弹出该元素,跳出方法
  6. 如果first的延迟delay时间值不为0的话,释放元素first的引用,避免内存泄露
  7. 判断leader元素是否为空,不为空的话阻塞当前线程
  8. 如果leader元素为空的话,把当前线程赋值给leader元素,然后阻塞delay的时间,即等待队首到达可以出队的时间,在finally块中释放leader元素的引用
  9. 循环执行从1~8的步骤
  10. 如果leader为空并且优先级队列不为空的情况下(判断还有没有其他后续节点),调用signal通知其他的线程
  11. 执行解锁操作
    /*** Retrieves and removes the head of this queue, waiting if necessary* until an element with an expired delay is available on this queue.** @return the head of this queue* @throws InterruptedException {@inheritDoc}*/public E take() throws InterruptedException {final ReentrantLock lock = this.lock;lock.lockInterruptibly();try {for (;;) {E first = q.peek();if (first == null)available.await();else {long delay = first.getDelay(NANOSECONDS);if (delay <= 0)return q.poll();first = null; // don't retain ref while waitingif (leader != null)available.await();else {Thread thisThread = Thread.currentThread();leader = thisThread;try {available.awaitNanos(delay);} finally {if (leader == thisThread)leader = null;}}}}} finally {if (leader == null && q.peek() != null)available.signal();lock.unlock();}}

get点

整个代码的过程中并没有使用上太难理解的地方,但是有几个比较难以理解他为什么这么做的地方

leader元素的使用

大家可能看到在我们的DelayQueue中有一个Thread类型的元素leader,那么他是做什么的呢,有什么用呢?

让我们先看一下元素注解上的doc描述:

Thread designated to wait for the element at the head of the queue.
This variant of the Leader-Follower pattern serves to minimize unnecessary timed waiting.
when a thread becomes the leader, it waits only for the next delay to elapse, but other threads await indefinitely.
The leader thread must signal some other thread before returning from take() or poll(…), unless some other thread becomes leader in the interim.
Whenever the head of the queue is replaced with an element with an earlier expiration time, the leader field is invalidated by being reset to null, and some waiting thread, but not necessarily the current leader, is signalled.
So waiting threads must be prepared to acquire and lose leadership while waiting.

上面主要的意思就是说用leader来减少不必要的等待时间,那么这里我们的DelayQueue是怎么利用leader来做到这一点的呢:

这里我们想象着我们有个多个消费者线程用take方法去取,内部先加锁,然后每个线程都去peek第一个节点.

  • 如果leader不为空说明已经有线程在取了,设置当前线程等待
if (leader != null)available.await();
  • 如果为空说明没有其他线程去取这个节点,设置leader并等待delay延时到期,直到poll后结束循环
else {Thread thisThread = Thread.currentThread();leader = thisThread;try {available.awaitNanos(delay);} finally {if (leader == thisThread)leader = null;}
}

take方法中为什么释放first元素

first = null; // don't retain ref while waiting

我们可以看到doug lea后面写的注释,那么这段代码有什么用呢?

想想假设现在延迟队列里面有三个对象(take() 方法中)

  • 线程A进来获取first,然后进入 else 的else ,设置了leader为当前线程A
  • 线程B进来获取first,进入else的阻塞操作,然后无限期等待
  • 这时在JDK 1.7下面他是持有first引用的
  • 如果线程A阻塞完毕,获取对象成功,出队,这个对象理应被GC回收,但是他还被线程B持有着,GC链可达,所以不能回收这个first.
  • 假设还有线程C 、D、E… 持有对象1引用,那么无限期的不能回收该对象1引用了,那么就会造成内存泄露.

相关文章

  1. DelayQueue 阻塞队列
2.3 ScheduledExecutorService(JDk 线程池)

JUC 提供了相关的类以支持对 延时任务周期任务 的支持,功能类似于 java.util.Timer,但官方推荐使用功能更为强大全面的
ScheduledThreadPoolExecutor,因为后者支持多任务作业,即线程池;

(1)实现
public void ScheduledExecutorServiceTest() throws InterruptedException {ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);System.out.println("创建5秒延迟的任务,时间:"+ DateTools.getLong2YyyyMmDdHhMmSs(System.currentTimeMillis()));ScheduledFuture<?> schedule = scheduledExecutorService.schedule(() -> doTask("5s"), 5, TimeUnit.SECONDS);Thread.sleep(4900);schedule.cancel(false);System.err.println("取消5秒延迟的任务,时间:" + DateTools.getLong2YyyyMmDdHhMmSs(System.currentTimeMillis()));System.out.println("创建3秒延迟的任务,时间" + DateTools.getLong2YyyyMmDdHhMmSs(System.currentTimeMillis()));ScheduledFuture<?> schedule2 = scheduledExecutorService.schedule(new Runnable() {@Overridepublic void run() {doTask("3s");}}, 3, TimeUnit.SECONDS);Thread.sleep(4000);}

执行结果:

创建5秒延迟的任务,时间:2020-12-14 13:49:17
取消5秒延迟的任务,时间:2020-12-14 13:49:22
创建3秒延迟的任务,时间2020-12-14 13:49:22
3s 任务执行,时间: 2020-12-14 13:49:25
2.4 Timer与TimerTask(JDK)

在这里插入图片描述

  • TaskQueue中的排序是对TimerTask中的下一次执行时间进行堆排序,每次去取数组第一个。
  • 而delayQueue是对queue中的元素的getDelay()结果进行排序

Timer是一种定时器工具,用来在一个后台线程计划执行指定任务。它可以计划执行一个任务一次或反复多次。 主要方法:
在这里插入图片描述

(1)实现
public class TimerDemo {public static void main(String[] args) {Timer timer = new Timer();// 实例化Timer类timer.schedule(new TimerTask() {public void run() {System.out.println("退出,时间:"+ DateTools.getLong2YyyyMmDdHhMmSs(System.currentTimeMillis()));this.cancel();}}, 5000); // 5sSystem.out.println("本程序存在5秒后自动退出,时间:"+ DateTools.getLong2YyyyMmDdHhMmSs(System.currentTimeMillis()));}
}
(2)优缺点

缺点:

  • Timers没有持久化机制.
  • Timers不灵活 (只可以设置开始时间和重复间隔,不是基于时间、日期、天等(秒、分、时)的)
  • Timers 不能利用线程池,一个timer一个线程
  • Timers没有真正的管理计划
(3)任务对比

在这里插入图片描述
还有: DelayQueue 延时队列(JDK),无法指定绝对的日期或时间,无法高可用(节点挂了任务不能跑)

2.5 Redis 延迟队列

Redis 实现延迟队列有两种方式:

  1. Redis sorted set
  2. Redis 过期回调
(1)Redis sorted set

Redis的数据结构Zset,同样可以实现延迟队列的效果,主要利用它的score属性,redis通过score来为集合中的成员进行从小到大的排序。
在这里插入图片描述
通过zadd命令向队列delayqueue 中添加元素,并设置score值表示元素过期的时间;

delayqueue 添加三个order1order2order3,分别是10秒20秒30秒后过期。

 zadd delayqueue 3 order3

消费端轮询队列delayqueue, 将元素排序后取最小时间与当前时间比对,如小于当前时间代表已经过期移除key

public class RedisZsetDelayQueue {private static String DELAY_QUEUE = "delay_queue";public static void main(String[] args) {RedisZsetDelayQueue redisDelay = new RedisZsetDelayQueue();redisDelay.pushOrderQueue();redisDelay.pollOrderQueue();redisDelay.deleteZSet();}/*** 消息入队*/public void pushOrderQueue() {Jedis jedis = JedisClient.JedisClient();Calendar cal1 = Calendar.getInstance();cal1.add(Calendar.SECOND, 10);int order1 = (int) (cal1.getTimeInMillis() / 1000); // 10s 后的时间戳Calendar cal2 = Calendar.getInstance();cal2.add(Calendar.SECOND, 20);int order2 = (int) (cal2.getTimeInMillis() / 1000);Calendar cal3 = Calendar.getInstance();cal3.add(Calendar.SECOND, 30);int order3 = (int) (cal3.getTimeInMillis() / 1000);/*** String key, double score, String member* key = DELAY_QUEUE* order1 = 权重* member = order1*/jedis.zadd(DELAY_QUEUE, order1, "order1");jedis.zadd(DELAY_QUEUE, order2, "order2");jedis.zadd(DELAY_QUEUE, order3, "order3");System.out.println(DateTools.getLong2YyyyMmDdHhMmSs(System.currentTimeMillis()) + " add finished.");}/*** 删除队列*/public void deleteZSet() {Jedis jedis = JedisClient.JedisClient();jedis.del(DELAY_QUEUE);}/*** 消费消息*/public void pollOrderQueue() {Jedis jedis = JedisClient.JedisClient();while (true) {Set<Tuple> set = jedis.zrangeWithScores(DELAY_QUEUE, 0, 0);String value = ((Tuple) set.toArray()[0]).getElement();int score = (int) ((Tuple) set.toArray()[0]).getScore();Calendar cal = Calendar.getInstance();int nowSecond = (int) (cal.getTimeInMillis() / 1000);if (nowSecond >= score) {// 移除有序集合中的一个或多个成员jedis.zrem(DELAY_QUEUE, value);System.out.println(DateTools.getLong2YyyyMmDdHhMmSs(System.currentTimeMillis()) + " removed key:" + value);}// 获取有序集合的成员数if (jedis.zcard(DELAY_QUEUE) <= 0) {System.out.println(DateTools.getLong2YyyyMmDdHhMmSs(System.currentTimeMillis()) + " zset empty ");return;}try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}

运行结果:

2020-12-14 16:49:52 add finished.
2020-12-14 16:50:02 removed key:order1
2020-12-14 16:50:12 removed key:order2
2020-12-14 16:50:22 removed key:order3
2020-12-14 16:50:22 zset empty 
(2)Redis 的过期回调

Rediskey过期回调事件,也能达到延迟队列的效果,简单来说我们开启监听key是否过期的事件,一旦key过期会触发一个callback事件。

修改redis.conf文件开启notify-keyspace-events Ex

在线文档:https://redis.io/topics/notifications

notify-keyspace-events Ex
############################# Event notification ############################### Redis can notify Pub/Sub clients about events happening in the key space.
# This feature is documented at http://redis.io/topics/notifications
#
# For instance if keyspace events notification is enabled, and a client
# performs a DEL operation on key "foo" stored in the Database 0, two
# messages will be published via Pub/Sub:
#
# PUBLISH __keyspace@0__:foo del
# PUBLISH __keyevent@0__:del foo
#
# It is possible to select the events that Redis will notify among a set
# of classes. Every class is identified by a single character:
#  下面是监听时,参数配置:
#  K     Keyspace events, published with __keyspace@<db>__ prefix.
#  E     Keyevent events, published with __keyevent@<db>__ prefix.
#  g     Generic commands (non-type specific) like DEL, EXPIRE, RENAME, ...  基本命令
#  $     String commands
#  l     List commands
#  s     Set commands
#  h     Hash commands
#  z     Sorted set commands
#  x     Expired events (events generated every time a key expires)
#  e     Evicted events (events generated when a key is evicted for maxmemory) 驱逐,赶出 OOM的时候监听
#  A     Alias for g$lshzxe, so that the "AKE" string means all the events.
#
#  The "notify-keyspace-events" takes as argument a string that is composed
#  of zero or multiple characters. The empty string means that notifications
#  are disabled.
#
#  Example: to enable list and generic events, from the point of view of the
#           event name, use:
#
#  notify-keyspace-events Elg
#
#  Example 2: to get the stream of the expired keys subscribing to channel
#             name __keyevent@0__:expired use:
#
#  notify-keyspace-events Ex  ### 参数为 “Ex”。x 代表了过期事件
#
#  By default all notifications are disabled because most users don't need
#  this feature and the feature has some overhead. Note that if you don't
#  specify at least one of K or E, no events will be delivered.
notify-keyspace-events "EX"

Redis监听配置,注入Bean RedisMessageListenerContainer

@Configuration
public class RedisListenerConfig {@BeanRedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {RedisMessageListenerContainer container = new RedisMessageListenerContainer();container.setConnectionFactory(connectionFactory);return container;}
}

编写Redis过期回调监听方法,必须继承KeyExpirationEventMessageListener ,有点类似于MQ的消息监听。

@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {super(listenerContainer);}@Overridepublic void onMessage(Message message, byte[] pattern) {String expiredKey = message.toString();System.out.println("监听到key:" + expiredKey + "已过期");}
}

到这代码就编写完成,非常的简单,接下来测试一下效果,在redis-cli客户端添加一个key 并给定3s的过期时间。

 set wxw 123 ex 3

在控制台成功监听到了这个过期的key

监听到过期的key为:wxw

开启过期事件监听后运行结果:
在这里插入图片描述

相关文章

  1. Redis Zset操作在线文档
(3)优缺点

优点:

  1. 由于使用Redis作为消息通道,消息都存储在Redis中。如果发送程序或者任务处理程序挂了,重启之后,还有重新处理数据的可能性。
  2. 做集群扩展相当方便
  3. 时间准确度高

缺点:

  1. 需要额外进行redis维护
2.6 RabbitMQ 延迟队列

利用 RabbitMQ 做延时队列是比较常见的一种方式,而实际上RabbitMQ 自身并没有直接支持提供延迟队列功能,而是通过 RabbitMQ 消息队列的 TTLDXL这两个属性间接实现的。

先来认识一下 TTLDXL两个概念:

  • TTL 顾名思义:指的是消息的存活时间,RabbitMQ可以通过x-message-tt参数来设置指定Queue(队列)和 Message(消息)上消息的存活时间,它的值是一个非负整数,单位为微秒。

    RabbitMQ 可以从两种维度设置消息过期时间,分别是队列消息本身

    • 设置队列过期时间,那么队列中所有消息都具有相同的过期时间。
    • 设置消息过期时间,对队列中的某一条消息设置过期时间,每条消息TTL都可以不同。

    如果同时设置队列和队列中消息的TTL,则TTL值以两者中较小的值为准。而队列中的消息存在队列中的时间,一旦超过TTL过期时间则成为Dead Letter(死信)。

  • Dead Letter ExchangesDLX):DLX即死信交换机,绑定在死信交换机上的即死信队列。RabbitMQQueue(队列)可以配置两个参数x-dead-letter-exchangex-dead-letter-routing-key(可选),一旦队列内出现了Dead Letter(死信),则按照这两个参数可以将消息重新路由到另一个Exchange(交换机),让消息重新被消费。

    • x-dead-letter-exchange:队列中出现Dead Letter后将Dead Letter重新路由转发到指定 exchange(交换机)。
    • x-dead-letter-routing-key:指定routing-key发送,一般为要指定转发的队列。

队列出现Dead Letter的情况有:

  • 消息或者队列的TTL过期
  • 队列达到最大长度
  • 消息被消费端拒绝(basic.reject or basic.nack)

案例分析

下边结合一张图看看如何实现超30分钟未支付,则关闭该订单的功能

我们在下单成功时,开启一个延迟消息队列order.delay.queue ,并设置x-message-tt消息存活时间为30分钟,当到达30分钟后订单消息A0001成为了Dead Letter(死信),延迟队列检测到有死信,通过配置x-dead-letter-exchange,将死信重新转发到能正常消费的关闭订单的消息队列,消费端直接监听关闭订单消息队列后检查数据库支付状态,如果未支付,则关闭该订单。
在这里插入图片描述

(1)实现

发送消息时指定消息延迟的时间

public void send(String delayTimes) {amqpTemplate.convertAndSend("order.pay.exchange", "order.pay.queue","大家好我是延迟数据", message -> {// 设置延迟毫秒值message.getMessageProperties().setExpiration(String.valueOf(delayTimes));return message;});}
}

设置延迟队列出现死信后的转发规则

	/*** 延时队列*/@Bean(name = "order.delay.queue")public Queue getMessageQueue() {return QueueBuilder.durable(RabbitConstant.DEAD_LETTER_QUEUE)// 配置到期后转发的交换机.withArgument("x-dead-letter-exchange", "order.close.exchange")// 配置到期后转发的路由键.withArgument("x-dead-letter-routing-key", "order.close.queue").build();}

源码地址:GitHub快速访问

(2)优缺点
  • 优点: 高效,可以利用rabbitmq的分布式特性轻易的进行横向扩展,消息支持持久化增加了可靠性。
  • 缺点:本身的易用度要依赖于rabbitMq的运维.因为要引用rabbitMq,所以复杂度和成本变高
2.7 时间轮(Netty|Kafka|RocketMQ)

前边几种延时队列的实现方法相对简单,比较容易理解,时间轮算法就稍微有点抽象了。kafkanetty都有基于时间轮算法实现延时队列,下边主要实践Netty的延时队列讲一下时间轮是什么原理。

先来看一张时间轮的原理图,解读一下时间轮的几个基本概念
在这里插入图片描述

  • wheel :时间轮,图中的圆盘可以看作是钟表的刻度。比如一圈round 长度为24秒,刻度数为 8,那么每一个刻度表示 3秒。那么时间精度就是 3秒。时间长度 / 刻度数值越大,精度越大。

当添加一个定时、延时任务A,假如会延迟25秒后才会执行,可时间轮一圈round 的长度才24秒,那么此时会根据时间轮长度和刻度得到一个圈数 round和对应的指针位置 index,也是就任务A会绕一圈指向0格子上,此时时间轮会记录该任务的roundindex信息。当round=0,index=0 ,指针指向0格子 任务A并不会执行,因为 round=0不满足要求。

所以每一个格子代表的是一些时间,比如1秒25秒 都会指向0格子上,而任务则放在每个格子对应的链表中,这点和HashMap的数据有些类似。

Netty构建延时队列主要用HashedWheelTimerHashedWheelTimer底层数据结构依然是使用DelayedQueue,只是采用时间轮的算法来实现。

下面我们用Netty 简单实现延时队列,HashedWheelTimer构造函数比较多,解释一下各参数的含义。

  • ThreadFactory :表示用于生成工作线程,一般采用线程池;

  • tickDurationunit:每格的时间间隔,默认100ms;

  • ticksPerWheel:一圈下来有几格,默认512,而如果传入数值的不是2的N次方,则会调整为大于等于该参数的一个2的N次方数值,有利于优化hash值的计算。

public HashedWheelTimer(ThreadFactory threadFactory, long tickDuration, TimeUnit unit, int ticksPerWheel) {this(threadFactory, tickDuration, unit, ticksPerWheel, true);}
  • TimerTask:一个定时任务的实现接口,其中run方法包装了定时任务的逻辑。

  • Timeout:一个定时任务提交到Timer之后返回的句柄,通过这个句柄外部可以取消这个定时任务,并对定时任务的状态进行一些基本的判断。

  • Timer:是HashedWheelTimer实现的父接口,仅定义了如何提交定时任务和如何停止整个定时机制。

(1)Netty 实现
public class NettyDelayQueue {public static void main(String[] args) {final Timer timer = new HashedWheelTimer(Executors.defaultThreadFactory(), 5, TimeUnit.SECONDS, 2);TimerTask task1 = new TimerTask() {public void run(Timeout timeout) throws Exception {System.out.println("order1  5s 后执行 ");timer.newTimeout(this, 5, TimeUnit.SECONDS);//结束时候再次注册}};timer.newTimeout(task1, 5, TimeUnit.SECONDS);TimerTask task2 = new TimerTask() {public void run(Timeout timeout) throws Exception {System.out.println("order2  10s 后执行");timer.newTimeout(this, 10, TimeUnit.SECONDS);//结束时候再注册}};timer.newTimeout(task2, 10, TimeUnit.SECONDS);//该任务仅仅运行一次timer.newTimeout(new TimerTask() {public void run(Timeout timeout) throws Exception {System.out.println("order3  15s 后执行一次");}}, 15, TimeUnit.SECONDS);}
}

从执行的结果看,order3order3延时任务只执行了一次,而order2order1为定时任务,按照不同的周期重复执行。

运行结果:

order1  5s 后执行 
order2  10s 后执行
order3  15s 后执行一次
order1  5s 后执行 
order2  10s 后执行
(2)Kafka 实现

实现思路:

  1. 基于延迟主题和 DelayQueue 实现延迟队列
  2. 基于单层文件时间轮实现延迟队列

(1)基于延迟主题和 DelayQueue 实现延迟队列

在发送延时消息的时候并不是先投递到要发送的真实主题(real_topic)中,而是先投递到一些 Kafka 内部的主题(delay_topic)中,这些内部主题对用户不可见,然后通过一个自定义的服务拉取这些内部主题中的消息,并将满足条件的消息再投递到要发送的真实的主题中,消费者所订阅的还是真实的主题。

如果采用这种方案,那么一般是按照不同的延时等级来划分的,比如设定5s、10s、30s、1min、2min、5min、10min、20min、30min、45min、1hour、2hour这些按延时时间递增的延时等级,延时的消息按照延时时间投递到不同等级的主题中,投递到同一主题中的消息的延时时间会被强转为与此主题延时等级一致的延时时间,这样延时误差控制在两个延时等级的时间差范围之内(比如延时时间为17s的消息投递到30s的延时主题中,之后按照延时时间为30s进行计算,延时误差为13s)。虽然有一定的延时误差,但是误差可控,并且这样只需增加少许的主题就能实现延时队列的功能。
在这里插入图片描述
发送到内部主题(delay*topic**)中的消息会被一个独立的 DelayService 进程消费,这个 DelayService 进程和 Kafka broker 进程以一对一的配比进行同机部署(参考下图),以保证服务的可用性

在这里插入图片描述

针对不同延时级别的主题,在 DelayService 的内部都会有单独的线程来进行消息的拉取,以及单独的 DelayQueue(这里用的是 JUC 中 DelayQueue)进行消息的暂存。与此同时,在 DelayService 内部还会有专门的消息发送线程来获取 DelayQueue 的消息并转发到真实的主题中。从消费、暂存再到转发,线程之间都是一一对应的关系。如下图所示,DelayService 的设计应当尽量保持简单,避免锁机制产生的隐患。
在这里插入图片描述

为了保障内部 DelayQueue 不会因为未处理的消息过多而导致内存的占用过大,DelayService 会对主题中的每个分区进行计数,当达到一定的阈值之后,就会暂停拉取该分区中的消息。

因为一个主题中一般不止一个分区,分区之间的消息并不会按照投递时间进行排序,DelayQueue的作用是将消息按照再次投递时间进行有序排序,这样下游的消息发送线程就能够按照先后顺序获取最先满足投递条件的消息。

缺点

  1. 为了DelayQueue排序,将消息放到一个分区,这样是简化了设计,但是丢失了动态的扩展性,无法提升单个分区的性能
  2. 有一定的延时误差,无法做到秒级别的精确延时(消息在拉取一批消息后,如果这批消息未达到延迟时间,那么将重新写入主题重新消费,但是当消费严重滞后(消息堆积)时,重新写入主题到再消费,可能需要已经过了指定时间)

(2)基于单层文件时间轮实现延迟队列

在这里插入图片描述

单层时间轮:延时消息也不再是缓存在 内存中, 而是暂存至文件中 时间轮中每个时间格代表一个延时时间, 并且每个时间 也对应一个文件,整体上可 作单层文件时间轮

每个时间格代表 秒,若要支持 小时(也就是 60=7200 )之内的延时时间的消息, 那么整个单层时间轮的时间格数就需要 7200 个,与此对应的也就需要 72 00 件,听上去似 乎需要庞大的系统开销,就单单文件句柄的使用也会耗费很多的系统资源 其实不然,我们并 不需要维持所有文件的文件句柄,只需要加载距离时间轮表盘指针( currentTime )相近位置的部分文件即可,其余都可以用类似“懒加载”的机制来维持:若与时间格对应的文件不存在则 可以新建,若与时间格对应的文件未加载则可以重新加载,整体上造成的时延相比于延时等级 方案而言微乎其微。随着表盘指针的转动,其相邻的文件也会变得不同, 整体上在内存中只需 要维持少量的文件句柄就可以让系统运转起来。

疑问点

  1. 为什么不采用多层时间轮?

    采用多层时间轮,设计到时间轮降级,那么会有频繁的文件在磁盘写入和删除,很影响性能

  2. 采用时间轮可以解决的问题

    • 可以解决延时进度的问题
    • 可以解决内存暴涨的问题,(方案一:通过给DelayQueue 设置阈值解决暴涨问题的)
    • 可靠性问题(多副本机制保证)

可靠性

生产者同样将消息写入多个备份(单层文件时间轮〉,待时间轮转动而触发某些时间格过期时就可以将时间格 对应的文件内容(也就是延时消息〉转发到真实主题中,并且删除相应的文件。与此同时,还 会有 个后台服务专门用来清理其他时间轮中相应的时间格

在这里插入图片描述

总体上而言,对于延时队列的封装 现,如果要求延时精度不是那么高,则建议使用延时 等级的实现方案,毕竟实现起来简单明了。反之,如果要求高精度或自定义延时时间,那么可以选择单层文件时间轮的方案。

(3)RocketMQ 实现

相关文章

  1. 阿里 RocketQM 定时消息和延迟消息

相关文章:

  1. 六种延迟队列实现方式 || 源码
  2. 延迟任务—队列实现
  3. 博客园——延迟队列

写作一点也不比上班干活轻松,查证资料反复验证demo的可行性,搭建各种RabbitMQRedis环境,只想说我太难了!

三、延迟队列总结

目前开源的延迟消息,也就pulsar比较可靠,其次是rocketmq,但rocket只支持18个等级的固定刻度,pulsar也是近几天内的任意时间,长时间不行。其他的,rabbit不适合大量堆积,redis key多的时候会严重滞后(惰性删除),而且没有ack。sorted set,在field超过一定量时预计5000左右会产生性能问题,需要做二次分片。jdk的delayqueue,性能就不提了,只能做短时、量少的延迟消息,而且还没有ack,可能存在丢失,如果要写入数据库,也很麻烦。内存时间轮性能好点,但也没法ack,而且消费阻塞会引起问题(单线程)

目前最佳实践就是组合以上的几种,定时任务+pulsar/rocket本身时间轮等,多重组合,可以实现海量,超长时间的延迟消息

3.1 Apache Pulsar 云原生分布式消息流平台

Apache Pulsar也是基于死信队列实现延迟消息的。Pulsar 作为下一代云原生分布式消息流平台,支持多租户、持久化存储、多机房跨区域数据复制,具有强一致性、高吞吐以及低延时的高可扩展流数据存储特性,内置诸多其他系统商业版本才有的特性,是云原生时代解决实时消息流数据传输、存储和计算的最佳解决方案。
在这里插入图片描述

Apache Pulsar 提供了统一的消费模型,支持 Stream(如 Kafka)和 Queue(如 RabbitMQ)两种消费模型, 支持 exclusive、failover 和 shared 三种消费模式。同时,Pulsar 提供和 Kafka 兼容的 API,以及 Kafka-On-Pulsar(KoP) 组件来兼容 Kafka 的应用程序,KoP 在 Pulsar Broker 中解析 Kafka 协议,用户不用改动客户端的任何 Kafka 代码就能直接使用 Pulsar

目前,Apache Pulsar 已经应用部署在国内外众多大型互联网公司和传统行业公司,案例分布在人工智能、金融、电信运营商、直播与短视频、物联网、零售与电子商务、在线教育等多个行业,如美国有线电视网络巨头Comcast、Yahoo!、腾讯、中国电信、中国移动、BIGO、VIPKID 等。

  • 官网地址:http://pulsar.apache.org/
3.2 有赞延迟队列
(1)整体结构

整个延迟队列由4个部分组成:

  • Job Pool用来存放所有Job的元信息。
  • Delay Bucket是一组以时间为维度的有序队列,用来存放所有需要延迟的/已经被reserve的Job(这里只存放Job Id)。
  • Timer负责实时扫描各个Bucket,并将delay时间大于等于当前时间的Job放入到对应的Ready Queue。
  • Ready Queue存放处于Ready状态的Job(这里只存放Job Id),以供消费程序消费。

如下图表述:
在这里插入图片描述

(2)网络拓扑

在这里插入图片描述

(3)生命周期
  • 用户对某个商品下单,系统创建订单成功,同时往延迟队列里put一个job。job结构为:{‘topic’:‘orderclose’, ‘id’:'ordercloseorderNoXXX’, ‘delay’:1800 ,’TTR’:60 , ‘body’:’XXXXXXX’}
  • 延迟队列收到该job后,先往job pool中存入job信息,然后根据delay计算出绝对执行时间,并以轮询(round-robbin)的方式将job id放入某个bucket。
  • timer每时每刻都在轮询各个bucket,当1800秒(30分钟)过后,检查到上面的job的执行时间到了,取得job id从job pool中获取元信息。如果这时该job处于deleted状态,则pass,继续做轮询;如果job处于非deleted状态,首先再次确认元信息中delay是否大于等于当前时间,如果满足则根据topic将job id放入对应的ready queue,然后从bucket中移除;如果不满足则重新计算delay时间,再次放入bucket,并将之前的job id从bucket中移除。
  • 消费端轮询对应的topic的ready queue(这里仍然要判断该job的合理性),获取job后做自己的业务逻辑。与此同时,服务端将已经被消费端获取的job按照其设定的TTR,重新计算执行时间,并将其放入bucket。
  • 消费端处理完业务后向服务端响应finish,服务端根据job id删除对应的元信息。
(4)缺点和不足
  1. timer是通过独立线程的无限循环来实现,在没有ready job的时候会对CPU造成一定的浪费。
  2. 消费端在reserve job的时候,采用的是http短轮询的方式,且每次只能取的一个job。如果ready job较多的时候会加大网络I/O的消耗。
  3. 数据存储使用的redis,消息在持久化上受限于redis的特性。
  4. scale-out的时候依赖第三方(nginx)。

相关文档

  1. 有赞延迟队列设计
3.3 分布式任务调度 SchedulerX

分布式任务调度 SchedulerX 2.0 是阿里巴巴基于 Akka 架构自研的新一代分布式任务调度平台。您可以使用 SchedulerX 2.0 编排定时任务、工作流任务、进行分布式任务调度。

  • 官方文档:SchedulerX
  • 阿里分布式任务调度平台(目前公测期免费)地址:快速访问

相关文章:

消息队列之关于如何实现延时队列

一、延时队列的应用 1.1 什么是延时队列&#xff1f; 顾名思义&#xff1a;首先它要具有队列的特性&#xff0c;再给它附加一个延迟消费队列消息的功能&#xff0c;也就是说可以指定队列中的消息在哪个时间点被消费。 延时队列在项目中的应用还是比较多的&#xff0c;尤其像…...

Linux Shell 002-基础知识

Linux Shell 002-基础知识 本节关键字&#xff1a;Linux、Bash Shell、基础知识、Bash特性 相关指令&#xff1a;bash、rm、cp、touch、date 基础知识 什么是Shell脚本 简单概括&#xff1a;将需要执行的命令保存到文本中&#xff0c;按照顺序执行。 准备描述&#xff1a;sh…...

前缀和+单调双队列+贪心:LeetCode2945:找到最大非递减数组的长度

本文涉及知识点 C算法&#xff1a;前缀和、前缀乘积、前缀异或的原理、源码及测试用例 包括课程视频 单调双队列 贪心 题目 给你一个下标从 0 开始的整数数组 nums 。 你可以执行任意次操作。每次操作中&#xff0c;你需要选择一个 子数组 &#xff0c;并将这个子数组用它所…...

【微服务】springboot整合kafka-stream使用详解

目录 一、前言 二、kafka stream概述 2.1 什么是kafka stream 2.2 为什么需要kafka stream 2.2.1 对接成本低 2.2.2 节省资源 2.2.3 使用简单 2.3 kafka stream特点 2.4 kafka stream中的一些概念 2.5 Kafka Stream应用场景 三、环境准备 3.1 搭建zk 3.1.1 自定义d…...

什么是动态代理?

目录 一、为什么需要代理&#xff1f; 二、代理长什么样&#xff1f; 三、Java通过什么来保证代理的样子&#xff1f; 四、动态代理实现案例 五、动态代理在SpringBoot中的应用 导入依赖 数据库表设计 OperateLogEntity实体类 OperateLog枚举 RecordLog注解 上下文相…...

【OAuth2】:赋予用户控制权的安全通行证--原理篇

&#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 接下来看看由辉辉所写的关于OAuth2的相关操作吧 目录 &#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 一.什么是OAuth? 二.为什么要用OAuth?…...

【K8s】2# 使用kuboard管理K8s集群(kuboard安装)

文章目录 安装 Kuboard v3部署计划 安装登录测试 安装 Kuboard v3 部署计划 在正式安装 kuboard v3 之前&#xff0c;需做好一个简单的部署计划的设计&#xff0c;在本例中&#xff0c;各组件之间的连接方式&#xff0c;如下图所示&#xff1a; 假设用户通过 http://外网IP:80…...

爬虫是什么?起什么作用?

【爬虫】 如果把互联网比作一张大的蜘蛛网&#xff0c;数据便是放于蜘蛛网的各个节点&#xff0c;而爬虫就是一只小蜘蛛&#xff0c;沿着网络抓取自己得猎物&#xff08;数据&#xff09;。这种解释可能更容易理解&#xff0c;官网的&#xff0c;就是下面这个。 爬虫是一种自动…...

代码随想录27期|Python|Day24|回溯法|理论基础|77.组合

图片来自代码随想录 回溯法题目目录 理论基础 定义 回溯法也可以叫做回溯搜索法&#xff0c;它是一种搜索的方式。 回溯是递归的副产品&#xff0c;只要有递归就会有回溯。回溯函数也就是递归函数&#xff0c;指的都是一个函数。 基本问题 组合问题&#xff08;无序&…...

mysql(49) : 大数据按分区导出数据

代码 import com.alibaba.gts.flm.base.util.Mysql8Instance;import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.u…...

阿里云ECS配置IPv6后,如果无法访问该服务器上的网站,可检查如下配置

1、域名解析到这个IPv6地址,同一个子域名可以同时解析到IPv4和IPv6两个地址&#xff0c;这样就可以给网站配置ip4和ipv6双栈&#xff1b; 2、在安全组规则开通端口可访问&#xff0c;设定端口后注意授权对象要特殊设置“源:::/0” 3、到服务器nginx配置处&#xff0c;增加端口…...

基于SSM的双减后初小教育课外学习生活活动平台的设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;Vue 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#xff1a;是 目录…...

HTTP前端请求

目录 HTTP 请求1.请求组成2.请求方式与数据格式get 请求示例post 请求示例json 请求示例multipart 请求示例数据格式小结 3.表单3.1.作用与语法3.2.常见的表单项 4.session 原理5.jwt 原理 HTTP 请求 1.请求组成 请求由三部分组成 请求行请求头请求体 可以用 telnet 程序测…...

前端性能优化二十四:花裤衩模板第三方库打包

(1). 工作原理: ①. externals配置在所创建bundle时:a. 会依赖于用户环境(consumers environment)中的依赖,防止将某些import的包(package)打包到bundle中b. 在运行时(runtime)再去从外部获取这些扩展依赖(external dependencies)②. webpack会检测这些组件是否在externals中注…...

多维时序 | MATLAB实现BiTCN-Multihead-Attention多头注意力机制多变量时间序列预测

多维时序 | MATLAB实现BiTCN-Multihead-Attention多头注意力机制多变量时间序列预测 目录 多维时序 | MATLAB实现BiTCN-Multihead-Attention多头注意力机制多变量时间序列预测预测效果基本介绍模型描述程序设计参考资料 预测效果 基本介绍 多维时序 | MATLAB实现BiTCN-Multihea…...

Qt的简单游戏实现提供完整代码

文章目录 1 项目简介2 项目基本配置2.1 创建项目2.2 添加资源 3 主场景3.1 设置游戏主场景配置3.2 设置背景图片3.3 创建开始按钮3.4 开始按钮跳跃特效实现3.5 创建选择关卡场景3.6 点击开始按钮进入选择关卡场景 4 选择关卡场景4.1场景基本设置4.2 背景设置4.3 创建返回按钮4.…...

SpringMVC之文件的下载

系列文章目录 提示&#xff1a;这里可以添加系列文章的所有文章的目录&#xff0c;目录需要自己手动添加 SpringMVC之文件的下载 提示&#xff1a;写完文章后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 系列文章目录前言一、文件下载实现…...

计算机组成原理第6章-(算术运算)【下】

移位运算 对于有符号数的移位称为算术移位,对于无符号数的移位称为逻辑移位。 算术移位规则【极其重要】 对于正数的算术移位,且不管是何种机器数【原码、反码、补码】,移位后出现的空位全部填0。 而对于负数的算术移位,机器数不同,移位后的规则也不同。 对于负数的原…...

【开题报告】基于微信小程序的校园资讯平台的设计与实现

1.选题背景与意义 随着移动互联网的快速发展&#xff0c;微信成为了人们日常生活中不可或缺的工具之一。在校园生活中&#xff0c;学生们对于校园资讯的获取和交流需求也越来越高。然而&#xff0c;传统的校园资讯发布方式存在信息不及时、传播范围有限等问题&#xff0c;无法…...

VUE前端导出文件之file-saver插件

VUE前端导出文件之file-saver插件 安装 npm install file-saver --save # 如使用TS开发&#xff0c;可安装file-saver的TypeScript类型定义 npm install types/file-saver --save-dev如果需要保存大于 blob 大小限制的非常大的文件&#xff0c;或者没有 足够的 RAM&#xff0…...

【Earth Engine】协同Sentinel-1/2使用随机森林回归实现高分辨率相对财富(贫困)制图

目录 1 简介与摘要2 思路3 效果预览4 代码思路5 完整代码6 后记 1 简介与摘要 最近在做一些课题&#xff0c;需要使用Sentinel-1/2进行机器学习制图。 然后想着总结一下相关数据和方法&#xff0c;就花半小时写了个代码。 然后再花半小时写下这篇博客记录一下。 因为基于多次拍…...

C++ 检测 是不是 com组件 的办法 已解决

在日常开发中&#xff0c;遇到动态库和 com组件库的调用 无法区分。检测是否com组件的办法 在头部文件&#xff0c;引入文件 如果能编译成功说明是 com组件&#xff0c;至于动态库如何引入&#xff0c;还在观察中 最简单办法 regsvr32 TerraExplorerX.dll 是com 组件 regs…...

linux buffer的回写的触发链路

mark_buffer_dirty中除了会标记dirty到buffer_head->state、page.flag、folio->mapping->i_pages外&#xff0c;还会调用inode所在文件系统的dirty方法&#xff08;inode->i_sb->s_op->dirty_inode&#xff09;。然后为inode创建一个它所在memory group的wri…...

Lambda表达式超详解

目录 背景 Lambda表达式的用法 函数式接口 Lambda表达式的基本使用 语法精简 变量捕获 匿名内部类 匿名内部类中的变量捕获 Lambda的变量捕获 Lambda表达式在类集中的使用 Collection接口 List接口 Map接口 总结 背景 Lambda表达式是Java SE 8中的一个重要的新特性.…...

西门子博途与菲尼克斯无线蓝牙模块通讯

菲尼克斯无线蓝牙模块 正常运行时,可以使用基站控制字0发送00E0(得到错误代码命令) 正常运行时,可以使用基站控制字0发送00E0(得到错误代码命令)得到各个无线I/O是否连 接的信号(状态字IN word 1的第2、6、10位) 小车1连接状态 小车2连接状态 小车3连接状态 1#小车自…...

vue2 之 实现pdf电子签章

一、前情提要 1. 需求 仿照e签宝&#xff0c;实现pdf电子签章 > 拿到pdf链接&#xff0c;移动章的位置&#xff0c;获取章的坐标 技术 : 使用fabric pdfjs-dist vuedraggable 2. 借鉴 一位大佬的代码仓亏 : 地址 一位大佬写的文章 &#xff1a;地址 3. 优化 在大佬的代码…...

什么是MVC?MVC框架的优势和特点

目录 一、什么是MVC 二、MVC模式的组成部分和工作原理 1、模型&#xff08;Model&#xff09; 2、视图&#xff08;View&#xff09; 3、控制器&#xff08;Controller&#xff09; 三、MVC模式的工作过程如下&#xff1a; 用户发送请求&#xff0c;请求由控制器处理。 …...

主从复制mysql-replication | Replication故障排除

主从复制mysql-replication 准备环境 #防火墙 selinux systemctl stop firewalld --now &&setenforce 0 #修改主机名&#xff1a;hostnamectl set-hostname 名字 tip&#xff1a;vim /etc/sysconfig/network-scripts/ifcfg-ens33 BOOTPRTOTstatic IPADDR192.168.100.…...

基于Java SSM框架实现教学质量评价评教系统项目【项目源码+论文说明】计算机毕业设计

基于java的SSM框架实现教学质量评价评教系统演示 摘要 随着科学技术的飞速发展&#xff0c;社会的方方面面、各行各业都在努力与现代的先进技术接轨&#xff0c;通过科技手段来提高自身的优势&#xff0c;教学质量评价系统当然也不能排除在外。教学质量评价系统是以实际运用为…...

03|模型I/O:输入提示、调用模型、解析输出

03&#xff5c;模型I/O&#xff1a;输入提示、调用模型、解析输出 从这节课开始&#xff0c;我们将对 LangChain 中的六大核心组件一一进行详细的剖析。 模型&#xff0c;位于 LangChain 框架的最底层&#xff0c;它是基于语言模型构建的应用的核心元素&#xff0c;因为所谓 …...

springcloud-gateway-2-鉴权

目录 一、跨域安全设置 二、GlobalFilter实现全局的过滤与拦截。 三、GatewayFilter单个服务过滤器 1、原理-官方内置过滤器 2、自定义过滤器-TokenAuthGatewayFilterFactory 3、完善TokenAuthGatewayFilterFactory的功能 4、每一个服务编写一个或多个过滤器&#xff0c…...

实现一个最简单的内核

更好的阅读体验&#xff0c;请点击 YinKai s Blog | 实现一个最简单的内核。 ​ 这篇文章带大家实现一个最简单的操作系统内核—— Hello OS。 PC 机的引导流程 ​ 我们这里将借助 Ubuntu Linux 操纵系统上的 GRUB 引导程序来引导我们的 Hello OS。 ​ 首先我们得了解一下&a…...

2024华为OD机试真题指南宝典—持续更新(JAVAPythonC++JS)【彻底搞懂算法和数据结构—算法之翼】

PC端可直接搜索关键词 快捷键&#xff1a;CtrlF 年份关键字、题目关键字等等 注意看本文目录-快速了解本专栏 文章目录 &#x1f431;2024年华为OD机试真题&#xff08;马上更新&#xff09;&#x1f439;2023年华为OD机试真题&#xff08;更新中&#xff09;&#x1f436;新…...

【12.23】转行小白历险记-算法02

不会算法的小白不是好小白&#xff0c;可恶还有什么可以难倒我这个美女的&#xff0c;不做花瓶第二天&#xff01; 一、螺旋矩阵 59. 螺旋矩阵 II - 力扣&#xff08;LeetCode&#xff09; 1.核心思路&#xff1a;确定循环的路线&#xff0c;左闭右开循环&#xff0c;思路简…...

k8s部署nginx-ingress服务

k8s部署nginx-ingress服务 经过大佬的拷打&#xff0c;终于把这块的内容配置完成了。 首先去 nginx-ingress官网查看相关内容。 核心就是这个&#xff1a; kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.8.2/deploy/static/prov…...

SpringBoot Elasticsearch全文搜索

文章目录 概念全文搜索相关技术Elasticsearch概念近实时索引类型文档分片(Shard)和副本(Replica) 下载启用SpringBoot整合引入依赖创建文档类创建资源库测试文件初始化数据创建控制器 问题参考 概念 全文搜索&#xff08;检索&#xff09;&#xff0c;工作原理&#xff1a;计算…...

Python 常用模块re

Python 常用模块re 【一】正则表达式 【1】说明 正则表达式是一种强大的文本匹配和处理工具&#xff0c;主要用于字符串的模式匹配、搜索和替换。正则表达式测试网址&#xff1a;正则表达式在线测试 正则表达式手册&#xff1a;正则表达式手册 【2】字符组 字符转使用[]表…...

【华为OD题库-106】全排列-java

题目 给定一个只包含大写英文字母的字符串S&#xff0c;要求你给出对S重新排列的所有不相同的排列数。如:S为ABA&#xff0c;则不同的排列有ABA、AAB、BAA三种。 解答要求 时间限制:5000ms,内存限制:100MB 输入描述 输入一个长度不超过10的字符串S&#xff0c;确保都是大写的。…...

Three.js 详细解析(持续更新)

1、简介&#xff1b; Three.js依赖一些要素&#xff0c;第一是scene&#xff0c;第二是render&#xff0c;第三是carmea npm install --save three import * as THREE from "three"; import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js&quo…...

Unity中Shader平移矩阵

文章目录 前言方式一&#xff1a;对顶点本地空间下的坐标进行相加平移1、在属性面板定义一个四维变量记录在 xyz 上平移多少。2、在常量缓冲区进行申明3、在顶点着色器中&#xff0c;在进行其他坐标转化之前&#xff0c;对模型顶点本地空间下的坐标进行转化4、我们来看看效果 方…...

python dash 的学习笔记1

dash 用python开发web界面 https://dash.plotly.com/ 官方上支持jula F# python一类。当然我只会python只学习python中使用dash. 要做一个APP&#xff0c;用php,java以及.net都可以写&#xff0c;只所有选择python是因为最近在用这一个。同时也发现python除了慢全是优点。 资料…...

SQLITE如何同时查询出第一条和最后一条两条记录

一个时间记录表&#xff0c;需要同时得到整个表或一段时间内第一条和最后一条两条记录&#xff0c;按如下方法会提示错误&#xff1a;ORDER BY clause should come after UNION not before select * from sdayXX order by op_date asc limit 1 union select * from sday…...

四、ensp配置ftp服务器实验

文章目录 实验内容实验拓扑操作步骤配置路由器为ftp server 实验内容 本实验模拟企业网络。PC-1为FTP 用户端设备&#xff0c;需要访问FTP Server&#xff0c;从服务器上下载或上传文件。出于安全角度考虑&#xff0c;为防止服务器被病毒文件感染&#xff0c;不允许用户端直接…...

VS2020使用MFC开发一个贪吃蛇游戏

背景&#xff1a; 贪吃蛇游戏 按照如下步骤实现:。初始化地图 。通过键盘控制蛇运动方向&#xff0c;注意重新设置运动方向操作。 。制造食物。 。让蛇移动&#xff0c;如果吃掉食物就重新生成一个食物&#xff0c;如果会死亡就break。用蛇的坐标将地图中的空格替换为 #和”将…...

【经典LeetCode算法题目专栏分类】【第9期】深度优先搜索DFS与并查集:括号生成、岛屿问题、扫雷游戏

《博主简介》 小伙伴们好&#xff0c;我是阿旭。专注于人工智能AI、python、计算机视觉相关分享研究。 ✌更多学习资源&#xff0c;可关注公-仲-hao:【阿旭算法与机器学习】&#xff0c;共同学习交流~ &#x1f44d;感谢小伙伴们点赞、关注&#xff01; 《------往期经典推荐--…...

字符设备驱动开发-注册-设备文件创建

一、字符设备驱动 linux系统中一切皆文件 1、应用层&#xff1a; APP1 APP2 ... fd open("led驱动的文件"&#xff0c;O_RDWR); read(fd); write(); close(); 2、内核层&#xff1a; 对灯写一个驱动 led_driver.c driver_open(); driver_read(); driver_write(…...

TrustZone之可信操作系统

有许多可信内核&#xff0c;包括商业和开源的。一个例子是OP-TEE&#xff0c;最初由ST-Ericsson开发&#xff0c;但现在是由Linaro托管的开源项目。OP-TEE提供了一个功能齐全的可信执行环境&#xff0c;您可以在OP-TEE项目网站上找到详细的描述。 OP-TEE的结构如下图所示&…...

java定义三套场景接口方案

一、背景 在前后端分离开发的背景下&#xff0c;后端java开发人员现在只需要编写接口接口。特别是使用微服务开发的接口。resful风格接口。那么一般后端接口被调用有下面三种场景。一、不需要用户登录的接口调用&#xff0c;第二、后端管理系统接口调用&#xff08;需要账号密…...

idea连接数据库,idea连接MySQL,数据库驱动下载与安装

文章目录 普通Java工程先创建JAVA工程JDBC连接数据库测试连接 可视化连接数据库数据库驱动下载与安装常用的数据库驱动下载MySQL数据库Oracle数据库SQL Server 数据库PostgreSQL数据库 下载MySQL数据库驱动JDBC连接各种数据库的连接语句MySQL数据库Oracle数据库DB2数据库sybase…...

Redis-实践知识

转自极客时间Redis 亚风 原文视频&#xff1a;https://u.geekbang.org/lesson/535?article681062 Redis最佳实践 普通KEY Redis 的key虽然可以自定义&#xff0c;但是最好遵循下面几个实践的约定&#xff1a; 格式&#xff1a;[业务名称]:[数据名]:[id] 长度不超过44字节 不…...