比如这个库存锁成功了,我们害怕订单后续操作失败了,导致库存没法回滚,我们库存要自己解锁,
那么可以把锁成功的消息,先发给消息队列,但是让消息队列先暂存一会儿。比如我们存上三十分钟,
因为我们现在的订单有可能是成功了,也有可能是失败。
无论是成功失败,我们三十分钟以后,再对订单进行操作,比如订单不支付,我们都给它关了。所以三十分钟以后订单肯定就见分晓了。
四十分钟以后我们把这个消息再发给解锁库存服务,解锁库存服务,一看这个订单早都没有了,或者订单都没有支付,被人取消了。
它就可以把当时锁的库存自动的解锁一下。
相当于我们整了一个定时的扫描订单、一个定时的库存解锁消息。
因为订单是保存30分钟之后,再对其进行彻底检查,这个检查是需要时间的,我们需要确保所有订单都处理完了,再对库存进行操作,所以设置为40分钟
这两个业务都使用定时任务的话会给我们带来很大的问题。
首先我们定时任务消耗我们这个系统内存,并且它增加数据库压力
因为定时任务是每隔一段时间就要轮巡去来访问数据库。
相当于我们这个数据库呢每隔一段时间就要做一个全盘扫描,扫这些订单,扫这些库存,这样整个数据库的压力就会非常大。
定时任务最致命的问题是它有较大的时间误差。
比如我们来看下面这个场景。
假设现在的时间是 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就是消息的存活时间
无论给哪个设置,它的意思都是一样的
就是指这个消息只要在我们指定的时间里边没有被人消费,那这个消息就相当于没用了,我们就把称为死信,然后这个消息相当于死了。
死了以后我们就可以把它做一个单独处理。
我们如果是给队列设置了过期时间,
队列里边的这些消息,我们存到里边。
如果这些消息一直没有被人监听,那一有人监听肯定就拿到了。
如果我们这个消息呢没有连上任何的消费者,队列里面的消息,只要三十分钟一过,那这些消息呢就会成为死信。
那就可以把它扔掉了,服务器默认就会把它进行丢弃。
那我们给消息设置,也是一样的效果。
如果我们给单独的每一个消息设置,设置了三十分钟过期时间,存到这个队列里边,
只要这个队列没人消费这个消息,消息三十分钟没有人消费,它就自己过期了。
设置消息的 TTL 的目的就是,在我们指定的这段时间内,没人把这个消息取走,这个消息就会被服务器认为是一个死信。
如果队列设置了,消息也设置了,那么会取小的。所以一个消息如果被路由到不同的队列中,这个消息死亡的时间有可能不一样(不同的队列设置)。这里单讲单个消息的TTL,因为它才是实现延迟任务的关键。可以通过设置消息的expiration
字段或者x-message-ttl
属性来设置时间,两者是一样的效果。
一个消息如果满足以下条件,那它就会成为死信
被Consumer拒收了,并且reject方法的参数里requeue是false。
也就是说不会被再次放在队列里,被其他消费者使用。 ( basic.reject/ basic.nack ) requeue=false
消息的TTL到了,消息过期了。
假设队列的长度限制满了,排在前面的消息就会被丢弃或者扔到死信路由上
但是如果这个消息死了,就直接把它丢掉,那就没有延时队列的功能了
假设我给队列设一个过期时间三十分钟,
然后三十分钟以后,只要这个消息没人消费,我们就认为是死信,我们让服务器把死信扔给隔壁的一个交换机。
这个交换机我们称为死信路由,它专门来收集这些死信,然后死信路由会把这些死信再送到另外一个新队列里边,别人专门监听这个新队列
注意:一开始我们设置30分钟过期的那个队列,我们不会让任何人监听,因为只要被人一监听,消息就算设置了过期时间,被人一拿,也就什么都没了。
死信去的那个新队列,里面其实存的都是初始队列里边过了三十分钟以后的这些消息,都是被死信路由给送过去的
这样就模拟了一个延迟,我们三十分钟让它在一开始的队列存一下,因为没人消费它,存完了以后又移到一个新的队列里。
如果我们解锁订单的服务,一直来监听那个新队列,
订单一下成功,先把订单消息放到初始队列延迟上三十分钟,延迟以后呢,交给交换机。
交换机再路由到新队列。
那么收到的这些订单消息一定都是过了30分钟的。
死信路由呢就是一个非常普通的路由而已。
只要 TTL 跟 死信路由两者结合,我们就能模拟出延时队列,
消息在初始队列保持三十分钟,这个队列一直不被人消费,然后三十分钟一过,消息被服务器认为是死信,再丢给交换机,然后,这个交换机再丢给我们指定的队列,然后这个指定的队列再被人消费。
发送者: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
这个队列,那这个人收到的消息都是在死信队列存过五分钟以后的过期消息,这是我们延时队列的第一种实现,设置队列过期时间。
比如我们这个消费者发了一个消息,它为发的这个消息,单独设置了一个过期时间,比如它是三百秒,五分钟。
然后,这个消息经过交换机交给我们这个的死信队列,
由于消息存到死信队列以后,没有人会永远去监听里边的内容。所以这个消息就会一直呆在死信队列里边。
服务器就会来检查这个消息,结果发现它是五分钟以后过期的,所以五分钟以后服务器就会把第一个消息拿出来,
然后把这个消息,按照我们队列指定的:死了的消息交给死信路由,然后再交给test.queue
所以消费者最终收到的消息也都是五分钟以后的过期消息。
推荐给队列设置过期时间
如果给消息设置过期时间的话,我们 Rabbit MQ 采用的是惰性检查机制,也就是懒检查,
什么叫懒检查,
假设我们这个 MQ 这个队列里边存了第一个消息。
第一个消息,我们指定它是一个五分钟以后过期的,我们给这个队列连发了三条消息。
第一个是五分钟以后过期,
第二个是一分钟以后过期
第三个是一秒以后过期,
我们按照正常情况,应该是一秒过期的,我们优先弹出这个队列,但是服务器不是这么检查的。
服务器是这样,它从队列里边呢先来拿第一个消息。
第一个消息呢,它刚一拿,发现是五分钟过期,然后呢,它又放回去了。五分钟以后再来拿
服务器呢五分钟以后会把第一个消息拿出来,那第一个消息呢相当于就过期了,变成死信交到交换机,最终进入死信队列,被消费者拿到
所以第一个消息过期了以后,服务器来拿第二个消息。
第二个消息,它说一分钟过期,但是服务器也不用等它一分钟,因为它发消息的时候又一个时间,服务器一看发现早过期了,然后就赶紧把它扔了。
然后,第三个它说一秒以后过期了。那我们也给它扔了,
但是我们会发现,扔后面的这两个消息,就会在五分钟以后才扔。
所以我们应该使用给整个队列设置一个过期时间,这样队列里边所有的消息都是这个过期时间,我们服务器直接批量全部拿出来,往后放就行了。
需求:让订单一旦下单成功以后,我们过了三十分钟没有人支付,让它来自动关闭订单。那么说以前可以用定时任务来做。那我们现在呢就用延时队列完成。
首先我们创建两个交换机,user.order.delay.exchange
、user.order.exchange
我们还创建了两个队列,user.order.delay.queue
、user.order.queue
首先生产者,就是我们的订单服务。
订单服务只要完成一个订单,也就是下单成功,他先给我们Rabbit MQ
来发一个消息。这个消息用的路由键就叫order_delay
。
然后这个消息,先发给user.order.delay.exchange
这个交换机
然后这个交换机就一看,有消息来了,用的是这个order_delay
的路由键,它就找到下面的user.order.delay.queue
这个队列跟他进行绑定。
但是大家注意。
我们这个叫user.order.delay.queue
的队列,是一个特殊的队列,
我们设置了三个参数。
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,我们就是感知当前微服务各种事件的交换机。
首先只要一下订单成功,给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
队列。
这个队列里的内容,都是我们刚创建完订单过了一分钟以后来的。
最终就在这来判断关单。
接下来,就按照这个场景来做延时队列。
项目地址