日常开发中,我们经常遇到这种业务场景,如:外卖订单超 30 分钟未支付,则自动取订单;用户注册成功 15 分钟后,发短信息通知用户等等。这就是延时任务处理场景。
在电商,支付等系统中,一设都是先创建订单(支付单),再给用户一定的时间进行支付,如果没有按时支付的话,就需要把之前的订单(支付单)取消掉。这种类以的场景有很多,还有比如到期自动收货,超时自动退款,下单后自动发送短信等等都是类似的业务问题。
定时任务(数据库轮询)
通过定时任务关闭订单,是一种成本很低,实现也很容易的方案。通过简单的几行代码,写一个定时任务,定期扫描数据库中的订单,如果时间过期,就将其状态更新为关闭即可。
java
@Scheduled(cron = "0 0 22 * * ?") public void pmNotify() { this.pmService.todoNotify(); }
优点:实现容易,成本低,基本不依赖其他组件。
缺点:
- 时间可能不够精确。由于定时任务扫描的间隔是固定的,所以可能造成一些订单已经过期了一段时间才被扫描到,订单关闭的时间比正常时间晚一些。
- 增加了数据库的压力。随着订单的数量越来越多,扫描的成本也会越来越大,执行时间也会被拉长,可能导致某些应该被关闭的订单迟迟没有被关闭。
总结:采用定时任务的方案比较适合对时间要求不是很敏感,并且数据量不太多的业务场景。
JDK 延迟队列 DelayQueue
DelayQueue是JDK提供的一个无界队列,DelayQueue队列中的元素需要实现Delayed,它只提供了一个方法,就是获取过期时间。
用户的订单生成以后,设置过期时间比如30分钟,放入定义好的DelayQueue,然后创建一个线程,在线程中通过while(true)不断的从DelayQueue中获取过期的数据。
优点:不依赖任何第三方组件,连数据库也不需要了,实现起来也方便。
缺点:
- 因为DelayQueue是一个无界队列,如果放入的订单过多,会造成JVMOOM。
- DelayQueue基于JVM内存,如果JVM重启了,那所有数据就丢失了。
- 创建了一个不断while(true)的线程,占用了cpu资源
总结:DelayQueue适用于数据量较小,且丢失也不影响主业务的场景,比如内部系统的一些非重要通知,就算丢失,也不会有太大影响。
redis过期监听
redis 是一个高性能的KV 数据库,除了用作缓存以外,其实还提供了过期监听的功能。在redis.conf 中,配置notify-keyspace-events Ex 即可开启此功能。然后在代码中继承KeyspaceEventMessageListener,实现onMessage 就可以监听过期的数据量。
java
public abstract class KeyspaceEventMessageListener implements MessageListener, InitializingBean, DisposableBean { private static final Topic TOPIC_ALL_KEYEVENTS = new PatternTopic("__keyevent@*"); //...省略部分代码 public void init() { if (StringUtils.hasText(keyspaceNotificationsConfigParameter)) { RedisConnection connection = listenerContainer.getConnectionFactory().getConnection(); try { Properties config = connection.getConfig("notify-keyspace-events"); if (!StringUtils.hasText(config.getProperty("notify-keyspace-events"))) { connection.setConfig("notify-keyspace-events", keyspaceNot