分布式环境下实现延时任务的方案

版权声明:转载请申明原创地址 https://blog.csdn.net/pw191410147/article/details/82113603

一,在日常开发中我们经常会使用到这样的一些功能

1,某宝购买了一件商品时尚未付款,然后30分钟后就会自动取消该订单

2,某宝购买一件商品后,确认收货后15天默认好评

类似这种功能我们叫延时任务,但是我们要怎么去实现这样的功能呢,下面就来说说我们的策略吧!

方案1:定时轮询数据库

启动一个job(job调度与业务逻辑处理分离),定时扫描数据库,执行的操作类似select * from xxx where status='1' and created_at<=当前时间-30分钟,获取到数据后,进行订单取消操作。

优点:简单,且可以支持集群扩展

缺点:(1)存在延迟,如果job一分钟执行一次,那么最大延迟就是1分钟

            (2)数据可能存在分布不均的情况,有可能超时的订单都集中在某个小范围时间段,其他时间段job相当于都在空跑

            (3)如果订单量巨大,每隔一分钟扫描一次,性能较差

该解决方案,我们实际中也有使用,定时任务大约5分钟跑一次,主要用于对时间要求不那么敏感且订单量较少(比如金融产品要求百万或者几百万的起投额,这样的业务单个订单的金额巨大,但是总体的订单量少)的场景。

方案2:jdk提供的延迟队列

jdk提供了一个现成的延迟队列实现,DelayQueue,DelayQueue<Element> queue = new DelayQueue<Element>(),这个Element必须要实现Delayed接口,当队列中的元素延迟到期后才能被取出来,可以通过在while循环中调用take方法获取到期的元素。

优点:所有的操作都在内存中,效率高,延迟非常低

缺点:(1)服务器如果重启后,数据就全部丢失了

        (2)如果订单量巨大,可能出现内存不够导致溢出,需要在初期充分预估内存大小

还有一种与这个类似的方法就是采用时间轮的方式,之前的文章中有介绍过,不过本质上也与方案b存在相同的问题。

方案3:jdk延迟队列或者时间轮+定时补偿任务

首先将订单信息(包含超时信息)写入到数据库,然后再写入必要的订单有关信息到jdk延迟队列中。jdk中的延迟任务处理后更新数据库中的订单信息。如果服务器重启数据丢失了,那么由补偿任务执行超时操作。

优点同方案2,缺点:还是可能出现内存不够导致溢出,需要在初期充分预估内存大小

方案4:redis缓存

根据方案方案2的缺点,害怕重启,以及内存溢出,那我们把这些数据都存到redis(规划专门的内存)中,这样也不怕机器重启和内存溢出了。可以利用redis提供的有序集合,具体可以看redisfans网站上的文档介绍。

首先我们使用zadd命令,其member为订单id,score为订单超时时间。然后在while循环中命令zrangebyScore查找超时时间小于等于当前时间的记录(System.currentTime),执行超时操作,然后删掉对应的member。

缺点:(1)redis需要进行额外的维护,redis还是有宕机的可能,宕机时主从切换的过程中可能丢失数据,需要解决这个问题(虽然可以通过配置保证数据不丢失,但会导致master拒绝客户端的写请求,另一种方法是也采用补偿的方式处理这种情况)。

        (2)如果同一时间超时的订单数巨大,zrangebyScore直接获取全部数据会有严重的性能问题,需要分页获取,这样会增大延迟,此时可能需要进行分片,

        (3)需要引入分布式锁避免单个订单被重复执行超时操作

方案5:使用mq延迟投递功能

比如利用rabbitmq的特性(TTL和dead letter exchanges),我们建议两个队列queue1和queue2首先将订单放入队列queue1,设置这个消息的时间为30分钟,超过时间后被转发到queue2,消息者消费queue2即可。

优点:高效,利于横向扩展,mq消息的持久化可以保证可靠性

缺点:需要引入对应的消息中间件,需要对其进行额外的维护(如果本身的项目中就用到了消息中间件,那么可以采用这种方案)

猜你喜欢

转载自blog.csdn.net/pw191410147/article/details/82113603
今日推荐