指定时间(后)执行某项操作

一般的做法有:

  • 定时任务

  • rocketmq延迟队列

  • rabbitmq死信队列

  • 时间轮算法

  • redis过期监听

一、定时任务关闭订单(最low)

一般情况下,最不推荐的方式就是关单方式就是定时任务方式,原因我们可以看下面的图来说明

我们假设,关单时间为下单后10分钟,定时任务间隔也是10分钟;通过上图我们看出,如果在第1分钟下单,在第20分钟的时候才能被扫描到执行关单操作,这样误差达到10分钟,这在很多场景下是不可接受的,另外需要频繁扫描主订单号造成网络IO和磁盘IO的消耗,对实时交易造成一定的冲击,所以PASS

二、rocketmq延迟队列方式

延迟消息 生产者把消息发送到消息服务器后,并不希望被立即消费,而是等待指定时间后才可以被消费者消费,这类消息通常被称为延迟消息。 在RocketMQ开源版本中,支持延迟消息,但是不支持任意时间精度的延迟消息,只支持特定级别的延迟消息。 消息延迟级别分别为1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h,共18个级别。

这种方式相比定时任务好了很多,但是有一个致命的缺点,就是延迟等级只有18种(商业版本支持自定义时间),如果我们想把关闭订单时间设置在15分钟该如何处理呢?显然不够灵活。

三、rabbitmq死信队列的方式

Rabbitmq本身是没有延迟队列的,只能通过Rabbitmq本身队列的特性来实现,想要Rabbitmq实现延迟队列,需要使用Rabbitmq的死信交换机(Exchange)和消息的存活时间TTL(Time To Live)

死信交换机 一个消息在满足如下条件下,会进死信交换机,记住这里是交换机而不是队列,一个交换机可以对应很多队列。

一个消息被Consumer拒收了,并且reject方法的参数里requeue是false。也就是说不会被再次放在队列里,被其他消费者使用。 上面的消息的TTL到了,消息过期了。

队列的长度限制满了。排在前面的消息会被丢弃或者扔到死信路由上。 死信交换机就是普通的交换机,只是因为我们把过期的消息扔进去,所以叫死信交换机,并不是说死信交换机是某种特定的交换机

消息TTL(消息存活时间) 消息的TTL就是消息的存活时间。RabbitMQ可以对队列和消息分别设置TTL。对队列设置就是队列没有消费者连着的保留时间,也可以对每一个单独的消息做单独的设置。超过了这个时间,我们认为这个消息就死了,称之为死信。如果队列设置了,消息也设置了,那么会取值较小的。所以一个消息如果被路由到不同的队列中,这个消息死亡的时间有可能不一样(不同的队列设置)。这里单讲单个消息的TTL,因为它才是实现延迟任务的关键。

byte[] messageBodyBytes = "Hello, world!".getBytes();  
AMQP.BasicProperties properties = new AMQP.BasicProperties();  
properties.setExpiration("60000");  
channel.basicPublish("my-exchange", "queue-key", properties, messageBodyBytes);  

复制代码

可以通过设置消息的expiration字段或者x-message-ttl属性来设置时间,两者是一样的效果。只是expiration字段是字符串参数,所以要写个int类型的字符串:当上面的消息扔到队列中后,过了60秒,如果没有被消费,它就死了。不会被消费者消费到。这个消息后面的,没有“死掉”的消息对顶上来,被消费者消费。死信在队列中并不会被删除和释放,它会被统计到队列的消息数中去。

这种方式可以自定义进入死信队列的时间;是不是很完美,但是有的小伙伴的情况是消息中间件就是rocketmq,公司也不可能会用商业版,怎么办?那就进入下一节

四、时间轮算法

(1)创建环形队列,例如可以创建一个包含3600个slot的环形队列(本质是个数组)

(2)任务集合,环上每一个slot是一个Set 同时,启动一个timer,这个timer每隔1s,在上述环形队列中移动一格,有一个Current Index指针来标识正在检测的slot。

Task结构中有两个很重要的属性: (1)Cycle-Num:当Current Index第几圈扫描到这个Slot时,执行任务 (2)订单号,要关闭的订单号(也可以是其他信息,比如:是一个基于某个订单号的任务)

假设当前Current Index指向第0格,例如在3610秒之后,有一个订单需要关闭,只需: (1)计算这个订单应该放在哪一个slot,当我们计算的时候现在指向1,3610秒之后,应该是第10格,所以这个Task应该放在第10个slot的Set中 (2)计算这个Task的Cycle-Num,由于环形队列是3600格(每秒移动一格,正好1小时),这个任务是3610秒后执行,所以应该绕3610/3600=1圈之后再执行,于是Cycle-Num=1

Current Index不停的移动,每秒移动到一个新slot,这个slot中对应的Set,每个Task看Cycle-Num是不是0: (1)如果不是0,说明还需要多移动几圈,将Cycle-Num减1 (2)如果是0,说明马上要执行这个关单Task了,取出订单号执行关单(可以用单独的线程来执行Task),并把这个订单信息从Set中删除即可。 (1)无需再轮询全部订单,效率高 (2)一个订单,任务只执行一次 (3)时效性好,精确到秒(控制timer移动频率可以控制精度)

五、redis过期监听

测试结果

        当key数量小于1万的时候 , 基本上都可以在10s内完成过期通知

        当key数量到3万 , 就有部分key会延迟120s

        当key数量到5万的时候 , 大部分都已经滞后了两分钟
        

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
可以使用 `ScheduledExecutorService` 来实现在指定时间执行某方法的功能。下面是一个简单的示例: ```java @Component public class MyTask { private final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1); public void scheduleTask() { // 延迟 10 秒后执行 long delay = 10L; scheduledExecutorService.schedule(this::doSomething, delay, TimeUnit.SECONDS); } public void doSomething() { // 要执行的任务逻辑 } } ``` 在上面的示例中,我们通过 `ScheduledExecutorService` 的 `schedule` 方法来实现在延迟指定时间执行某方法的功能。`schedule` 方法的第一个参数是一个 `Runnable` 对象,可以使用 lambda 表达式来简化代码。第二个参数是延迟的时间,单位是 `TimeUnit` 类型的枚举,这里我们设置延迟 10 秒后执行。第三个参数是执行时间时间单位,这里我们使用秒作为单位。 需要注意的是,在使用 `ScheduledExecutorService` 时,需要在程序结束时手动关闭线程池,以避免线程泄漏。可以在 Spring Boot 应用的关闭钩子中添加关闭线程池的逻辑,例如: ```java @Component public class MyShutdownHook implements CommandLineRunner, ApplicationListener<ContextClosedEvent> { @Autowired private MyTask myTask; private ScheduledExecutorService scheduledExecutorService; @Override public void run(String... args) { scheduledExecutorService = myTask.getScheduledExecutorService(); } @Override public void onApplicationEvent(ContextClosedEvent event) { scheduledExecutorService.shutdown(); } } ``` 在上面的示例中,我们实现了 `CommandLineRunner` 和 `ApplicationListener` 接口,并在 `run` 方法中获取了 `MyTask` 中定义的 `ScheduledExecutorService` 对象。在 `onApplicationEvent` 方法中,我们在 Spring Boot 应用关闭时手动关闭了线程池,以避免线程泄漏。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值