Spring Boot (26) RabbitMQ延迟队列
延迟消息就是指当消息被发送以后,并不想让消费者立即拿到消息,而是等待指定时间后,消费者才拿到这个消息进行消费。
延迟队列
订单业务: 在电商/点餐中,都有下单后30分钟内没有付款,就自动取消订单。
短信通知:下单成功后60s之后给用户发送短信通知。
失败重试:业务操作失败后,间隔一定的时间进行失败重试。
这类业务的特点就是:非实时的,需要延迟处理,需要进行失败重试。一种比较笨的方法是采用定时任务,轮训数据库,方法简单好用,但性能低下,在高并发情况下容易弄死数据库,间隔时间不好设置,时间过大,影响精度,过小影响性能,而且做不到按超时的时间顺序处理。另一种就是用java中的DelayQueue位于java.util.concurrent包下,本质是由PriorityQueue和BlockingQueue实现的阻塞优先级队列,这东西的问题就是不支持分布式与持久化。
RabbitMQ实现思路
RabbitMQ队列本身是没有直接实现支持延迟队列的功能,但可以通过它的Time-To-Live Extensions和Dead Letter Exchange的特性模拟出延迟队列的功能。
Time-To-Live Extensions
RabbitMQ支持为队列或者消息设置TTL(time to live 存活时间)。TTL表明了一条消息可再队列中存活的最大时间。当某条消息被设置了TTL或者当某条消息进入了设置了TTL的队列时,这条消息会在TTL时间后死亡成为Dead Letter。如果既配置了消息的TTL,又配置了队列的TTL,那么娇小的那个值会被取用。
Dead Letter Exchange
死信交换机,上文中提到设置了TTL的消息或队列最终会成为Dead Letter。如果为队列设置了Dead Letter Exchange(DLX),那么这些Dead Letter就会被重新发送到Dead Letter Exchange中,然后通过Dead Letter Exchange 路由到其他队列,即可实现延迟队列的功能。

