7. RabbitMQ 延时队列

延时队列场景

7. RabbitMQ 延时队列_第1张图片

比如这个库存锁成功了,我们害怕订单后续操作失败了,导致库存没法回滚,我们库存要自己解锁,

那么可以把锁成功的消息,先发给消息队列,但是让消息队列先暂存一会儿。比如我们存上三十分钟,

因为我们现在的订单有可能是成功了,也有可能是失败。

无论是成功失败,我们三十分钟以后,再对订单进行操作,比如订单不支付,我们都给它关了。所以三十分钟以后订单肯定就见分晓了。

四十分钟以后我们把这个消息再发给解锁库存服务,解锁库存服务,一看这个订单早都没有了,或者订单都没有支付,被人取消了。

它就可以把当时锁的库存自动的解锁一下。

相当于我们整了一个定时的扫描订单、一个定时的库存解锁消息。

为什么是40分钟

因为订单是保存30分钟之后,再对其进行彻底检查,这个检查是需要时间的,我们需要确保所有订单都处理完了,再对库存进行操作,所以设置为40分钟

定时任务的问题

这两个业务都使用定时任务的话会给我们带来很大的问题

首先我们定时任务消耗我们这个系统内存,并且它增加数据库压力

因为定时任务是每隔一段时间就要轮巡去来访问数据库

相当于我们这个数据库呢每隔一段时间就要做一个全盘扫描,扫这些订单,扫这些库存,这样整个数据库的压力就会非常大

定时任务最致命的问题是它有较大的时间误差

比如我们来看下面这个场景。

定时任务的时效性问题

7. RabbitMQ 延时队列_第2张图片

假设现在的时间是 10:00,如果我们使用定时任务,每隔 30分钟来扫一次,

假设第一次下单,这个订单是在 10:01 下的,在这个订单之前,刚有一个定时任务运行完,

10:30 的时候,定时任务开始扫描,扫描的时候,10:01 下的这个订单还没有经过 30分钟 的煎熬期。

因为我们设计的是只有 30分钟以后没支付才能关,它还差1分钟,所以我们这个定时任务扫到它的时候,发现它不符合条件,那不给它进行关单,

接下来

结果定时任务刚一结束,这个订单就相当于超时了。

超时了以后,那就得等下一次的定时任务,相当于我们再来等 29分钟,这样再加上前期订单保存的这 30分钟。

有1分钟是过期时间,相当于59分钟我们这个订单可能才会被扫到,被定时任务发现过期了要解锁库存了。

这就是定时任务可能会出现的时效性问题。

那基于这些考虑,我们就不会在这个场景下采用定时任务。

我们采用 MQ 的延时队列

延时队列使用介绍

延时队列,它是基于消息的 TTL(存活时间)以及死信Exchange (路由)结合的

那有了这个延时队列,应该是这样工作的。

比如说我们这个下订单,订单一下成功以后,我们就给消息队列里发一个消息:哪个单下成功了。

我们发的这个消息发到队列里边,这个队列最大的特点就是它们这些消息三十分钟以后才能被人收到。

这样的话,我们如果有一个服务专门来监听这个队列,那我们这里边存来的这些订单下成功的消息。

只有三十分钟以后才会来到我们的监听者的这一块。那这样监听者拿到这个订单,再一查结果发现订单还没支付,那么就给你关了。

所以整个过程无需任何定时任务,我们相当于让 MQ 把消息暂缓一段时间。

包括我们这个锁库存也一样,只要我们库存锁成功了,我们就给 MQ 里边发一个消息。

MQ 先把消息保持上一段时间,不发然后到了保存时间以后,MQ 自己发出去,发出去之后,

解锁库存的服务,发现订单没支付或者订单早都没有了,就给它解锁库存。

所以我们如果使用延时队列,基本上就能解决定时任务的大面积时效性问题。

这个延时队列可能时效性差那么一秒、五秒乃至于一分钟,但是都不可能差上二十分钟。

所以,我们应该使用延时队列的这个场景来做我们的下订单、关闭订单、锁库存以后的解锁库存操作

最终保证我们的事务一致性,也就是我们事务的最终一致性

我们引入MQ 的第一个目的,就是来解决事务的最终一致性

因为我们这个订单最终还是要关的。所以我们使用 MQ 来暂缓一段时间消息。不占用系统的任何资源,只是多架设一个MQ 服务器,等时间到了以后,保证我们最终的数据一致。

延时队列最大的特点

一、TTL

二、死信路由

消息的 TTL(Time To Live)

消息的TTL就是消息的存活时间

RabbitMQ 可以对队列和消息分别设置TTL

无论给哪个设置,它的意思都是一样的

就是指这个消息只要在我们指定的时间里边没有被人消费,那这个消息就相当于没用了,我们就把称为死信,然后这个消息相当于死了

死了以后我们就可以把它做一个单独处理。

我们如果是给队列设置了过期时间

队列里边的这些消息,我们存到里边。

