平常我们都用过淘宝,京东这些电商平台,同时肯定也在这些平台上面下过单,这种情况不保证大家都有遇到过,但做开发的,肯定也知道有这个环节的存在:确认货品配置无误之后,我们都会点击购买,随之而来的就是一个结算页,让你确认商品信息、收货地址、价格等信息,但是如果这个时候,我们退出这个结算页,那此时,这个订单就属于生成但未支付的状态,对应的电商平台就会对这样的订单做延时任务取消操作,这样就可以稳定商品库存和提供用户一定的犹豫时间,那从技术的角度来看,这个延时任务是怎么实现的?
看着上面的案例,有的人会问:这个简单,我们直接用定时任务,做个半小时后的查询数据库来判断是该取消了还是怎么样不就行了吗?
其实这个场景,我们就不能说是定时任务了,准确来说是延时任务,那什么是延时呢?延时就是从一个点开始后的多久后再执行,比方说:火车八点到站,但是晚点十分钟,是那就是八点后的十分钟
上面的解答,概念有点模棱两可,和定时任务不是特别能区分开,那接下来我们就介绍一下定时和延时任务的区别
1、定时任务有明确的触发事件,而延迟任务没有
举例:火车行程表
进入火车站,我们都会看到大屏上显示各车次的出发时间、始发地、终点站等等信息定时任务:火车半小时一列,出发时间就是每天0点开始算,0:30一班,1:00一班,
哪怕第一趟火车0:10到站,那第二趟火车依旧是0:30到站延时任务:第二趟火车是在第一趟火车到站之后的半小时后一班,比如第一班火车晚点了,
是0:10分到站,那第二趟火车就是0:40到站,由前面那趟火车到站时间为起点后的半小时作为第二趟火车的到站时间2、定时任务有执行周期,而延时任务是在条件满足之后的一段时间后再执行,没有执行周期
举例:站岗换班
定时任务:晚上22点开始站岗,每半小时换一次人,那接下来就是22:30、23:00、23:30...没半小时换一个人,周期是固定的,半小时一换,硬性要求,半小时必换班
延时任务:同样是晚上22点开始站岗,每半小时换一次人,假设第一个人站岗已经两个小时了,需要第二个人来替班,这个时候第二个人才会来替班,第二个人替班的前提是:第一个人满足半小时站岗时间并且累了要求换人,而且第二个人替班时间是:0点,并非22:30,要求是半小时一换,但是有个前提,如果第一个人超过半小时而且不换,那就没有执行周期了,第二个人就可以摸鱼了
3、定时任务一般执行的都是多个任务,而延时任务一般来说都是单个任务
举例:订单取消
定时任务:如果用定时任务来取消订单,如果定死是30分钟整,不考虑秒的话,那可能到了半小时之后,会取消多个不同秒下单的订单,这个场景用代码来实现的话,最常见的就是把数据库订单的创建时间批量查出来,只精确到分,>30分钟的都要取消的,这取消的可能是大批量的数据和任务
延时任务:同样是半小时整,但因为上面1和2的条件,延时任务针对的是每笔订单下单未支付后的半小时内的订单取消,他自然而然会把秒考虑进去,比如,9:00:55这个时间下单未支付,那取消订单会在9:30:55取消,同时我9:01:02下了另一个单,但未支付,那取消这笔订单的时间就是9:31:02
那接下来我们正式来介绍一下有多少种常见的延时方案
实现思路
一般来说会直接用定时框架quartz来实现,通过一个线程定时的去请求数据库,通过订单的创建时间计算出有没有超过规定时间没支付的订单,如果有就update,没有就等下一轮轮询
【优点】
相对简单,支持集成多线程
【缺点】
1、对服务器消耗比较大
2、可能会存在延迟
3、扫描的如果是订单这种大量数据的表,那频繁的请求数据库,数据库的压力也不小
实现思路
利用JDK提供的DelayQueue队列来实现,它是一个无界阻塞队列,啥意思呢,就是说这个队列只有在延迟期满了之后才能从里面获取元素
代码Demo
1、定义订单OrderDelay来继承Delayed
public class OrderDelay implements Delayed { private String orderNo; private long timeout; OrderDelay(String orderNo, long timeout) { this.orderNo = orderNo; //规定超时的时间+订单创建时间(现在)时间戳 this.timeout = timeout + System.nanoTime(); } /** * @Description: 时间比对 */ public int compareTo(Delayed other) { if (other == this) return 0; OrderDelay t = (OrderDelay) other; //设定的有效时间-当前系统时间是否是小于等于0 long d = (getDelay(TimeUnit.NANOSECONDS) - t.getDelay(TimeUnit.NANOSECONDS)); return (d == 0) ? 0 : ((d < 0) ? -1 : 1); } // 返回距离你自定义的超时时间还有多少 public long getDelay(TimeUnit unit) { return unit.convert(timeout - System.nanoTime(), TimeUnit.NANOSECONDS); } public void print() { System.out.println("订单号为:" + orderNo + "的订单即将被取消支付"); } }
2、编写main方法
public static void main(String[] args) { List<String> list = new ArrayList<String>(); //模拟订单号 list.add("DD111111"); list.add("DD222222"); list.add("DD333333"); list.add("DD444444"); list.add("DD555555"); //存入队列 DelayQueue<OrderDelay> queue = new DelayQueue<OrderDelay>(); long start = System.currentTimeMillis(); for (int i = 0; i < 5; i++) { //延迟三秒取消订单 queue.put(new OrderDelay(list.get(i),TimeUnit.NANOSECONDS.convert(3, TimeUnit.SECONDS))); try { queue.take().print(); System.out.println("删除时间:" +(System.currentTimeMillis() - start) + "s"); System.out.println("====================================================="); } catch (InterruptedException e) { e.printStackTrace(); } } }
3、观察控制台输出
订单号为:DD111111的订单即将被取消支付 删除时间:3005s ===================================================== 订单号为:DD222222的订单即将被取消支付 删除时间:6018s ===================================================== 订单号为:DD333333的订单即将被取消支付 删除时间:9027s ===================================================== 订单号为:DD444444的订单即将被取消支付 删除时间:12042s ===================================================== 订单号为:DD555555的订单即将被取消支付 删除时间:15058s =====================================================
看时间其实是可以知道的,两个相邻的订单被取消的时间都在3秒,每3秒取消一笔订单
【优点】
效率高,延迟低
【缺点】
1、服务器重启或者宕机,数据会全部丢失
2、集群拓展困难
3、如果下单未付款的订单数太多,会出现OOM的异常
4、代码复杂度较高,上面的案例较为简单,可以实现一些指定的简单场景
实现逻辑
可以用时钟来理解,我们看一下较长的时针,时针按照某一个方向按固定的速度或频率转动,每一次刻度是一个tick,所以就引出了时间轮的三个概念:ticksPerWheel(一轮的tick数),tickDuration(一个tick的持续时间)和timeUnit(时间单位),怎么理解这三个概念呢?比如:如果ticksPerWheel=60,tickDuration=1,timeUnit=秒,那就和我们平常认知里的时钟的时针转动的速度是一样的了
结合案例说一下:如果指针指到2那个刻度,同时一个任务需要4秒以后执行,那么这个执行的线程回调会放在6上面,如果需要20s之后执行会咋样呢,由于这个始终只有12个,如果是20s,就转一圈再转一圈,20个刻度停止,也就是10点的那个位置
代码Demo
1、使用netty提供的HashedWheelTimer来实现。先添加Netty依赖
<dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.42.Final</version> </dependency>
2、创建一个定时任务
public class HashedWheelTimerTest { static class MyTimerTask implements TimerTask { boolean flag; public MyTimerTask(boolean flag) { this.flag = flag; } //要执行延时的逻辑 public void run(Timeout timeout) { System.out.println("正在取消订单的路上..."); this.flag = false; } } public static void main(String[] argv) { MyTimerTask timerTask = new MyTimerTask(true); Timer timer = new HashedWheelTimer(); //三个参数分别是,定时要实现的定时任务/延时时长/时间单位(这儿是秒) timer.newTimeout(timerTask, 5, TimeUnit.SECONDS); int i = 1; while (timerTask.flag) { try { //延时1s,1s打印一次,上面的延时是五秒后 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("已经过去了" + i + "秒"); i++; } } }
3、Timer中的newTimeout讲解
public Timeout newTimeout(TimerTask task, long delay, TimeUnit unit) { if (task == null) { throw new NullPointerException("task"); } else if (unit == null) { throw new NullPointerException("unit"); } else { //任务数,相当于线程池的大小 long pendingTimeoutsCount = this.pendingTimeouts.incrementAndGet(); //不能超过最大任务数 if (this.maxPendingTimeouts > 0L && pendingTimeoutsCount > this.maxPendingTimeouts) { this.pendingTimeouts.decrementAndGet(); throw new RejectedExecutionException("Number of pending timeouts (" + pendingTimeoutsCount + ") is greater than or equal to maximum allowed pending timeouts (" + this.maxPendingTimeouts + ")"); } else { //开始上面demo中的任务 this.start(); //计算是否超时 long deadline = System.nanoTime() + unit.toNanos(delay) - this.startTime; if (delay > 0L && deadline < 0L) { deadline = 9223372036854775807L; } //添加到timeouts队列,这是一个MPSC队列,MPSC队列是多生产者单消费者无锁的并发队列,worker线程会对队列进行消费 HashedWheelTimer.HashedWheelTimeout timeout = new HashedWheelTimer.HashedWheelTimeout(this, task, deadline); this.timeouts.add(timeout); return timeout; } } }
4、最后打印
已经过去了1秒 已经过去了2秒 已经过去了3秒 已经过去了4秒 已经过去了5秒 正在取消订单的路上... 已经过去了6秒
第五秒之后会去执行取消订单的操作
【优点】
1、效率比较高,整体来说比JDK提供的DeplayQueue效率要好
【缺点】
1、服务重启或宕机,数据会丢失
2、对集群拓展不太友好
3、和JDK一样,会出现OOM异常
实现思路
使用Redis五大数据类型中的zset类型,它是一个有序集合,每一个元素都有一个score,通过这个score排序来获取集合中的值,就取消订单来说,我们将订单超时的时间戳与订单号分别存放到score和member,然后让系统扫描第一个元素是否超时,这个相对简单,不讲了,说一个更好玩的思路
代码Demo
1、编写代码
public class ZsetTest { //创建redis实例 private static JedisPool jedisPool = new JedisPool("127.0.0.1", 6379); public static Jedis getJedis() { return jedisPool.getResource(); } //生产者,生成5个订单放进去 public void productionDelayMessage(){ for(int i=0;i<5;i++){ Calendar cal1 = Calendar.getInstance(); //延迟3秒 cal1.add(Calendar.SECOND, 3); int second3later = (int) (cal1.getTimeInMillis() / 1000); // //ket score member ZsetTest.getJedis().zadd("OrderNo",second3later,"ORDER0000001"+i); System.out.println(System.currentTimeMillis()+"ms:Redis生成了一个订单任务:订单号为"+"ORDER0000001"+i); } } //消费者,获取订单 public void consumerDelayMessage(){ Jedis jedis = ZsetTest.getJedis(); while(true){ //扫描key下面所有数据 Set<Tuple> items = jedis.zrangeWithScores("OrderNo", 0, 1); if(items == null || items.isEmpty()){ System.out.println("当前没有需要等待执行的任务"); try { //等待500ms Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } continue; } //获取score int score = (int) ((Tuple)items.toArray()[0]).getScore(); Calendar cal = Calendar.getInstance(); //计算现在的时间戳 int nowSecond = (int) (cal.getTimeInMillis() / 1000); //对比,如果创建的时间戳已经不再规定的时间范围内,就是过期了 if(nowSecond >= score){ String orderNo = ((Tuple)items.toArray()[0]).getElement(); //移除订单 jedis.zrem("OrderNo", orderNo); System.out.println(System.currentTimeMillis() +"ms:Redis消费了一个订单任务:消费的订单号为"+orderNo); } } } public static void main(String[] args) { ZsetTest zsetTest =new ZsetTest(); //生成订单 zsetTest.productionDelayMessage(); //获取订单,并取消订单 zsetTest.consumerDelayMessage(); } }
2、打印结果
1650851642873ms:Redis生成了一个订单任务:订单号为ORDER00000010 1650851642875ms:Redis生成了一个订单任务:订单号为ORDER00000011 1650851642876ms:Redis生成了一个订单任务:订单号为ORDER00000012 1650851642877ms:Redis生成了一个订单任务:订单号为ORDER00000013 1650851642878ms:Redis生成了一个订单任务:订单号为ORDER00000014 1650851645000ms:Redis消费了一个订单任务:消费的订单号为ORDER00000010 1650851645001ms:Redis消费了一个订单任务:消费的订单号为ORDER00000011 1650851645001ms:Redis消费了一个订单任务:消费的订单号为ORDER00000012 1650851645001ms:Redis消费了一个订单任务:消费的订单号为ORDER00000013 1650851645002ms:Redis消费了一个订单任务:消费的订单号为ORDER00000014 当前没有需要等待执行的任务 当前没有需要等待执行的任务
3、我这里可能打印结果体验不出来接下来要说的问题,那就是多线程会消费同一个资源,那怎么解决嘞?方案就是:对zrem返回值进行判断,>0的时候消费数据,否则不消费,我们修改一下上面demo代码中取消订单的if判断中的逻辑,修改后的代码如下:
if(nowSecond >= score){ String orderNo = ((Tuple)items.toArray()[0]).getElement(); //移除订单 Long no = jedis.zrem("OrderNo", orderNo); if(no!=null && no>0){ System.out.println(System.currentTimeMillis() +"ms:Redis消费了一个订单任务:消费的订单号为"+orderNo); } }
【优点】
1、集群拓展相当友好
2、时间准确度较高
3、数据不易丢失,如果服务出现重启或宕机,当服务启动的时候,会有重新处理数据的可能
【缺点】
需要对应的Redis维护的能力
实现原理
RabbitMQ自身是可以实现延迟队列,RabbitMQ可以针对Queue和Message设置 x-message-tt,来控制消息的生存时间,如果超时,则消息变为dead letter
代码Demo
具体的延时队列的使用介绍和代码,请移驾~到下面的链接,专门为这一个需求写的文章
【RabbitMQ】延迟队列_阿小冰的博客-程序员秘密【RabbitMQ】延迟队列
https://blog.csdn.net/qq_38377525/article/details/124989700
【优点】
1、效率较高
2、有关RabbitMQ的横向拓展也十分友好
3、支持数据持久化
【缺点】
需要对RabbitMQ的安装配置和API要有一定的功底
UE4中Crypto++库加密解密第三节:UE4中实现PBKDF2加密验证文章目录UE4中Crypto++库加密解密前言代码1. C++代码2. 蓝图测试结果参考前言通过哈希算法进行加密。因为哈希算法是单向的,可以将任何大小的数据转化为定长的“指纹”,而且无法被反向计算。另外,即使数据源只改动了一丁点,哈希的结果也会完全不同。这样的特性使得它非常适合用于保存密码,因为我们需要加密后的密码无法被解密,同时也能保证正确校验每个用户的密码。但是哈希加密可以通过字典攻击和暴力攻击破解。密码加盐。盐是一
欢迎添加微信互相交流学习哦!项目源码:https://gitee.com/oklongmm/biye
一、容器简化了程序员自身的多线程编程。 各种Web容器,如Tomcat,Resion,Jetty等都有自己的线程池(可在配置文件中配置),所以在客户端进行请求调用的时候,程序员不用针对Client的每一次请求,都新建一个线程。而容器会自动分配线程池中的线程,提高访问速度。 二、Tomcat线程池实现:1、使用APR的Pool技术,使用了JNI。
最近在项目当中遇到了一个问题,项目用到了springMVC的模式,本来
最近项目需要实现在Windows上进行文件打包,熟悉squashfs文件系统的都知道,是为Linux打造的文件系统,在Linux上也有相应的工具,安装后用命令即可实现打包。那么,Windows下如何呢?经过查找资源,发现大神做出了Windows下的exe文件。具体文件放在文末链接处。用法如下1 将文件解压到Windows可执行目录下2 通过win+R,然后cmd进入解压的当前目录3 执行mksquashfs.exe 文件1 ...... 目标文件目录进行打包 ...
ngx_http_headers_module模块一. 前言ngx_http_headers_module模块提供了两个重要的指令add_header和expires,来添加 “Expires” 和 “Cache-Control” 头字段,对响应头添加任何域字段。add_header可以用来标示请求访问到哪台服务器上,这个也可以通过nginx模块nginx-http-footer-filter研究...
分别将两个图像或两个音频做FFT,再选取图像1/音频1的幅度,结合上图像2/音频2的相位,再做IFFT。看返回的图像或音频更接近图像1/音频1,还是图像2/音频2。若是前者,则说明幅度的信息量更重要;若是后者,则说明相位的信息量更重要。
Matlab-拓扑图绘制(1)效果图话不多说,先上效果图:这是8个节点的全连接拓扑,即任意两点之间均连接,但连接的权重不同,在图上主要呈现的是拓扑连接关系:全连接标记节点的名称:1, 2,…, 8标记连接权重:0.28125, 0.42188,…连接权重越大,连接线越粗连接线的线型设置:虚线代码clcclearvarsclose alltic%==== 构建邻接矩阵g ====%n = 8; % 节点个数g = ones(n) -
首先要感谢“笔尖bj” 提供的代码分享:https://blog.csdn.net/u013165921/article/details/79380097 在他的前文中已经讲解了菜单栏、工具栏、任务栏的实现方法。我直接用他的代码发现他用的信号和槽的编程方法还是Qt4版的编程方法,我更新为Qt5中常用的编程方式。除了信号和槽以外,我对另存方法做了一点优化,另外在每个调用函数中添加了测试语...
1. 参加该课程后,选择较靠前的开课期数,比如第一次开课,即可查看课程相应视频。2.使用知乎某位大佬制作的视频查询工具,可查询出视频链接。但有时会查不出来,不太全面。 操作方法:打开慕课网---->搜索想要查看的课程--->在课程名上右键“复制链接地址”---->将该复制内容粘贴到工具内的搜索框中--->点击查询http://tools.antlm.com/...
一、介绍这是基于lombok.jar以及相应的注解来做的。二,安装使用https://jingyan.baidu.com/article/22fe7cede363d83002617f3a.html或者网上使用lombok.jar的安装使用教程。需要注意的是,下图的打开方式也是可以的(javaTM)三、关于该jar包的使用https://www.cnblogs.com/heyonggan...
2004年4月20日最新版本的GCC编译器3.4.0发布了。目前,GCC可以用来编译C/C++、FORTRAN、JAVA、OBJC、ADA等语言的程序,可根据需要选择安装支持的语言。GCC 3.4.0比以前版本更好地支持了C++标准。本文以在Redhat Linux上安装GCC3.4.0为例,介绍了GCC的安装过程。安装之前,系统中必须要有cc或者gcc等编译器,并且是可用的,或者用环境变量CC指...