在前面章节中我们介绍了分布式消息中间件RabbitMQ的相关知识要点,其中就包括RabbitMQ的核心基础组件和各种基本的消息模型。比较典型的消息模型包括基于TopicExchange的消息模型、基于DirectExchange的消息模型和基于FanoutExchange的消息模型。
这些消息模型都有一个共同的特点,那就是消息一旦进入队列,将立即被对应的消费者监听消费。然而在某些业务场景中,有些业务数据对应的消息在进入队列后不希望立即被处理,而是要求该消息可以“延迟”一定的时间,再被消费者监听消费处理,这便是“死信队列/延迟队列”出现的初衷
死信队列又称之为延迟队列、延时队列,也是RabbitMQ队列中的一种,指进入该队列中的消息会被延迟消费的队列。这种队列跟普通的队列相比,最大的差异在于消息一旦进入普通队列将会立即被消费处理,而延迟队列则是会过一定的时间再进行消费。
在传统企业级应用系统中,实现消息、业务数据的延迟处理一般是通过开启定时器的方式,轮询扫描并获取数据库表中满足条件的业务数据记录,然后比较数据记录的业务时间和当前时间。如果当前时间大于记录中的业务时间,则说明该数据记录已经超过了指定的时间而未被处理,此时需要执行相应的业务逻辑,比如失效该数据记录、发送通知信息给指定的用户等。对于这种处理方式,定时器是每隔一定的时间频率不间断地去扫描数据库表,并不断地获取满足业务条件的数据,直到手动关闭该定时器(如果不关闭的话,定时器开启的线程将一直运行下去)。
下面以“春运期间12306抢票”为例,介绍RabbitMQ死信队列的作用。春运期间抢票,相信读者并不陌生,当我们用12306抢票软件抢到火车票时,12306官方会提醒用户“请在30分钟内付款”,正常情况下用户会立即付款,然后输入相应的支付密码支付车票的价格,扣款成功后,12306官方会以邮件或者短信方式通知用户抢票和付款成功。然而,实际中却存在着一些特殊情况。比如用户抢到火车票后,由于各种原因而迟迟没有付款(比如抢到的车票出行时间不是自己想要的),过了30分钟后仍然没有支付车票的价格,导致系统自动取消该笔订单。
早期的很多抢票软件每当赶上春运高峰期时,经常会出现网站崩溃、单击购买车票却一直没响应等状况,这在某种程度上可能是因为在某一时刻产生的高并发,或者定时频繁拉取数据库得到的数据量过大等状况而导致内存、CPU、网络和数据库服务等负载过高所引起的。而消息中间件RabbitMQ的引入,不管是从业务层面还是从应用的性能层面,都得到了很大的改善。如图6.2所示为引入RabbitMQ消息中间件后“抢票成功后30分钟内对未付款的处理流程”的优化。
从该优化后的处理流程中可以看出,RabbitMQ的引入主要是替代了传统处理流程的“定时器”处理逻辑,取而代之的是采用RabbitMQ的死信队列/延迟队列进行处理。死信队列/延迟队列指的是可以延迟一定的时间再处理相应的业务逻辑,而这也可以看作是死信队列的作用,即死信队列/延迟队列可以实现特定的消息或业务数据等待一定的时间TTL后,再被消费者监听消费处理。
值得一提的是,RabbitMQ提供的“死信队列”这一功能特性在实际生产环境中确实能起到很好的作用,比如上面讲的“成功抢到票后30分钟内未付款的处理流程”就是比较典型的一种。除此之外,商城购物时“单击去付款而迟迟没有在规定的时间内支付”流程的处理;App点外卖时“下单成功后迟迟没有在规定的时间内付款”流程的处理;用户提交会员注册信息后“30分钟内没有进行邮箱或短信验证时发送提醒”等,这些都是实际生产环境中比较典型的场景。
盛名之下自然无虚士。RabbitMQ的死信队列也是如此。相对于传统定时器的轮询处理方式,死信队列具有占用系统资源少(比如不需要再轮询数据库获取数据,减少DB层面资源的消耗)、人为干预很少(只需要搭建好死信队列消息模型,就可以不需要再去干预了),以及自动消费处理(当指定的延迟时间一到,消息将自动被路由到实际的队列进行处理)等优势。不夸张地讲,在实际项目中,“任何需要延迟、延时处理的业务”都可以用上“死信队列”这个强大的组件。在学习完本章内容之后,读者将会发现死信队列的高效性和便捷性。
与普通的队列相比,死信队列同样也具有消息、交换机、路由和队列等专有名词,只不过在死信队列里增加了另外3个成员,即DLX、DLK和TTL。
其中DLX跟DLK是必需的成员,而TTL则是可选、非必需的。下面着重介绍一下这3个成员:
● DLX,即Dead Letter Exchange,中文为死信交换机,是交换机的一种类型,只是属于特殊的类型。
● DLK,即Dead Letter Routing-Key,中文为死信路由,同样也是一种特殊的路由,主要是跟DLX组合在一起构成死信队列。
● TTL,即Time To Live,指进入死信队列中的消息可以存活的时间。当TTL一到,将意味着该消息“死了”,从而进入下一个“中转站”,等待被真正的消息队列监听消费。值得一提的是,当消息在一个队列中发生以下几种情况时,才会出现“死信”的情况:
● 消息被拒绝(比如调用basic.reject或者basic.nack方法时即可实现)并且不再重新投递,即requeue参数的取值为false。
● 消息超过了指定的存活时间(比如通过调用messageProperties.setExpiration()设置TTL时间即可实现)。
● 队列达到最大长度了。
对于RabbitMQ的死信队列,简单理解就是消息一旦进入死信队列,将会等待TTL时间,而 TTL一到,消息将会进入死信交换机,然后被路由到绑定的真正队列中,最终被真正的队列对应的消费者监听消费。值得一提的是,TTL既可以设置成为死信队列的一部分,也可以在消息中单独进行设置,当队列跟消息同时都设置了存活时间 TTL时,则消息的“最大生存时间”或者“存活时间”将取两者中较短的时间。
因此,为了能更好地实现“自动检测用户下单记录是否已经超过了支付时间”这个功能,分布式消息中间件RabbitMQ提供了死信队列,即它可以实现“延迟一定的时间处理业务数据”的功能。由于“用户下单”这种业务场景具有时效性,因而可以在处理用户下单的业务逻辑中,将“超时时间”即 TTL设置为死信队列的组成成分,将“下单记录id”充当消息发送至死信队列中。然后只需要在“真正队列”对应的消费者中监听消费该消息,当消费者能监听到消息时,即代表着该下单记录已经超过了30分钟。此时可以根据获取到的消息,即“下单记录id”,作为查询条件前往数据库表中查询相应的记录。如果该下单记录的支付状态为“未付款”或者“已保存”,则代表该用户下单记录已经支付超时了,此时需要将该记录更新为失效状态;如果该下单记录的支付状态为“已付款”,则代表该用户已经在30分钟内成功支付了该笔订单。