前言
在业务中使用场景有非常多,比如订单超时未支付取消、用户注册几天未操作自动注销等等。这里应用到的是物品到期做一个提醒。RabbitMQ实现延时队列有两种方式,一是在队列里的TTL时间,这种如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先得到执行。所以推荐第二种插件方式实现,他的TTL时间是在交换机中。
基于插件实现延迟队列
1、安装延迟插件
- 选择rabbitmq_delayed_message_exchange的Releases
- 选择自己安装的mq与Erlang对应版本,下载ez后缀的文件rabbitmq_delayed_message_exchange-3.10.0.ez
- 将下载的压缩包放在RabbitMq的安装目录下的plgins目录,可以使用whereis rabbitmq查看安装路径
- 执行启用插件命令
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
- 重启RabbitMq服务
rabbitmqctl stop
rabbitmq-server -detached
- 安装结果验
2、pom.xml引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
<version>2.5.15</version>
</dependency>
3、application.yml配置
spring:
rabbitmq:
host: 地址
port: 5672
username: 用户名
password: 密码
4、延迟队列配置代码
配置架构图
/**
* 延迟队列配置
*
* @author 权仔
*/
@Configuration
public class DelayedQueueConfig {
public static final String EXCHANGE_NAME = "goods.delay.exchange";
/**
* 到期的
*/
public static final String EXPIRED_DELAY_QUEUE = "expired.delay.queue";
public static final String EXPIRED_KEY = "expired";
/**
* 死信队列
*/
public static final String DEAD_EXCHANGE_NAME = "goods.dead.exchange";
public static final String EXPIRED_DEAD_QUEUE = "expired.dead.queue";
public static final String EXPIRED_DEAD_KEY = "expired.dead";
@Bean(EXCHANGE_NAME)
public CustomExchange delayExchange() {
// 插件形式实现延迟消息
Map<String, Object> args = new HashMap<>();
args.put("x-delayed-type", "direct");
return new CustomExchange(EXCHANGE_NAME, "x-delayed-message", true, false, args);
}
@Bean(DEAD_EXCHANGE_NAME)
public DirectExchange deadExchange() {
return new DirectExchange(DEAD_EXCHANGE_NAME, true, false);
}
@Bean(EXPIRED_DELAY_QUEUE)
public Queue expiredQueue() {
return QueueBuilder.durable(EXPIRED_DELAY_QUEUE)
.deadLetterExchange(DEAD_EXCHANGE_NAME)
.deadLetterRoutingKey(EXPIRED_DEAD_KEY).build();
}
@Bean(EXPIRED_DEAD_QUEUE)
public Queue expiredDeadQueue() {
return QueueBuilder.durable(EXPIRED_DEAD_QUEUE).build();
}
@Bean
public Binding bindingA(@Qualifier(EXPIRED_DELAY_QUEUE) Queue queue,
@Qualifier(EXCHANGE_NAME) CustomExchange exchange) {
// 过期物品提醒队列与交换机绑定
return BindingBuilder.bind(queue).to(exchange).with(EXPIRED_KEY).noargs();
}
@Bean
public Binding bindingExpiredDead(@Qualifier(EXPIRED_DEAD_QUEUE) Queue queue,
@Qualifier(DEAD_EXCHANGE_NAME) DirectExchange exchange) {
// 过期死信队列与交换机绑定
return BindingBuilder.bind(queue).to(exchange).with(EXPIRED_DEAD_KEY);
}
}
5、生产者代码
/**
* 延迟队列生产者
*
* @author 权仔
*/
@Slf4j
@Service
public class DelayedQueueProducer {
@Resource
private RabbitTemplate rabbitTemplate;
/**
* 发送到任务延迟队列
*
* @param message 消息
* @param delayTime 延时时间,毫秒值
* @param routingKey 队列类型 {@link DelayedQueueConfig}
*/
public void sendTask(String message, Integer delayTime, String routingKey) {
log.info("当前时间:{},发送一条时长:{} ms 信息给延迟队列:{},类型:{}", new Date(), delayTime, message, routingKey);
// 发送消息对应config声明的交换机关系
rabbitTemplate.convertAndSend(DelayedQueueConfig.EXCHANGE_NAME, routingKey,
message, msg -> {
// 发送消息的时候 延迟时长 单位:ms
msg.getMessageProperties().setDelay(delayTime);
return msg;
});
}
}
6、消费者代码
/**
* 延迟队列消费者
*
* @author 权仔
*/
@Slf4j
@Component
public class DelayedQueueConsumer {
@Resource
private IGoodsService goodsService;
@RabbitListener(queues = DelayedQueueConfig.EXPIRED_DELAY_QUEUE)
public void expired(Message message, Channel channel) throws IOException {
try {
String msg = new String(message.getBody());
log.info("当前时间:{} ,过期队列收到消息 :{} ", new Date(), msg);
Goods goods = goodsService.getById(msg);
if (null == goods) {
// 代表已经删除的物品
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} else {
// TODO 业务处理
if (send) {
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} else {
// 业务处理失败,手动应答拒绝放回死信队列
channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
}
}
} catch (Exception e) {
log.error("过期队列消息处理异常", e);
channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
}
}
@RabbitListener(queues = DelayedQueueConfig.EXPIRED_DEAD_QUEUE)
public void expiredDead(Message message, Channel channel) throws IOException {
try {
String msg = new String(message.getBody());
log.info("当前时间:{} ,过期死信队列收到消息 :{} ", new Date(), msg);
// 处理失败的消息,保存到数据库,根据业务处理
saveDeadMsg(message, channel, "expired");
} catch (Exception e) {
log.error("死信队列过期消息处理异常", e);
channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
}
}
}