24小时内未支付的订单过期失效。
这个太简单了,就是在查询的时候判断是否失效,如果失效了就给他设置失效状态。但是弊端也很明显,每次查询都要对未失效的订单做判断,如果用户不查询,订单就不失效,那么如果有类似统计失效状态个数的功能,将会受到影响,所以只能适用于简单独立的场景。简直low爆了。
这种是常见的方法,利用一个定时器,在设置的周期内轮询检查并处理需要过期的订单。
具体实现有基于Timer
的,有基于Quartz
,还有springboot自带的Scheduler
,实现起来比较简单。
就写一下第三个的实现方法吧:
@EnableScheduling
@Scheduled
注解,如下图那么简单。
弊端
基于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());
运行结果:
除此之外还有用到消息队列的。夜深了,我得玩会游戏了。
没有绝对的好方案,只有在不同场景下的更合适的方案。随着需求的变化,技术的革新,方案也会不断的被优化和迭代,唯一不变的是工资。