如果这些消息一直没有被人监听,那一有人监听肯定就拿到了。

如果我们这个消息呢没有连上任何的消费者,队列里面的消息,只要三十分钟一过,那这些消息呢就会成为死信

那就可以把它扔掉了,服务器默认就会把它进行丢弃。

那我们给消息设置,也是一样的效果。

如果我们给单独的每一个消息设置,设置了三十分钟过期时间,存到这个队列里边,

只要这个队列没人消费这个消息,消息三十分钟没有人消费,它就自己过期了

设置消息的 TTL 的目的就是,在我们指定的这段时间内,没人把这个消息取走,这个消息就会被服务器认为是一个死信。

如果队列设置了,消息也设置了,那么会取小的。所以一个消息如果被路由到不同的队列中,这个消息死亡的时间有可能不一样(不同的队列设置)。这里单讲单个消息的TTL,因为它才是实现延迟任务的关键。可以通过设置消息的expiration字段或者x-message-ttl属性来设置时间,两者是一样的效果。

死信路由(Dead Letter Exchanges)DLX

什么是死信

一个消息如果满足以下条件,那它就会成为死信

  1. 被Consumer拒收了,并且reject方法的参数里requeue是false。

    也就是说不会被再次放在队列里,被其他消费者使用。 ( basic.reject/ basic.nack ) requeue=false

  2. 消息的TTL到了,消息过期了。

  3. 假设队列的长度限制满了,排在前面的消息就会被丢弃或者扔到死信路由上

    但是如果这个消息死了,就直接把它丢掉,那就没有延时队列的功能了

死信路由的实现

假设我给队列设一个过期时间三十分钟,

然后三十分钟以后,只要这个消息没人消费,我们就认为是死信,我们让服务器把死信扔给隔壁的一个交换机。

这个交换机我们称为死信路由,它专门来收集这些死信,然后死信路由会把这些死信再送到另外一个新队列里边,别人专门监听这个新队列

注意:一开始我们设置30分钟过期的那个队列,我们不会让任何人监听,因为只要被人一监听,消息就算设置了过期时间,被人一拿,也就什么都没了

死信去的那个新队列,里面其实存的都是初始队列里边过了三十分钟以后的这些消息,都是被死信路由给送过去的

这样就模拟了一个延迟,我们三十分钟让它在一开始的队列存一下,因为没人消费它,存完了以后又移到一个新的队列里。

如果我们解锁订单的服务,一直来监听那个新队列,

订单一下成功,先把订单消息放到初始队列延迟上三十分钟,延迟以后呢,交给交换机。

交换机再路由到新队列。

那么收到的这些订单消息一定都是过了30分钟的。

死信路由呢就是一个非常普通的路由而已。

只要 TTL 跟 死信路由两者结合,我们就能模拟出延时队列,

消息在初始队列保持三十分钟,这个队列一直不被人消费,然后三十分钟一过,消息被服务器认为是死信,再丢给交换机,然后,这个交换机再丢给我们指定的队列,然后这个指定的队列再被人消费。

延时队列的实现

设置队列过期时间

7. RabbitMQ 延时队列_第3张图片

发送者:P

路由键:deal.message

非常普通的交换机:X

死信队列:delay.Sm.queue

死信路由:delay.exchange

新队列:test.queue

消费者:C

首先发送者发消息的时候,指定了一个路由键,然后这个消息先会被交给一个交换机,

交换机会按照路由键把它交给一个队列,那这个队列跟交换机的绑定关系就是那个路由键,

但这个队列很特殊,它有这么几项设置

  • x-message-ttl:消息的存活时间,它以毫秒为单位,相当于300秒,也就是五分钟以后消息过期

  • x-dead-letter-exchange:死信交换机,就是死信路由,意思就是告诉服务器当前这个队列里边的消息死了,别乱丢,扔给隔壁的死信路由

  • delay-message:死信队列往死信路由那扔消息用的路由键

所以我们的死信就会通过delay.message这个路由键交给我们的死信路由,一个普普通通的路由,

然后死信路由收到死信之后,一看路由键是delay.message,它就会找哪个队列绑定的路由键叫delay.message

然后,死信路由发现test.queue这个队列的路由键是delay.message,它就把这个消息就交给了它,

以后只要有人监听test.queue这个队列,那这个人收到的消息都是在死信队列存过五分钟以后的过期消息,这是我们延时队列的第一种实现,设置队列过期时间。

设置消息过期时间

7. RabbitMQ 延时队列_第4张图片

比如我们这个消费者发了一个消息,它为发的这个消息,单独设置了一个过期时间,比如它是三百秒,五分钟。

然后,这个消息经过交换机交给我们这个的死信队列,

由于消息存到死信队列以后,没有人会永远去监听里边的内容。所以这个消息就会一直呆在死信队列里边。

服务器就会来检查这个消息,结果发现它是五分钟以后过期的,所以五分钟以后服务器就会把第一个消息拿出来,

然后把这个消息,按照我们队列指定的:死了的消息交给死信路由,然后再交给test.queue

