共计 4857 个字符,预计需要花费 13 分钟才能阅读完成。
需要剖析:
24 小时内未领取的订单过期生效。
解决方案
- 被动设置:在查问订单的时候查看是否过期并设置过期状态。
- 定时调度:定时器定时查问并过期须要过期的订单。
- 延时队列:将未领取的订单放入一个延时队列中,顺次取出过期订单。
- 过期揭示 :reids 反对将一个过期的 key(订单号) 告诉给客户端,依据过期的订单号进行相应的解决。
1. 被动设置
这个太简略了,就是在查问的时候判断是否生效,如果生效了就给他设置生效状态。然而弊病也很显著,每次查问都要对未生效的订单做判断,如果用户不查问,订单就不生效,那么如果有相似统计生效状态个数的性能,将会受到影响,所以只能实用于简略独立的场景。几乎 low 爆了。
2. 定时调度
这种是常见的办法,利用一个定时器,在设置的周期内轮询查看并解决须要过期的订单。
具体实现有基于 Timer
的, 有基于 Quartz
, 还有 springboot 自带的Scheduler
,实现起来比较简单。
就写一下第三个的实现办法吧:
- 启动类加上注解
@EnableScheduling
- 新建一个定时调度类,办法上加上
@Scheduled
注解,如下图那么简略。
弊病
- 不可能精准的去解决过期订单,轮询周期设置的越小,精准度越高,然而我的项目的压力越大,咱们上一个我的项目就有这种情况,太多定时器在跑,我的项目运行起来比拟轻便。
- 而且须要解决的是过期的订单,然而要查问所有未领取的订单,范畴大。对于大订单量的操作不适合。
3. 延时队列
基于 JDK 的实现办法,将未领取的订单放到一个有序的队列中,程序会主动顺次取出过期的订单。
如果以后没有过期的订单,就会阻塞,直至有过期的订单。因为每次只解决过期的订单,并且解决的工夫也很精准,不存在定时调度计划的那两个弊病。
实现:
1. 首先创立一个订单类 OrderDelayDto
须要实现 Delayed
接口。而后重写 getDelay()
办法和 compareTo()
办法,只加了订单编号和过期工夫两个属性。
这两个办法很重要,getDelay()
办法实现过期的策略,比方,订单的过期工夫等于以后工夫就是过期,返回正数就代表须要解决。否则不解决。compareTo()
办法实现订单在队列中的排序规定,这样即便前面退出的订单,也能退出到排序中,我这里写的规定是依照过期工夫排序,最先过期的排到最后面,这一点很重要,因为排在最后面的如果没有被解决,就会进入阻塞状态,前面的不会被解决。
import lombok.Data;
import java.util.Date;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
/**
* @author mashu
* Date 2020/5/17 16:25
*/
@Data
public class OrderDelayDto implements Delayed {
/**
* 订单编号
*/
private String orderCode;
/**
* 过期工夫
*/
private Date expirationTime;
/**
* 判断过期的策略:过期工夫大于等于以后工夫就算过期
*
* @param unit
* @return
*/
@Override
public long getDelay(TimeUnit unit) {return unit.convert(this.expirationTime.getTime() - System.currentTimeMillis(), TimeUnit.NANOSECONDS);
}
/**
* 订单退出队列的排序规定
*
* @param o
* @return
*/
@Override
public int compareTo(Delayed o) {OrderDelayDto orderDelayDto = (OrderDelayDto) o;
long time = orderDelayDto.getExpirationTime().getTime();
long time1 = this.getExpirationTime().getTime();
return time == time1 ? 0 : time < time1 ? 1 : -1;
}
}
其实这样曾经算是写好了。我没有耍你。
写个 main 办法测试一下,创立两个订单 o1 和 o2,放入到延时队列中,而后 while()办法一直的去取。
在此办法内通过队列的 take()
办法取得已过期的订单,而后做出相应的解决。
public static void main(String[] args) {DelayQueue<OrderDelayDto> queue = new DelayQueue<>();
OrderDelayDto o1 = new OrderDelayDto();
// 第一个订单,过期工夫设置为一分钟后
o1.setOrderCode("1001");
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.MINUTE, 1);
o1.setExpirationTime(calendar.getTime());
OrderDelayDto o2 = new OrderDelayDto();
// 第二个订单,过期工夫设置为当初
o2.setOrderCode("1002");
o2.setExpirationTime(new Date());
// 往队列中放入数据
queue.offer(o1);
queue.offer(o2);
// 延时队列
while (true) {
try {OrderDelayDto take = queue.take();
System.out.println("订单编号:" + take.getOrderCode() + "过期工夫:" + take.getExpirationTime());
} catch (InterruptedException e) {e.printStackTrace();
}
}
}
运行后果:
我成心把第二个订单的过期工夫设置为第一个订单之前,从后果能够看出,他们曾经主动排序把最先过期的排到了最后面。
第一个订单的生效工夫是以后工夫的后一分钟,后果也显示一分钟后处理了第一条订单。
2. 然而通常状况下,咱们会应用多线程去取延时队列中的数据,这样即便线程启动之后也能动静的向队列中增加订单。
创立一个线程类 OrderCheckScheduler
实现 Runnable
接口,
增加一个延时队列属性,重写 run()
办法,在此办法内通过队列的 take()
办法取得已过期的订单,而后做出相应的解决。
import java.util.concurrent.DelayQueue;
/**
* @author mashu
* Date 2020/5/17 14:27
*/
public class OrderCheckScheduler implements Runnable {
// 延时队列
private DelayQueue<OrderDelayDto> queue;
public OrderCheckScheduler(DelayQueue<OrderDelayDto> queue) {this.queue = queue;}
@Override
public void run() {while (true) {
try {OrderDelayDto take = queue.take();
System.out.println("订单编号:" + take.getOrderCode() + "过期工夫:" + take.getExpirationTime());
} catch (InterruptedException e) {e.printStackTrace();
}
}
}
}
好了,写个办法测试一下:
public static void main(String[] args) {
// 创立延时队列
DelayQueue<OrderDelayDto> queue = new DelayQueue<>();
OrderDelayDto o1 = new OrderDelayDto();
// 第一个订单,过期工夫设置为一分钟后
o1.setOrderCode("1001");
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.MINUTE, 1);
o1.setExpirationTime(calendar.getTime());
OrderDelayDto o2 = new OrderDelayDto();
// 第二个订单,过期工夫设置为当初
o2.setOrderCode("1002");
o2.setExpirationTime(new Date());
// 运行线程
ExecutorService exec = Executors.newFixedThreadPool(1);
exec.execute(new OrderCheckScheduler(queue));
// 往队列中放入数据
queue.offer(o1);
queue.offer(o2);
exec.shutdown();}
后果和下面的一样,图就不截了,置信我。
过期揭示
基于 redis 的过期揭示性能,听名字就晓得这个计划最是纯洁、最间接的,就是单纯解决过期的订单。
批改个 redis 的配置吧先,因为 redis 默认不开启过期揭示。notify-keyspace-events
改为 notify-keyspace-events "Ex"
写一个类用来接管来自 redis 的暖心揭示 OrderExpirationListener
, 继承一下KeyExpirationEventMessageListener
抽象类。重写 onMessage()
办法,在此办法中解决接管到的过期 key.
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* @author mashu
* Date 2020/5/17 23:01
*/
@Component
public class OrderExpirationListener extends KeyExpirationEventMessageListener {public OrderExpirationListener(RedisMessageListenerContainer listenerContainer) {super(listenerContainer);
}
@Override
public void onMessage(Message message, byte[] pattern) {final String expiredKey = message.toString();
System.out.println("我过期了" + expiredKey+"以后工夫:"+new Date());
}
}
ok,向 redis 中存入一个订单,过期工夫为 1 分钟。
redis.set("orderCode/10010", "1", 1L, TimeUnit.MINUTES);
System.out.println("redis 存入订单号 key: orderCode/10010,value:1, 过期工夫一分钟,以后工夫"+new Date());
运行后果:
除此之外还有用到音讯队列的。夜深了,我得玩会游戏了。
没有相对的好计划,只有在不同场景下的更适合的计划。随着需要的变动,技术的变革,计划也会一直的被优化和迭代,惟一不变的是工资。