RabbitMQ 延迟队列-非常非常实用
RabbitMQ 延迟队列-非常非常实用
一、使用场景
目前常见的应用软件都有消息的延迟推送的影子,应用也极为广泛,例如:
淘宝七天自动确认收货,自动评价功能等。在我们签收商品后,物流系统会在七天后延时发送一个消息给支付系统,通知支付系统将款打给商家,这个过程持续七天,就是使用了消息中间件的延迟推送功能;相应的,自动评价也是类似的。
12306 购票支付确认页面。我们在选好票点击确定跳转的页面中往往都会有倒计时,代表着 30 分钟内订单不确认的话将会自动取消订单。其实在下订单那一刻开始购票业务系统就会发送一个延时消息给订单系统,延时30分钟。
当用户下订单后,将用户的订单的标识全部发送到延时队列中,30分钟后进去消费队列中被消费,消费时先检查该订单的状态,如果未支付则标识该订单失效,如果之前已经支付了,则可以通过逻辑代码判断来忽略掉收到的消息。
有以下几种延时任务处理方式:
Java自带的DelayQueue队列(底层代码DelayQueue,而Delayed继承了Comparable,所以,可以实现一个排序效果)
这是java本身提供的一种延时队列,如果项目业务复杂性不高可以考虑这种方式。它是使用jvm内存来实现的,停机会丢失数据(需要自行持久化),扩展性不强。
使用redis监听key的过期来实现
当用户下订单后把订单信息设置为redis的key,30分钟失效,程序编写监听redis的key失效,然后处理订单(我也尝试过这种方式)。这种方式最大的弊端就是只能监听一台redis的key失效,集群下将无法实现,也有人监听集群下的每个redis节点的key,但我认为这样做很不合适。如果项目业务复杂性不高,redis单机部署,就可以考虑这种方式。
而其他的解决方案,重点讲解延迟插件,以前的死信队列+TTL过期时间方式,请自行研究。
二、消息延迟推送的实现
在 RabbitMQ3.6.x 之前我们一般采用死信队列+TTL过期时间来实现延迟队列,我们这里不做过多介绍,网上很多文章都有过介绍。在 RabbitMQ 3.6.x 开始,RabbitMQ 官方提供了延迟队列的插件,可以下载放置到 RabbitMQ 根目录下的 plugins 下。
本人使用的RabbitMQ是3.7.7版本,rabbitmq_delayed_message_exchange-3.8.0.ez这个插件放到RabbitMQ安装目录的plugins文件中 在RabbitMQ 安装目的sbin用cmd使用命令
插件下载地址:https://www.rabbitmq.com/community-plugins.html
延迟:发消息后,要过一段时间才进行消费。
使用过死信队列,完成对消息进行延迟,借助的是:死信交换机与延迟队列来实现,配置相对麻烦
实际工作中,如果要实现消息延迟,还可以借助延迟插件来实现通过安装插件,通过自定义交换机,让交换机拥有延迟发送消息的能力,从而实现延迟消息。
两种区别:
由于死信队列方式需要创建两个交换机(死信队列交换机+处理队列交换机)、两个队列(死信队列+处理队列),而延迟插件方式只需创建一个交换机和一个队列,所以后者使用起来更简单。
下图就是基于死信队列,来实现延迟队列的效果。
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
开启插件后,启动RabbitMQ,访问登录后访问http://localhost:15672,用guest/guest登录后,在交换机exchanges的tab下,底部新增将看到如下图设置,则表示插件已启动,以后直接就可以使用了。
插件从github下载速度较慢,本人提交了插件到码云了,同时项目地址也在下方。
链接: rabbitmq_delayed_message_exchange-3.8.0.ez下载地址.
延迟插件底层简单原理图:
实现原理
原始的DLX + TTL 的模式,消息首先会路由到一个正常的队列,根据设置的 TTL 进入死信队列,与之不同的是通过 x-delayed-message 声明的交换机(具体代码请看下面config下的配置类交换机定义参数),它的消息在发布之后不会立即进入队列,先将消息保存至 Mnesia(一个分布式数据库管理系统,适合于电信和其它需要持续运行和具备软实时特性的 Erlang 应用。目前资料介绍的不是很多)。
这个插件将会尝试确认消息是否过期,首先要确保消息的延迟范围是 Delay > 0, Delay =< ?ERL_MAX_T(在 Erlang 中可以被设置的范围为 (2^32)-1 毫秒),如果消息过期通过 x-delayed-type 类型标记的交换机投递至目标队列,整个消息的投递过程也就完成了。
项目结构:
config:配置类,含各种交换机队列等设置。
controller:不想解释。
rabbitmq.listener:消费者,如果是微服务,请提出去。这里为了方便,则放入了同一个项目中。
service:生产者,用于发送消息到mq。
po:实体类,简单业务测试
test:测试类,单独只写了一个批量延迟消息生产者,可以生产一定数量的消息。
三、项目具体实现
本项目是在springboot下,代码如下,首先引入amqp:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
配置文件如下:
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
virtual-host: /
connection-timeout: 15000
#设置为true后 生产者在消息入队列后,但是没有被路由到合适队列情况下,会被生产者者这边的ReturnCallback监听,不会自动删除,为false,会被自动剔除
template:
mandatory: true
listener:
simple:
# 开启手动确认
acknowledge-mode: manual
#开启return 确认消息
publisher-returns: true
# ConfirmCallback开启发送到交换机Exchange触发回调
publisher-confirm-type: correlated
配置文件,下面有注释的地方,是额外确保消息可达防丢失,和成功失败触发相关的配置。
具体的业务逻辑如上图所示(目前测试过主题和直连,广播模式经过测试也可以使用):
配置config包下,用于配置交换机,队列和routingkey,对应上图中的名称:
package com.woniuxy.config;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* @author: mayuhang <br/>
* Date: 2021/3/17:16:03 <br/>
* Description:延迟队列配置类
*/
@Configuration
public class LazyExchangeConfig {
public static final String LAZY_EXCHANGE="Ex.LazyExchange";
public static final String LAZY_QUEUE="MQ.LazyQueue";
public static final String LAZY_KEY="lazy.#";
@Bean
public CustomExchange lazyExchange() {
//第一种设置方法 设置延迟交换机配置