导入依赖
pom.xml中加入spring-boot-starter-amqp的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
属性配置
spring:
rabbitmq:
username: david
password: 123456
host: localhost
port: 5672
virtual-host: /
listener:
simple:
acknowledge-mode: manual #手动ACK 不开启自动ACK模式,目的是防止报错后为正确处理消息丢失 默认为none
定义队列
package com.spring.boot.utils; import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import java.util.HashMap;
import java.util.Map; @Configuration
public class RabbitConfig { @Bean
public RabbitTemplate rabbitTemplate(CachingConnectionFactory connectionFactory) {
connectionFactory.setPublisherConfirms(true);
connectionFactory.setPublisherReturns(true);
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMandatory(true);
//设置成功回调
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> System.out.println("消息发送成功:correlationData:" + correlationData + ",ack:" + ack + ",cause:" + cause));
//设置失败回调
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> System.out.println("消息丢失:exchange:" + exchange + ",route:" + routingKey + ",replyCode:" + replyCode + ",replyText:" + replyText + ",message:" + message));
return rabbitTemplate;
} //延迟队列TTL名称
public static final String REGISTER_DELAY_QUEUE = "dev.book.register.delay.queue";
//DLX,死信发送到的交换机
public static final String REGISTER_DELAY_EXCHANGE = "book.exchange";
//路由名称
public static final String DELAY_ROUTING_KEY = "all"; public static final String REGISTER_QUEUE_NAME = "dev.book.register.queue";
public static final String REGISTER_EXCHANGE_NAME = "dev.book.register.exchange";
public static final String ROUTING_KEY = "all"; /**
* 返回一个延迟队列
* @return
*/
@Bean
public Queue delayProcessQueue(){
Map<String,Object> parms = new HashMap<>();
//x-dead-letter-exchange声明了队列里的死信转发到DLX交换机
parms.put("x-dead-letter-exchange",REGISTER_EXCHANGE_NAME);
//x-dead-letter-routing-key 声明了这些死信在转发时携带的routing-key名称
parms.put("x-dead-letter-routing-key",ROUTING_KEY);
return new Queue(REGISTER_DELAY_QUEUE,true,false,false,parms);
} @Bean
public DirectExchange delayExchange(){
return new DirectExchange(REGISTER_DELAY_EXCHANGE);
} /**
* 需要将一个队列绑定到交换机上,要求该消息与一个特定的路由键完全匹配
* 这是一个完整的匹配。如果一iGetter队列绑定到该交换机上要求路由建"dog",则只有被标记为"dog"的消息才会被转发,也不会转发dog。xxx 只会转发dog
*/
@Bean
public Binding dlxBinding(){
return BindingBuilder.bind(delayProcessQueue()).to(delayExchange()).with(DELAY_ROUTING_KEY);
} @Bean
public Queue registerBookQueue(){
return new Queue(REGISTER_QUEUE_NAME,true);
} @Bean
public TopicExchange registerBookTopicExchange(){
return new TopicExchange(REGISTER_EXCHANGE_NAME);
} @Bean
public Binding registerBookBinding(){
return BindingBuilder.bind(registerBookQueue()).to(registerBookTopicExchange()).with(ROUTING_KEY);
} }
实体类
package com.spring.boot.bean;
import java.io.Serializable;
public class Book implements Serializable{
private Integer id;
private String name;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
控制器
创建BookController用于消息发送工作
package com.spring.boot.controller; import com.spring.boot.bean.Book;
import com.spring.boot.utils.RabbitConfig;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.AbstractJavaTypeMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import java.time.LocalDateTime; @RestController
@RequestMapping("/books")
public class BookController { //spring boot 2.x版本推荐构造器注入 而不是属性注入
private final RabbitTemplate rabbitTemplate; @Autowired
public BookController(RabbitTemplate rabbitTemplate) {
this.rabbitTemplate = rabbitTemplate;
} @GetMapping("/defaultMessage")
public void defaultMessage() {
Book book = new Book();
book.setId(1);
book.setName("hello RabbitMQ"); //添加延迟队列
this.rabbitTemplate.convertAndSend(RabbitConfig.REGISTER_DELAY_EXCHANGE,RabbitConfig.DELAY_ROUTING_KEY,book,message -> {
//TODO 第一句是可要可不要,根据自己需要自行处理
message.getMessageProperties().setHeader(AbstractJavaTypeMapper.DEFAULT_CONTENT_CLASSID_FIELD_NAME,Book.class.getName());
//TODO 如果配置了 parms.put("x-message-ttl",5*1000);那么这一句也可以省略 根据具体的业务需要是提前声明还是发送时候自己控制时间
message.getMessageProperties().setExpiration(5 * 1000 + "");
return message;
}); System.out.println("发送时间 :"+LocalDateTime.now()); } }
消息消费者
默认情况下spring-boot-data-amqp是自动ACK机制,不管是否执行成功都清除掉队列中的数据。手动ACK是为了当队列中的任务发生异常时,取消此操作,仍然保存在队列中
package com.spring.boot.handler; import com.rabbitmq.client.Channel;
import com.spring.boot.bean.Book;
import com.spring.boot.utils.RabbitConfig;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component; import java.io.IOException;
import java.text.MessageFormat;
import java.time.LocalDateTime; @Component
public class BookHandler { //TODO
@RabbitListener(queues = {RabbitConfig.REGISTER_QUEUE_NAME})
public void listenerDelayQueue(Book book, Message message,Channel channel) throws IOException { System.out.println(MessageFormat.format("[listenerDelayQueue 监听的消息] - [消费时间] - [{0}] - [{1}]", LocalDateTime.now(), book.toString())); try{
// TODO 通知MQ 已经被消费完成 可以ACK了
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (IOException e) {
//TODO 处理失败,重新压入MQ
channel.basicRecover();
e.printStackTrace();
}
}
}
启动程序,输入地址 http://localhost:8088/books/defaultMessage 进行测试
发送时间 :2018-06-13T15:07:43.766
消息发送成功:correlationData:null,ack:true,cause:null
------5秒后------
[listenerDelayQueue 监听的消息] - [消费时间] - [2018-06-13T15:07:48.771] - [com.spring.boot.bean.Book@2887a177]
Spring Boot (26) RabbitMQ延迟队列的更多相关文章
- Spring Boot 实现 RabbitMQ 延迟消费和延迟重试队列
本文主要摘录自:详细介绍Spring Boot + RabbitMQ实现延迟队列 并增加了自己的一些理解,记录下来,以便日后查阅. 项目源码: spring-boot-rabbitmq-delay-q ...
- Spring Boot (25) RabbitMQ消息队列
MQ全程(Message Queue)又名消息队列,是一种异步通讯的中间件.可以理解为邮局,发送者将消息投递到邮局,然后邮局帮我们发送给具体的接收者,具体发送过程和时间与我们无关,常见的MQ又kafk ...
- Spring Boot 之 RabbitMQ 消息队列中间件的三种模式
开门见山(文末附有消息队列的几个基本概念) 1.直接模式( Direct)模式 直白的说就是一对一,生产者对应唯一的消费者(当然同一个消费者可以开启多个服务). 虽然使用了自带的交换器(Exchang ...
- 85. Spring Boot集成RabbitMQ【从零开始学Spring Boot】
这一节我们介绍下Spring Boot整合RabbitMQ,对于RabbitMQ这里不过多的介绍,大家可以参考网络上的资源进行安装配置,本节重点是告诉大家如何在Spring Boot中使用Rabbit ...
- Spring Boot (十三): Spring Boot 整合 RabbitMQ
1. 前言 RabbitMQ 是一个消息队列,说到消息队列,大家可能多多少少有听过,它主要的功能是用来实现应用服务的异步与解耦,同时也能起到削峰填谷.消息分发的作用. 消息队列在比较主要的一个作用是用 ...
- Spring Boot 集成 RabbitMQ 实战
Spring Boot 集成 RabbitMQ 实战 特别说明: 本文主要参考了程序员 DD 的博客文章<Spring Boot中使用RabbitMQ>,在此向原作者表示感谢. Mac 上 ...
- C# RabbitMQ延迟队列功能实战项目演练
一.需求背景 当用户在商城上进行下单支付,我们假设如果8小时没有进行支付,那么就后台自动对该笔交易的状态修改为订单关闭取消,同时给用户发送一份邮件提醒.那么我们应用程序如何实现这样的需求场景呢?在之前 ...
- RabbitMQ延迟队列
rabbitmq延迟队列 rabbitmq实现延迟队列用了rabbitmq-delayed-message-exchange插件,需要提前安装,并启用. 原理 其原理是通过Exchange来实现延迟功 ...
- RabbitMQ延迟队列插件安装
RabbitMQ延迟队列插件安装 一.下载插件 下载地址:https://www.rabbitmq.com/community-plugins.html 二.把下载的插件放到指定位置 下载的文件为zi ...
随机推荐
- redis学习——数据类型
一.内容简介 Redis不仅仅是简单的key-value 存储器,同时也是一种data structures server.传统的key-value是指支持使用一个key字符串来索引value字符串的 ...
- pace.js – 网页自动加载进度条插件
网站顶部的页面加载进度条是怎么实现的,页面的加载进度百分比,有时候获取是比较麻烦的,当然也可以利用一些优秀的JavaScript插件来实现,今天就为大家介绍这样子的一款插件:pace.js. [官方网 ...
- Codeforces Round #249 (Div. 2) 总结
D.E还是很难的.....C不想多说什么... A:提意:给出每一组人的个数,以及一次车载容量,求出最少需要多少次才能载走所有的人. water: http://codeforces.com/cont ...
- Linux下汇编语言学习笔记73 ---
这是17年暑假学习Linux汇编语言的笔记记录,参考书目为清华大学出版社 Jeff Duntemann著 梁晓辉译<汇编语言基于Linux环境>的书,喜欢看原版书的同学可以看<Ass ...
- nyoj_278_排队_201403282135
排队 时间限制:3000 ms | 内存限制:65535 KB 难度:1 描述 周末了,软件ACM的队员准备玩玩游戏,娱乐一下,CY想了一个好主意,所有队员站成一个圈,从1开始报数,凡是报出指 ...
- 洛谷 P1993 小K的农场
P1993 小K的农场 题目描述 小K在MC里面建立很多很多的农场,总共n个,以至于他自己都忘记了每个农场中种植作物的具体数量了,他只记得一些含糊的信息(共m个),以下列三种形式描述: 农场a比农场b ...
- Windows 10+Ubuntu 16.04双系统切换后时间不同步的问题解决
主要时时区不对造成的,少了8个小时,解决方法: 在Ubuntu的终端下输入以下命令: sudo timedatectl set-local-rtc 1 参考: http://blog.csdn.net ...
- eventlet
项目中要模拟openstack的swift的多路write功能.初步设想是用python的eventlet来实现多线程.比如在write的时候要同时写入两个storage,就用eventlet生成2个 ...
- Oracle Multitenant Environment (四) Create One or More CDBs
Using the CREATE DATABASE Statement to Create a CDB This section describes creating a CDB using the ...
- android (13) Fragment使用下
一.Fragment使用: 要在你的activity中管理Fragment,须要使用FragmentManager,能够通过getFragmentManager(),这里注意要是在v4包要用getSu ...