延迟队列实现及其原理详解
1.绪论
本文主要讲解常见的几种延迟队列的实现方式,以及其原理。
2.延迟队列的使用场景
延迟队列主要用于解决每个被调度的任务开始执行的时间不一致的场景,主要包含如下场景:
1.比如订单超过15分钟后,关闭未关闭的订单。
2.比如用户可以下发任务,并且可以自定义任务的开始时间。
3.延迟队列的几大要素
延迟队列主要包含如下几个要素:
1.延迟队列里面存储的其实就是需要调度的任务,所以我们需要一个存储任务的容器;这个容器,可以是的数据库,redis或者内存中队列(包括链表,优先队列等);
2.一个线程,来轮询的存储任务的容器,判断任务是否已经到达执行时间;
4.延迟队列的实现方式
上面我们说了,定时任务其实就是由两个部分组成,分别是存储任务的容器和轮询线程,接下来我们根据这两个组件来分析各种延迟队列的实现。
4.1 定时任务扫表
4.1.1 组成组件
1.调度线程:一般采用分布式的定时任务,如果xxljob等。
2.存储任务的容器:数据库
4.1.2 实现方式
启动定时任务,每隔一段时间,轮询数据库,找出已经到达任务开始时间的任务,查询出后,执行业务逻辑。
4.1.3 优缺点
1.频繁的对数据库进行全表扫描,数据库压力大。
2.可能有时间延迟问题,延迟大小取决于轮询间隔。
3.定时轮询也会增加自身服务器开销。
4.2 基于内存队列的实现方式
4.2.1 实现原理
1.基本实现
如图所示,可以将需要调度的任务,存储到一个链表里面,然后开启一个线程,轮询该链表,如果如果某个任务的执行时间已到,便执行该任务。
但是上述场景存在一个问题,就是每个需要遍历整个链表,时间复杂度为o(n)。在这个定时任务重,过期时间小的任务一定会先被执行,所以我们可以考虑将时间最小的任务放到队首,这样就以o(1)的时间复杂度取出下一个需要执行的任务。
2.基于优先队列的优化
在jdk中可以采用优先队列来实现PriorityQueue,它其实就是一个小顶堆,每次插入元素的时候,可以以O(nlogn)的时间复杂度来维持堆首元素最小的特征。
4.2.2 jdk自带的延迟队列DelayQueue实现方式
我们可以看一下jdk自带的延迟队列DelayQueue的实现方式。
1.组成组件
1.存储任务的容器:数据库
2.调度线程:可能有同学好奇DelayQueue的调度线程是哪一个,其实是我们在使用DelayQqueue时间,会启动一个线程,循环轮询DelayQueue,这线程就是DelayQueue的调度线程。
new Thread(() -> {while(true) {delayQueue.offer();}
}).start();
2.添加元素
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();}}
添加元素其实就是往优先队列里面写入一个任务,优先队列会自动的将过期时间最小的任务放在队首。
3.取出元素
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();}}
其实就是轮询整个优先队列,优先队列的队首元素就是下一个需要调度的任务,如果队首元素的直线时间小于当前时间,返回该任务,否者阻塞当前任务到下一个任务的执行时间,再返回当前任务。
4.2.3 优缺点
1.被调度任务存储在内存中,如果重启服务,需要调度的任务会丢失。
2.基于优先队列实现,插入调度任务时间复杂度为o(nlogn),如果是数据量庞大,插入性能可能会被影响,并且上一个任务的执行时间可能会影响到下一个任务的执行。
4.3 基于时间轮的实现
4.3.1 什么是时间轮
时间轮其实就是利用一个环形队列来表示时间,队列上的每个元素挂载了在这个时间刻度上需要执行的任务。
1.单层时间轮
如图所示,就是一个时间轮,分成了6个刻度,假设每个刻度代表1秒,假设当前时间为0秒,则第一秒执行的任务放在刻度1,第2秒执行的任务放在刻度2。如果任务的执行时间超过了刻度6,比如第8秒需要执行的任务放在哪儿呢。我们可以将其对6求余,放在刻度2的位置,然后用ticket来表示还差几轮才会轮到自己执行。
所以时间轮的执行步骤为,通过一个线程轮询环形队列,找到当前刻度,取出当前刻度上任务链表,如果任务链表中的任务的ticket为1,立刻执行该任务,如果大于1,便将ticket减1,说明是后面轮次的任务。
2.多层时间轮
单层时间轮,一旦时间跨度过大,就会导致时间轮的轮数过多,每个刻度上挂载的链表过长,所以引入多层时间轮。
多层时间轮,其实就是有多个不同刻度的单层时间轮组成的一种结构,以一天为例子,可以用一个3层时间轮来表示。其中,一个时间轮刻度为1秒,一个时间轮刻度为1分钟,一个时间轮刻度为1小时。如果秒时间轮已经转完60个刻度,即1分钟。则分时间轮需要向下转动一个刻度,将任务取出分散到秒时间轮上。这样便实现了任务的分散。
4.3.2 Netty中的时间轮
1.组成组件
1.存储任务的容器:任务数组
2.调度线程:netty启动的推动时间轮的线程。
2.添加元素
//向时间轮中添加定时任务的方法,但该方法实际上只会把定时任务存放到timeouts队列中public Timeout newTimeout(TimerTask task, long delay, TimeUnit unit) {if (task == null) {throw new NullPointerException("task");}if (unit == null) {throw new NullPointerException("unit");}//启动工作线程,并且确保只启动一次,这里面会涉及线程的等待和唤醒start();//计算该定时任务的执行时间,startTime是worker线程的开始时间。以后所有添加进来的任务的执行时间,都是根据这个开始时间做的对比long deadline = System.nanoTime() + unit.toNanos(delay) - startTime;HashedWheelTimeout timeout = new HashedWheelTimeout(task, deadline);//将定时任务和任务的执行时间添加到普通任务队列中timeouts.add(timeout);return timeout;}
注意:netty实现的方式是,业务线程会执行任务加入到HashedWheelTimeout 这个普通队列中,然后再由推动时间轮的线程,来将HashedWheelTimeout中的任务移到时间轮中。其实这一步,也可以省略,直接在业务线程添加调度的任务的时候,将执行任务写入到时间轮询中。增加HashedWheelTimeout的原因应该是为了减少并发读写。
3.执行定时任务
//时间轮线程一直执行的private final class Worker implements Runnable {//这个属性代表当前时间轮的指针移动了几个刻度private long tick;@Overridepublic void run() {//给starttime赋值,这里要等待该值复制成功后,另一个线程才能继续向下执行startTime = System.nanoTime();//这里是不是就串联起来了,通知之前的线程可以继续向下运行了startTimeInitialized.countDown();do {//返回的是时间轮线程从开始工作到现在执行了多少时间了final long deadline = waitForNextTick();if (deadline > 0) {//获取要执行的定时任务的那个数组下标。就是让指针当前的刻度和掩码做位运算int idx = (int) (tick & mask);//上面已经得到了要执行的定时任务的数组下标,这里就可以得到该bucket,而这个bucket就是定时任务的一个双向链表//链表中的每个节点都是一个定时任务HashedWheelBucket bucket = wheel[idx];//在真正执行定时任务之前,把即将被执行的任务从普通任务队列中放到时间轮的数组当中transferTimeoutsToBuckets();//执行定时任务bucket.expireTimeouts(deadline);//指针已经移动了,所以加1tick++;}//暂且让时间轮线程一直循环} while (true);}
}
这里可以看出会有一个线程,来一直推动时间轮向前。并执行任务。
4.4 基于redis的实现方式
redis实现延迟队列有两种方式,分别是监听key过期和通过zset来存储调度任务。
4.4.1 监听key过期
1.实现原理
即业务系统将调度任务数据存储到redis作为key,过期时间设置为任务执行时间。并监听这些key,当key过期被删除的时候,redis回给业务系统发送通知。
2.优缺点
1.redis采用定期删除+惰性删除的方式,所以一个key计算过期,也可能不会被立即删除掉,而是等待下一次访问该key或者被redis的定时任务扫到,才会删除key,导致任务执行时间不精准。
4.4.2 基于zset存储key和执行时间实现
1.实现原理
实现原理和jdk自带的延迟队列实现原理一样,只是存储任务的数据采用Redis中的zset实现,下次需要执行的任务放在zset的首部,只需要获取首部任务元素,然后获取到该元素的过期时间,redission启动一个定时任务,阻塞线程至首部元素的执行时间,才开始执行任务,并且将其加入到一个阻塞队列中。业务系统会启动一个线程,一直监听阻塞队列,如果有数据,证明有任务到达执行时间了,便取出数据,开始执行任务。
2.Redisson的实现
1.组成组件
1.存储任务的容器:
- 一个普通的list:主要是为了保存执行任务的插入顺序,方便执行增删改操作;
- 一个zset:key为执行任务,score为任务执行时间,利用zset的排序功能zrange,可以取出执行时间最小的任务;
- blist:阻塞队列,如果执行任务到期便会被转移到阻塞队列中,业务线程会轮询阻塞队列,取出里面执行任务,完成消费逻辑。
2.执行线程:其实是redisson客户端开启的一个线程。
2.源码分析
redisson的执行逻辑其实可以分成两个层面:
1.就是上面的普通逻辑,redisson客户端会启动一个线程,一直轮询zset,取出里面的过期任务,转移到阻塞队列中。但是这里轮询并不是定时扫描,而是每次取出到期任务过后,会返回最近的下一次任务的到达时间,然后启动一个定时器,等到下一个任务执行时间到期后,才再次从redis中拉取数据,大大的减少了io操作。这一操作其实和jdk的延迟队列是一样的。
2.还有就是处理特殊场景,一是在初始化的时候,如何判断下一个任务到达时间是多少;二是在redis中已经拉取到最新的一条任务的过期时间后,有新的任务添加到redis中,而且这个新的任务的过期时间是小于以前的最近的一条任务的过期时间的。针对这两种情况,redisson采用发布订阅的思想。redisson在构造延迟队列的时候,会订阅redisson_delay_queue_channel这个channel。
每次添加任务的时候,会判断被添加任务的过期时间是不是超过zset中所有任务的过期时间,如果是,便会向redisson_delay_queue_channel发布消息,消息体包含了最近的这条任务的时间。redisson收到消息过后,会更新定时器的执行时间为最新的一条执行任务的时间。
a)构造器
protected RedissonDelayedQueue(QueueTransferService queueTransferService, Codec codec, final CommandAsyncExecutor commandExecutor, String name) {super(codec, commandExecutor, name);//创建定时任务QueueTransferTask task = new QueueTransferTask(commandExecutor.getServiceManager()) { //这个逻辑是核心的转移逻辑,就是前面说的取出zset前面100条数据,并且返回下一次任务//的执行时间protected RFuture<Long> pushTaskAsync() {return commandExecutor.evalWriteAsync(RedissonDelayedQueue.this.getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_LONG, "local expiredValues = redis.call('zrangebyscore', KEYS[2], 0, ARGV[1], 'limit', 0, ARGV[2]); if #expiredValues > 0 then for i, v in ipairs(expiredValues) do local randomId, value = struct.unpack('Bc0Lc0', v);redis.call('rpush', KEYS[1], value);redis.call('lrem', KEYS[3], 1, v);end; redis.call('zrem', KEYS[2], unpack(expiredValues));end; local v = redis.call('zrange', KEYS[2], 0, 0, 'WITHSCORES'); if v[1] ~= nil then return v[2]; end return nil;", Arrays.asList(RedissonDelayedQueue.this.getRawName(), RedissonDelayedQueue.this.timeoutSetName, RedissonDelayedQueue.this.queueName), new Object[]{System.currentTimeMillis(), 100});}//创建redisson_delay_queue_channel这个channelprotected RTopic getTopic() {return RedissonTopic.createRaw(LongCodec.INSTANCE, commandExecutor, RedissonDelayedQueue.this.channelName);}};//真正的定时任务调度转移zset任务至阻塞队列的逻辑queueTransferService.schedule(this.queueName, task);this.queueTransferService = queueTransferService;}
//可以看出,在启动这个线程的时候,会订阅redisson_delay_queue_channel这个topic
public void start() {RTopic schedulerTopic = this.getTopic();this.statusListenerId = schedulerTopic.addListener(new BaseStatusListener() {public void onSubscribe(String channel) {QueueTransferTask.this.pushTask();}});this.messageListenerId = schedulerTopic.addListener(Long.class, new MessageListener<Long>() {//当有消息到达的时候,证明此时有新的执行任务的过期时间小于zset中任务最小的过期时间public void onMessage(CharSequence channel, Long startTime) {//所以需要更新定时器中定时时间QueueTransferTask.this.scheduleTask(startTime);}});}
可以看出,在初始化的时候,redisson就订阅了redisson_delay_queue_channel这个channel,其他都是回调方法。
b)添加任务
public RFuture<Void> offerAsync(V e, long delay, TimeUnit timeUnit) {if (delay < 0L) {throw new IllegalArgumentException("Delay can't be negative");} else {long delayInMs = timeUnit.toMillis(delay);long timeout = System.currentTimeMillis() + delayInMs;byte[] random = this.getServiceManager().generateIdArray(8);//这是添加任务的核心方法,其实就是将任务添加到zset中,如果当前任务的过期时间小于zset中所有任务的过期时间,便会执行发布一条消息。return this.commandExecutor.evalWriteNoRetryAsync(this.getRawName(), this.codec, RedisCommands.EVAL_VOID, "local value = struct.pack('Bc0Lc0', string.len(ARGV[2]), ARGV[2], string.len(ARGV[3]), ARGV[3]);redis.call('zadd', KEYS[2], ARGV[1], value);redis.call('rpush', KEYS[3], value);local v = redis.call('zrange', KEYS[2], 0, 0); if v[1] == value then redis.call('publish', KEYS[4], ARGV[1]); end;", Arrays.asList(this.getRawName(), this.timeoutSetName, this.queueName, this.channelName), new Object[]{timeout, random, this.encode(e)});}}
可以看出,添加任务其实就是将任务添加到zset中,如果当前任务的过期时间小于zset中所有任务的过期时间,便会执行发布一条消息到redisson_delay_queue_channel中,触发上面回调方法QueueTransferTask.this.scheduleTask(startTime)。
c)转移任务至阻塞队列
private void scheduleTask(Long startTime) {QueueTransferTask.TimeoutTask oldTimeout = (QueueTransferTask.TimeoutTask)this.lastTimeout.get();if (startTime != null) {if (oldTimeout != null) {oldTimeout.getTask().cancel();}long delay = startTime - System.currentTimeMillis();if (delay > 10L) {//创建一个定时器,其实是由java中的timer实现的Timeout timeout = this.serviceManager.newTimeout(new TimerTask() {public void run(Timeout timeout) throws Exception {QueueTransferTask.this.pushTask();//定时器时间为当前zset中的最小时间QueueTransferTask.TimeoutTask currentTimeout = (QueueTransferTask.TimeoutTask)QueueTransferTask.this.lastTimeout.get();if (currentTimeout.getTask() == timeout) {QueueTransferTask.this.lastTimeout.compareAndSet(currentTimeout, (Object)null);}}}, delay, TimeUnit.MILLISECONDS);if (!this.lastTimeout.compareAndSet(oldTimeout, new QueueTransferTask.TimeoutTask(startTime, timeout))) {timeout.cancel();}} else {this.pushTask();}}}
其实就是创建一个定时器,定时器为当前zset中的最小时间,当定时任务到达时,执行 QueueTransferTask.this.pushTask()方法。
最终执行的其实就是前面构造函数中的pushTaskAsync方法,里面其实就是一段lua脚本:
local expiredValues = redis.call('zrangebyscore', KEYS[2], 0, ARGV[1], 'limit', 0, ARGV[2]); if #expiredValues > 0 then for i, v in ipairs(expiredValues) do local randomId, value = struct.unpack('Bc0Lc0', v);redis.call('rpush', KEYS[1], value);redis.call('lrem', KEYS[3], 1, v);end; redis.call('zrem', KEYS[2], unpack(expiredValues));end; local v = redis.call('zrange', KEYS[2], 0, 0, 'WITHSCORES'); if v[1] ~= nil then return v[2]; end return nil;
这个逻辑是核心的转移逻辑,就是前面说的取出zset前面100条数据,如果任务到期,便转移到阻塞队列中,并且返回下一次任务的执行时间。
4.5 基于RocketMq的实现方式
RocketMQ 本身不直接支持延时消息队列,但是可以通过特定的设置来实现类似的功能。在 RocketMQ 中,消息的延时级别可以在发送消息时通过设置 delayLevel来实现,delayLevel
是一个整数,表示消息延时级别,级别越高,延时越大。RocketMQ 默认定义了 18 个延时级别,级别 1 表示 1s 延时,级别 2 表示 5s 延时,依此类推,级别 18 表示 18levels 延时(level 是自定义的延时系数,默认是 1000 毫秒。在rocketmq5.0中,也支持了自定义任务执行时间的延迟队列。它本质上还是通过时间轮来实现的。
5.总结
可以看出,延迟队列主要包括两个部分,分别是存储任务的数据结构(可以是内存队列,redis,数据库,mq等),还有就是需要线程,来推送扫描队列中的任务。万变不离其宗。
相关文章:

延迟队列实现及其原理详解
1.绪论 本文主要讲解常见的几种延迟队列的实现方式,以及其原理。 2.延迟队列的使用场景 延迟队列主要用于解决每个被调度的任务开始执行的时间不一致的场景,主要包含如下场景: 1.比如订单超过15分钟后,关闭未关闭的订单。 2.比如用户可以…...

web APIs
目录 Web APIs第一天Dom获取&属性操作Web API基本认知变量声明作用和分类什么是DOMDOM树DOM对象 获取Dom对象根据CSS选择器来获取DOM元素(重点)其他获取DOM元素方法(了解) 操作元素内容对象.innerText 属性对象.innerHTML 属性…...

【Web前端概述】
HTML 是用来描述网页的一种语言,全称是 Hyper-Text Markup Language,即超文本标记语言。我们浏览网页时看到的文字、按钮、图片、视频等元素,它们都是通过 HTML 书写并通过浏览器来呈现的。 一、HTML简史 1991年10月:一个非正式…...

文献阅读:一种基于艾伦脑图谱的空间表达数据可视化、空间异质性描绘和单细胞配准工具
::: block-1 文献介绍 文献题目: AllenDigger,一种基于艾伦脑图谱的空间表达数据可视化、空间异质性描绘和单细胞配准的工具 研究团队: 王晓群(北京师范大学) 发表时间: 2023-03-16 发表期刊:…...

Redis学习笔记(三)--Redis客户端
文章目录 一、命令行客户端二、图形界面客户端1、Redis Desktop Manager2、RedisPlus 三、java代码客户端 本文参考: Redis学习汇总(已完结) Redis超详细入门教程(基础篇) Redis视频从入门到高级,redis视频…...
面试知识梳理
一、vue篇章 1.vue2和vue3性能方面的提升最主要的原因是什么? 1、1响应式的系统优化: vue3使用了es6的proxy对象来实现响应式系统,取代了vue2中基于Object.defineProperty的方法。Proxy提供了更强大和灵活的拦截能力,可以更有效地…...
Unity3D ScrollView 滚动视图组件详解及代码实现
前言 在Unity3D中,ScrollView(滚动视图)是一种常用的UI组件,它允许用户通过滚动来查看超出当前视图范围的内容。ScrollView通常用于显示长列表、大量文本或图像等。本文将详细介绍Unity3D中的ScrollView组件,并提供代…...
13.java面向对象:封装
java面向对象:封装 我们程序设计要追求“高内聚,低耦合”。高内聚就是类的内部数据操作细节自己完成,不允许外部干涉;低耦合:仅暴露少量的方法给外部使用。 封装(数据的隐藏)通常应禁止直接访问一个对象中…...

记录:网鼎杯2024赛前热身CRYPT01密码学
题目 下载并打开附件 判断为凯撒密码,尝试移位解密 在第10位发现flag字样 提交得分 解密脚本为个人自用,因比赛未结束故不开源...
GitHub加速
GitHub加速 终端命令行 支持终端命令行 git clone , wget , curl 等工具下载. 支持 raw.githubusercontent.com , gist.github.com , gist.githubusercontent.com 文件下载.注意:不支持 SSH Key 方式 git clone 下载. git clone git clone https://ghp.ci/https:…...
每天学习一个Linux命令:xrandr
xrandr 是一个用于在 X Window 系统中管理显示器的命令行工具。它可以用来设置显示器的分辨率、刷新率、旋转方向和连接状态等。下面是 xrandr 的详细用法和案例。 基本用法 xrandr [选项]常用选项 -q 或 --query: 查询当前显示器的状态。-s 或 --size: 设置显示器的分辨率。…...

路由表来源(基于华为模拟器eNSP)
概叙 在交换网络中,若要实现不同网段之间的通信,需要依靠三层设备(路由器、三层交换机等),而路由器只知道其直连网段的路由条目,对于非直连的网段,在默认情况下,路由器是不可达的&a…...
并查集(Union-Find)
并查集(Disjoint Set,也称为Union-Find数据结构)是一种用于高效处理不相交集(即集合内元素互相独立,没有交集)的数据结构。它主要用于解决以下两种操作: 查找(Find)&…...
Linux上的AI框架都有哪些?哪些AI框架适合驱动EACO地球链自动发展完善?
Linux上的AI框架种类繁多,涵盖了深度学习、机器学习、自然语言处理等多个领域。以下是一些常用的AI框架: 深度学习框架 Deeplearning4j 简介:Deeplearning4j(Deep Learning For Java)是Java和Scala环境下的一个开源分…...
java的第一个游戏界面
看视频02_大鱼吃小鱼_添加背景图_尚学堂_哔哩哔哩_bilibili 学习方法: 就对的视频小代码,书籍没有,遇到不懂的问ai 今日成果, 界面代码 package new_gameobj;import java.awt.Graphics; import java.awt.Image; import java.…...

【AIGC】ChatGPT提示词Prompt高效编写模式:Self-ask Prompt、ReACT与Reflexion
博客主页: [小ᶻZ࿆] 本文专栏: AIGC | ChatGPT 文章目录 💯前言💯自我提问 (Self-ask Prompt)如何工作应用实例优势结论 💯协同思考和动作 (ReACT)如何工作应用实例优势结论 💯失败后自我反思 (Reflexion)如何工作…...
android studio无法下载依赖包问题
新建Flutter项目Android项目后,点击运行出现报错! error.png 这是镜像站点无法访问造成的!只需要修改为国内可访问的站点即可。 第一步:修改项目Android目录下的build.gradle buildscript { ext.kotlin_version 1.3.50 repositorie…...

SQL入门
一、SQL 语言概述 数据库就是指数据存储的库,作用就是组织数据并存储数据,数据库如按照:库 -> 表 -> 数据三个层级进行数据组织,而 SQL 语言,就是一种对数据库、数据进行操作、管理、查询的工具,通过…...
Java中的Math类
关于Math类的介绍,这是一个在Java和其他许多编程语言中常见的内置库或模块,主要用于提供各种数学运算的方法。在Java中,Math类位于java.lang包下,它包含大量静态方法执行基本的数学函数,如三角函数、指数函数、对数函数…...

大厂常问iOS面试题–Runloop篇
大厂常问iOS面试题–Runloop篇 一.RunLoop概念 RunLoop顾名思义就是可以一直循环(loop)运行(run)的机制。这种机制通常称为“消息循环机制” NSRunLoop和CFRunLoopRef就是实现“消息循环机制”的对象。其实NSRunLoop本质是由CFRunLoopRef封装的,提供了面向对象的AP…...

深入浅出Asp.Net Core MVC应用开发系列-AspNetCore中的日志记录
ASP.NET Core 是一个跨平台的开源框架,用于在 Windows、macOS 或 Linux 上生成基于云的新式 Web 应用。 ASP.NET Core 中的日志记录 .NET 通过 ILogger API 支持高性能结构化日志记录,以帮助监视应用程序行为和诊断问题。 可以通过配置不同的记录提供程…...

装饰模式(Decorator Pattern)重构java邮件发奖系统实战
前言 现在我们有个如下的需求,设计一个邮件发奖的小系统, 需求 1.数据验证 → 2. 敏感信息加密 → 3. 日志记录 → 4. 实际发送邮件 装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其…...

[ICLR 2022]How Much Can CLIP Benefit Vision-and-Language Tasks?
论文网址:pdf 英文是纯手打的!论文原文的summarizing and paraphrasing。可能会出现难以避免的拼写错误和语法错误,若有发现欢迎评论指正!文章偏向于笔记,谨慎食用 目录 1. 心得 2. 论文逐段精读 2.1. Abstract 2…...
将对透视变换后的图像使用Otsu进行阈值化,来分离黑色和白色像素。这句话中的Otsu是什么意思?
Otsu 是一种自动阈值化方法,用于将图像分割为前景和背景。它通过最小化图像的类内方差或等价地最大化类间方差来选择最佳阈值。这种方法特别适用于图像的二值化处理,能够自动确定一个阈值,将图像中的像素分为黑色和白色两类。 Otsu 方法的原…...
Qt Http Server模块功能及架构
Qt Http Server 是 Qt 6.0 中引入的一个新模块,它提供了一个轻量级的 HTTP 服务器实现,主要用于构建基于 HTTP 的应用程序和服务。 功能介绍: 主要功能 HTTP服务器功能: 支持 HTTP/1.1 协议 简单的请求/响应处理模型 支持 GET…...

Python爬虫(一):爬虫伪装
一、网站防爬机制概述 在当今互联网环境中,具有一定规模或盈利性质的网站几乎都实施了各种防爬措施。这些措施主要分为两大类: 身份验证机制:直接将未经授权的爬虫阻挡在外反爬技术体系:通过各种技术手段增加爬虫获取数据的难度…...

Linux --进程控制
本文从以下五个方面来初步认识进程控制: 目录 进程创建 进程终止 进程等待 进程替换 模拟实现一个微型shell 进程创建 在Linux系统中我们可以在一个进程使用系统调用fork()来创建子进程,创建出来的进程就是子进程,原来的进程为父进程。…...

网站指纹识别
网站指纹识别 网站的最基本组成:服务器(操作系统)、中间件(web容器)、脚本语言、数据厍 为什么要了解这些?举个例子:发现了一个文件读取漏洞,我们需要读/etc/passwd,如…...
Java数值运算常见陷阱与规避方法
整数除法中的舍入问题 问题现象 当开发者预期进行浮点除法却误用整数除法时,会出现小数部分被截断的情况。典型错误模式如下: void process(int value) {double half = value / 2; // 整数除法导致截断// 使用half变量 }此时...

LLMs 系列实操科普(1)
写在前面: 本期内容我们继续 Andrej Karpathy 的《How I use LLMs》讲座内容,原视频时长 ~130 分钟,以实操演示主流的一些 LLMs 的使用,由于涉及到实操,实际上并不适合以文字整理,但还是决定尽量整理一份笔…...