所以消费者最终收到的消息也都是五分钟以后的过期消息。

推荐使用哪种方式

推荐给队列设置过期时间

如果给消息设置过期时间的话,我们 Rabbit MQ 采用的是惰性检查机制,也就是懒检查,

什么叫懒检查,

假设我们这个 MQ 这个队列里边存了第一个消息。

第一个消息,我们指定它是一个五分钟以后过期的,我们给这个队列连发了三条消息。

第一个是五分钟以后过期,

第二个是一分钟以后过期

第三个是一秒以后过期,

我们按照正常情况,应该是一秒过期的,我们优先弹出这个队列,但是服务器不是这么检查的。

服务器是这样,它从队列里边呢先来拿第一个消息。

第一个消息呢,它刚一拿,发现是五分钟过期,然后呢,它又放回去了。五分钟以后再来拿

服务器呢五分钟以后会把第一个消息拿出来,那第一个消息呢相当于就过期了,变成死信交到交换机,最终进入死信队列,被消费者拿到

所以第一个消息过期了以后,服务器来拿第二个消息。

第二个消息,它说一分钟过期,但是服务器也不用等它一分钟,因为它发消息的时候又一个时间,服务器一看发现早过期了,然后就赶紧把它扔了。

然后,第三个它说一秒以后过期了。那我们也给它扔了,

但是我们会发现,扔后面的这两个消息,就会在五分钟以后才扔。

所以我们应该使用给整个队列设置一个过期时间,这样队列里边所有的消息都是这个过期时间,我们服务器直接批量全部拿出来,往后放就行了。

延时队列模拟订单关闭

基本设计

7. RabbitMQ 延时队列_第5张图片

需求:让订单一旦下单成功以后,我们过了三十分钟没有人支付,让它来自动关闭订单。那么说以前可以用定时任务来做。那我们现在呢就用延时队列完成。

首先我们创建两个交换机,user.order.delay.exchangeuser.order.exchange

我们还创建了两个队列,user.order.delay.queueuser.order.queue

首先生产者,就是我们的订单服务。

订单服务只要完成一个订单,也就是下单成功,他先给我们Rabbit MQ来发一个消息。这个消息用的路由键就叫order_delay

然后这个消息,先发给user.order.delay.exchange这个交换机

然后这个交换机就一看,有消息来了,用的是这个order_delay的路由键,它就找到下面的user.order.delay.queue这个队列跟他进行绑定。

但是大家注意。
我们这个叫user.order.delay.queue的队列,是一个特殊的队列,

我们设置了三个参数。

  • :60000`:消息存活时间,1分钟
  • x-dead-letter-exchange:user.order.exchange:死信路由
  • x-dead-letter-routing-key:延时队列往死信路由那扔消息用的路由键

里面的消息只要一超过1分钟,我们的Rabbit MQ就会从队列里边把这个消息拿出来,这个消息变成死信了。

然后死信会交给我们这个隔壁的user.order.exchange交换机,

然后,这个交换机通过绑定的路由键order,找到下面的user.order.queue队列,

最终死信,就会跑到user.order.queue队列里边。

只要有消费者去来监听这个user.order.queue队列里边的内容,就会发现里面的消息一定是过期1分钟以后的消息。

所以只要这个队列收到内容了,我们就可以判断这个订单,只要没支付,就可以给他关单。

设计规范

每一个微服务有他自己的交换机,当前微服务加event,我们就是感知当前微服务各种事件的交换机。

7. RabbitMQ 延时队列_第6张图片

在这里插入图片描述

进阶设计

7. RabbitMQ 延时队列_第7张图片

首先只要一下订单成功,给Rabbit MQ发消息,然后绑定order.create.order这个路由键,

路由键会把消息路由到order-event-exchange这个交换机,

这个交换机绑定了两个队列,

一个是通过order.create.order连接的order.delay.queue队列,

一个是通过order.relaease.order连接的order.release.order.queue队列,

交换机先按照order.create.order这个路由键找到order.delay.queue队列,

发送者的消息就会沿着这个绑定关系来到order.delay.queue

里面有以下配置,一开始信进来的存活时间、死信的去向、送死信走的时候用的路由键,

然后这个order.delay.queue队列里面过期的消息就会去找order-event-exchange这个交换机

相当于死信重新交给我们的订单服务的这个事件交换机。

但是此次交出去的这个消息,用的路由键叫order.relaease.order

所以他就根据这个路由键找到了order.release.order.queue队列。

最终死信就来到了order.release.order.queue这个队列

最后这个消费者,也就是我们的订单释放服务,我们就专门来监听这个order.release.order.queue队列。

这个队列里的内容,都是我们刚创建完订单过了一分钟以后来的。

最终就在这来判断关单。

接下来,就按照这个场景来做延时队列。

测试延时队列 Demo

在这里插入图片描述

项目地址

你可能感兴趣的:(RabbitMQ,延时队列,死信,TTL,死信路由)