Java分布式定时任务场景的思考与设计

Posted c.

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java分布式定时任务场景的思考与设计相关的知识,希望对你有一定的参考价值。

文章目录

Java分布式定时任务场景的思考与设计

假设场景:业务触发之后会更新状态,所以我们需要在业务触发一段时间之后监控业务数据状态是否发生了改变,把没有发生改变的数据抽取出来发送邮件给开发人员。

延迟队列DelayQueue

jdk中DelayQueue可以实现上述需求,DelayQueue就是延时队列。

  1. DelayQueue提供了在指定时间才能获取队列元素的功能,队列头元素是最接近过期的元素

  2. DelayQueue是一个无界阻塞队列,只有在延迟期满时,才能从中提取元素。

所以要实现这个功能,我们需要在服务启动之后,开启一个线程不断的去阻塞获取延迟到期的数据,然后在判断这个数据的状态有没有发生改变,如果没有改变则需要发送邮件

参考: springBoot之延时队列

缺点:

  1. 占用了一个线程不停空转去获取延迟队列中的数据
  2. 分布式的场景下,每个服务都会有各自的线程去维护各自的延迟队列
  3. 数据量大的场景下,数据积压会比较大,会占用过多的内存,存在OOM的风险
  4. 在数据量大的场景下,可能会发送大量的邮件,邮件速率会过大
  5. 数据存于内存,没有持久化,出现系统宕机的情况下会丢失数据
  6. 需要在业务代码中埋点

MQ 死信队列+TTL实现延迟队列

对于没有直接支持延迟队列的中间件MQ,可以采用另一种方式来实现延迟队列。

对于支持死信队列的中间件MQ,可以采用死信队列+TTL过期时间来实现延迟消费Message的功能。

以下几种方式会把消息推送到死信队列中:

  1. 消息被拒绝
  2. 消息过期
  3. 队列达到最大长度

所以我们可以利用消息过期会把消息移动到死信队列这一特点,来实现延迟队列。

上面图中的例子,消费者不去监听正常的队列这个队列,而是直接监听死信队列队列。那么我们每次发送消息到正常队列队列,由于设置队列消息的TTL,在TTL时间之后,消息进入到死信队列,从而被消费者消费。从而实现了延迟队列。

对于RabbitMQ中间件来说,网上有很多现成的案例可以参考,对于RabbitMQ的实现可以参考如下:

RabbitMQ消息和队列的TTL以及死信队列和延迟队列

RabbitMQ:过期时间TTL和死信队列、延迟队列

而如果使用了Solace中间件的,网上文档比较少,主要还是以官方文档为主,可以参考如下:

How to delay sending out messages in Solace PubSub+

优点:

  1. 解决了分布式的问题,对于延迟队列的维护交给MQ中间件去做
  2. 解决了持久化的问题,及时系统宕机,但是消息依然保留在了MQ中
  3. 像一些电商中超时未支付关闭订单的场景就会是用这种延迟队列的方式,好处就是吞吐量高(相比起定时任务扫表)

缺点:

  1. RabbitMQ对于消息过期的检测:只会检测最近将要被消费的那条消息是否到达了过期时间,不会检测非末端消息是否过期。造成的问题是:非末端消息已经过期了,但是因为末端消息还未过期,非末端消息处于阻塞状态,所以非末端消息不会被检测到已经过期。使业务产生与预期严重不一致的结果。(所以一般采用设置队列过期时间)
  2. 如果采用对队列设置过期时间的话,如果业务中存在不同的过期时间,则每增加一个新的时间需求,就要新增一个队列。(思考:或许能通过同一个过期时间的队列,但是使用一个requeue_count计数器来控制,比如过期时间就是ttl*requeue_count,一旦过期之后判断requeue_count是否为0,如果不是就减1,然后重新投递到正常队列中,这样就能保证正常队列中的过期时间都是一致的,但是最终的过期时间是不一样的,但是缺点就是存在很多次重新投递的动作)
  3. 当数据量大的时候,也就是有很多同时过期消息的时候,可能会出现发送邮件的速率过快的问题。(思考:有些MQ支持批量获取数据的功能: batch-consumers, 可以通过批处理消息来发送邮件)
  4. 需要在业务代码中埋点

参考:

RabbitMQ死信队列在SpringBoot中的使用

基于RabbitMq的实现消息延时发送的优点以及其局限性

MQ 延迟队列

在 RabbitMQ 3.6.x 之前我们一般采用死信队列+TTL过期时间来实现延迟队列,在 RabbitMQ 3.6.x 开始,RabbitMQ 官方提供了延迟队列的插件。

RabbitMQ 官方提供了延迟队列的插件的优点就是解决了之前采用死信队列+TTL过期时间来实现延迟队列的一些缺点。

参考:RabbitMQ 延迟队列,太实用了!

缺点:

  1. 并不是很多MQ中间件支持了延迟队列,网上都是使用RabbitMQ的居多
  2. 需要在业务代码中埋点

对于使用了Solace中间件,似乎也有类似的功能,但没具体实现过,可以参考官网文档:

Delayed Delivery

在官网中提到了,如果你使用了死信队列的方式来实现延迟队列,可以考虑迁移到新的方式:

If you are currently using DMQs to implement a delivery delay, contact Solace Professional Services for guidance on migrating your implementation to use the delivery-delay option instead.
参考:Configuring Delayed Delivery

Schedule Quartz Job

对于定时任务我们可能最常用的就是Schedule Quartz Job,也支持分布式。或者是使用现成的一些分布式定时任务的框架,比如美团分布式定时调度框架XXL-Job

缺点:

  1. 对于某些业务场景下, 比如电商中超时未支付关闭订单的场景,系统的吞吐量不够高

优点:
2. 可指定corn表达式,自由配置
3. 可以抽取一段时间的数据进行批处理,不存在之前延迟队列一条条处理,邮件发送速率过快的问题也能解决
4. 不需要在业务代码中埋点

总结

其实还是具体业务场景具体分析,并没有哪一套设计就是能解决一切问题的"银弹"。当有分布式定时任务的场景,就可以结合实际场景来考虑一下以上的方案。

以上是关于Java分布式定时任务场景的思考与设计的主要内容,如果未能解决你的问题,请参考以下文章

分布式定时任务设计及其框架

分布式定时任务设计及其框架

面试官:怎么不用定时任务实现关闭订单?

面试官:怎么不用定时任务实现关闭订单?

分布式定时任务框架选型,写得太好了!

Java三种方式实现redis分布式锁