RabbitMQ
一、MQ
1、介绍
消息队列(Message Queue,简称MQ),本质是个队列,FIFO先入先出,只不过队列中存放的内容是message而已。 其主要用途:不同进程Process/线程Thread之间通信。
MQ框架非常之多,比较流行的有RabbitMq、ActiveMq、ZeroMq、kafka,以及阿里开源的RocketMQ
2、特点
1.系统解耦
交互系统之间没有直接的调用关系,只是通过消息传输,故系统侵入性不强,耦合度低。
2.提高系统响应时间
例如原来的一套逻辑,完成支付可能涉及先修改订单状态、计算会员积分、通知物流配送几个逻辑才能完成;通过MQ架构设计,就可将紧急重要(需要立刻响应)的业务放到该调用方法中,响应要求不高的使用消息队列,放到MQ队列中,供消费者处理。
3.为大数据处理架构提供服务
通过消息作为整合,大数据的背景下,消息队列还与实时处理架构整合,为数据处理提供性能支持
4.Java消息服务——JMS
Java消息服务(Java Message Service,JMS)应用程序接口是一个Java平台中关于面向消息中间件(MOM)的API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。
JMS中的P2P和Pub/Sub消息模式:点对点(point to point, queue)与发布订阅(publish/subscribe,topic)最初是由JMS定义的。这两种模式主要区别或解决的问题就是发送到队列的消息能否重复消费(多订阅)。
3、场景
1.异步通信
有些业务不想也不需要立即处理消息。消息队列提供了异步处理机制,允许用户把一个消息放入队列,但并不立即处理它。想向队列中放入多少消息就放多少,然后在需要的时候再去处理它们。
2.解耦
降低工程间的强依赖程度,针对异构系统进行适配。在项目启动之初来预测将来项目会碰到什么需求,是极其困难的。通过消息系统在处理过程中间插入了一个隐含的、基于数据的接口层,两边的处理过程都要实现这一接口,当应用发生变化时,可以独立的扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束。
3.冗余
有些情况下,处理数据的过程会失败。除非数据被持久化,否则将造成丢失。消息队列把数据进行持久化直到它们已经被完全处理,通过这一方式规避了数据丢失风险。许多消息队列所采用的”插入-获取-删除”范式中,在把一个消息从队列中删除之前,需要你的处理系统明确的指出该消息已经被处理完毕,从而确保你的数据被安全的保存直到你使用完毕。
4.扩展性
因为消息队列解耦了你的处理过程,所以增大消息入队和处理的频率是很容易的,只要另外增加处理过程即可。不需要改变代码、不需要调节参数。便于分布式扩容。
5.过载保护
在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量无法提取预知;如果以为了能处理这类瞬间峰值访问为标准来投入资源随时待命无疑是巨大的浪费。使用消息队列能够使关键组件顶住突发的访问压力,而不会因为突发的超负荷的请求而完全崩溃。
6.可恢复性
系统的一部分组件失效时,不会影响到整个系统。消息队列降低了进程间的耦合度,所以即使一个处理消息的进程挂掉,加入队列中的消息仍然可以在系统恢复后被处理。
7.顺序保证
在大多使用场景下,数据处理的顺序都很重要。大部分消息队列本来就是排序的,并且能保证数据会按照特定的顺序来处理。
8.缓冲
在任何重要的系统中,都会有需要不同的处理时间的元素。消息队列通过一个缓冲层来帮助任务最高效率的执行,该缓冲有助于控制和优化数据流经过系统的速度。以调节系统响应时间。
9.数据流处理
分布式系统产生的海量数据流,如:业务日志、监控数据、用户行为等,针对这些数据流进行实时或批量采集汇总,然后进行大数据分析是当前互联网的必备技术,通过消息队列完成此类数据收集是最好的选择。
二、RabbitMQ
1、介绍
RabbitMQ是部署最广泛的开源消息代理,是最受欢迎的开源消息代理之一,在全球范围内的小型初创企业和大型企业中都得到使用。支持多种消息传递协议,满足大规模,高可用性的要求
官网:点击进入
中文教程网:点击进入
RabbitMQ支持AMQP协议,高级消息队列协议
底层实现的语言:Erlang
AMQP即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同开发语言等条件的限制。 优点:可靠、通用
2、安装
基于Docker安装RabbitMQ
#1.创建并运行
docker run -d --name rabbitmq -p15672:15672 -p 5672:5672 rabbitmq:management
#2.访问测试
http://服务器ip:15672
###默认账号和密码:guest/guest
3、使用
1、导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2、配置文件
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
virtual-host: /
1、简单模式
一个队列对应一个消息发送者和一个消息消费者
1、创建队列
@Configuration
public class RabbitMQConfig {
// 创建队列
@Bean
public Queue createQ(){
return new Queue("boot_queue",true,false,false);
}
}
2、发布消息
@RestController
public class MqController {
@Autowired
private RabbitTemplate template;
//普通消息
@GetMapping("/api/mq/sendmsg/{msg}")
public String t1(@PathVariable String msg){
//发送消息
template.convertAndSend("","boot_queue",msg);
return template.getUUID();
}
3、监听消费
@Component //ioc
@RabbitListener(queues = "q21112") //rabbitMQ消费者 监听指定的队列
public class QueueListener {
@RabbitHandler //修饰方法,这个方法可以接收队列中的消息
public void handler(String msg){
System.out.println("消费者:"+msg);
}
}
2、工作模式
Worker(又名:任务队列 )的主要思想是避免立即执行资源密集型任务,而不得不等待它完成。
消费者竞争消费消息,一个消息可以被多个消费者来抢夺,最终实现一个消息只能被消费一次!
解决:高并发的消息生成
如果说目前消息的提供者生成的消息速度过快,那么就可以实现多个消费者共同来消费这些消息,这种情况下,就可以使用工作模式
比普通模式多了一个消费者即可
3、订阅模式
Pub/Sub模式:发布订阅模式,可以实现一个消息被多个消费者同时消费
采用的模式是发送者将消息发送到交换器中。交换器再将消息进行推送到队列中,每个队列又有自己的消费者
交换器:一方面,它接收来自生产者的消息,另一方面,将它们转发给队列
采用交换器 有四种方式
1.fanout:直接转发:交换器获取消息,直接将消息全部转发到绑定的所有队列中
配置文件
/**
* 订阅模式
*/
//@Configuration
public class RabbitMQConfig2 {
public static final String EXCHANGE_NAME = "boot_fanout_exchange";
public static final String QUEUE_NAME = "boot_queue";
public static final String QUEUE_NAME2 = "boot_queue2";
//1.交换机
/**
* Fanout类型的交换机会将消息分发到所有的绑定队列,没有RoutingKey的概念
* @return
*/
@Bean("bootExchange")
public FanoutExchange bootExchange(){
return new FanoutExchange(EXCHANGE_NAME);
}
//2.Queue 队列1
@Bean("bootQueue")
public Queue bootQueue(){
return new Queue(QUEUE_NAME);
}
//2.Queue 队列2
@Bean("bootQueue2")
public Queue bootQueue2(){
return new Queue(QUEUE_NAME2);
}
//绑定交换机和队列 两种方式绑定看用哪种简洁吧
// @Bean
// public Binding createBE1(){
// return BindingBuilder.bind(bootQueue()).to(bootExchange());
// }
@Bean
public Binding bindQueueExchange(@Qualifier("bootQueue") Queue queue, @Qualifier("bootExchange") FanoutExchange exchange){
return BindingBuilder.bind(queue).to(exchange);
}
@Bean
public Binding bindQueueExchange2(@Qualifier("bootQueue2") Queue queue, @Qualifier("bootExchange") FanoutExchange exchange){
return BindingBuilder.bind(queue).to(exchange);
}
}
发布
//订阅模式
// 1、1个生产者,多个消费者
// 2、每一个消费者都有自己的一个队列
// 3、生产者没有将消息直接发送到队列,而是发送到了交换机
// 4、每个队列都要绑定到交换机
// 5、生产者发送的消息,经过交换机,到达队列,实现,一个消息被多个消费者获取的目的
// ""不能省略,否则会发送成功,但是接受不到
@Test
public void TestPublist(){
rabbitTemplate.convertAndSend(RabbitMQConfig2.EXCHANGE_NAME,"","这是订阅者模式");
}
消费者同上 修改队列名字即可
2、direct
路由匹配转发:通过路由匹配,路由这里只支持精确路由。
1)创建配置类
@Configuration
public class DirectConfig {
@Bean
public Queue directQueue1(){
return new Queue("directQueue1");
}
@Bean
public Queue directQueue2(){
return new Queue("directQueue2");
}
@Bean
public DirectExchange directExchange(){
return new DirectExchange("directExchange");
}
@Bean
public Binding bingDirectQueue1(){
return BindingBuilder.bind(directQueue1()).to(directExchange()).with("zhangsan");
}
@Bean
public Binding bingDirectQueue2(){
return BindingBuilder.bind(directQueue2()).to(directExchange()).with("lisi");
}
}
2)创建生产者
@Component
public class DirectProducer {
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private DirectExchange directExchange;
public void send(){
System.out.println("DirectProducer");
rabbitTemplate.convertAndSend(directExchange.getName(),"zhangsan","zhangsanContent");
rabbitTemplate.convertAndSend(directExchange.getName(),"lisi","lisiContent");
}
}
3)创建两个消费者
@Component
@RabbitListener(queues = "directQueue1")
public class DirectCustomer_01 {
@RabbitHandler
public void receive(String content){
System.out.println("DirectCustomer_01:"+content);
}
}
@Component
@RabbitListener(queues = "directQueue2")
public class DirectCustomer_02 {
@RabbitHandler
public void receive(String content){
System.out.println("DirectCustomer_02:"+content);
}
}
4)在测试类中添加对象和方法进行测试
@Autowired
private DirectProducer directProducer;
@Test
public void testDirectProducer(){
directProducer.send();
}
3、topic
路由匹配转发:通过路由匹配实现操作,而且这个路由支持模糊匹配
*(星号)可以代替一个单词。
#(哈希)可以替代零个或多个单词
1)创建配置类
@Configuration
public class TopicConfig {
@Bean
public Queue topicQueue1(){
return new Queue("topicQueue1");
}
@Bean
public Queue topicQueue2(){
return new Queue("topicQueue2");
}
@Bean
public TopicExchange topicExchange(){
return new TopicExchange("topicExchange");
}
@Bean
public Binding bingTopicQueue1(){
return BindingBuilder.bind(topicQueue1()).to(topicExchange()).with("wangwu.*");
}
@Bean
public Binding bingTopicQueue2(){
return BindingBuilder.bind(topicQueue2()).to(topicExchange()).with("zhaoliu.#");
}
}
2)创建生产者
@Component
public class TopicProducer {
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private TopicExchange topicExchange;
public void send(){
System.out.println("TopicProducer");
rabbitTemplate.convertAndSend(topicExchange.getName(),"wangwu.abc","wangwuContent");
rabbitTemplate.convertAndSend(topicExchange.getName(),"zhaoliu.xyz.qwer","zhaoliuContent");
}
}
创建两个消费者
@Component
@RabbitListener(queues = "topicQueue1")
public class TopicCustomer_01 {
@RabbitHandler
public void receive(String content){
System.out.println("TopicCustomer_01:"+content);
}
}
@Component
@RabbitListener(queues = "topicQueue2")
public class TopicCustomer_02 {
@RabbitHandler
public void receive(String content){
System.out.println("TopicCustomer_02:"+content);
}
}
在测试类中添加对象和方法进行测试
@Autowired
private TopicProducer topicProducer;
@Test
public void testTopicProducer(){
topicProducer.send();
}
4、headers
headers:头交换器模式 通过每次消息的消息头进行交换器的匹配对应的属性:x-match
支持的2种取值:
all: 默认 head中的键值对和消息的键值对完全匹配,才可以实现转发
any: 只需要匹配任意一个,就可以实现消息的转发
不常用省略
4、手动Ack
从队列读取到消息后有可能消费的时候,造成异常了消息还是直接被自动删除,但是存入数据库造成异常回滚,当前消息已经自动删除。
在 application.properties 中添加配置
spring.rabbitmq.listener.simple.acknowledge-mode=manual
在Topic模式的中添加 AckCustomer 演示
@Component
@RabbitListener(queues = "topicQueue1")
public class AckConsumer {
@RabbitHandler
public void receive(String content,Channel channel, Message message)throws Exception{
try {
byte[] body = message.getBody();
String msg = new String(body);
System.out.println(msg);
//System.out.println(content);
//int i = 1 / 0;
//手动Ack,确定消费消息
// deliveryTag:该消息的index
// multiple:是否批量.true:将一次性ack所有小于deliveryTag的消息。
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}catch (Exception e){
//丢弃
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,false);
}
}
}
5、RabbitMQ事务
RabbitMQ的事务是对AMQP协议的实现,通过设置
Channel
的模式来完成,具体操作如下:
channel.txSelect(); //开启事务
// ....本地事务操作
channel.txCommit(); //提交事务
channel.txRollback(); //回滚事务
特别说明:RabbitMQ的事务机制是同步操作,会极大的降低RabbitMQ的性能。
6、Confirm机制
由于RabbitMQ的事务性能的问题,于是就又推出了发送方确认模式。
在 application.properties 中添加配置
# 配置开启Confirm和Return
spring.rabbitmq.publisher-confirm-type= simple
spring.rabbitmq.publisher-returns= true
创建配置类
@Configuration
public class PublisherConfirmAndReturnConfig implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void initMethod(){
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnCallback(this);
}
@Override
public void confirm(CorrelationData correlationData, boolean ack, String s) {
if(ack){
System.out.println("到达交换机");
}else{
System.out.println("没有到达交换机");
}
}
/**
* Return 消息机制用于处理一个不可路由的消息。在某些情况下,如果我们在发送消息的时候,当前的 exchange 不存在或者指定路由 key 路由不到,这个时候我们需要监听这种不可达的消息
* 就需要这种return机制
* @param message
* @param replyCode
* @param replyText
* @param exchange
* @param routingKey
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("没有到达队列");
}
}
7、死信队列
列延时队列:跟普通队列比起来,多了一个延时的作用
延时队列中的消息都具备有效期,在指定的时间内需要完成消息的处理
延时队列就是主要用来存储,需要在指定时间内,去处理的消息队列
1.超时订单订单的支付超时时间,比如一些产品,订单下单之后,要求指定时间内必须完成付款
比如:
12306 30分钟需要完成付款
淘宝 订单支付超时为24小时
兴盛优选 订单支付超时15分钟
2.订单的默认评价
订单如果超过一定期限还是有人未评价,这时就需要自动好评
3.订单的默认收货
订单收货时间超时,就会执行默认收货
4.用户注册之后的激活超时
用户注册之后,要求多久之内要实现激活
5.预约模块
预约功能的提醒
6.订单退款
订单发起退款请求,如果超过一定期限,还是没人处理,自动处理
ps:其实上述的这些都可以采用 定时任务实现。或者是Redis的失效监听实现
1.创建队列、交换器、绑定
//1.队列 2个 1.需要设置有效期信息
@Bean
public Queue createQ2(){
Map<String,Object> params=new HashMap<>();
//设置队列的参数信息
//1.设置死信交换器
params.put("x-dead-letter-exchange","deadexchange");
//2.设置死信路由关键字匹配
params.put("x-dead-letter-routing-key","rkorderto");
//3.设置消息的有效期 1分钟 单位毫秒
params.put("x-message-ttl",60000);
return QueueBuilder.durable("qttl213").withArguments(params).build();
}
@Bean
public Queue createQ3(){
return new Queue("ordertimeout");
}
//2.交换器 direct
@Bean
public DirectExchange cearetDe(){
return new DirectExchange("deadexchange");
}
//3.实现绑定 让交换器和队列有联系
@Bean
public Binding createBd1(DirectExchange de){
return BindingBuilder.bind(createQ3()).to(de).with("rkorderto");
}
2.编写代码发送消息 qttl213
//发送延迟消息
@GetMapping("/api/mq/sendyc.do")
public String send(String msg){
template.convertAndSend("","qttl213",msg);
return "OK";
}
3.消息消费 监听的队列:ordertimeout 死信队列
@Component //ioc 创建对象
@RabbitListener(queues = "ordertimeout") //设置要监听的队列
public class MqDeadYcListener {
@RabbitHandler //修饰方法,确定处理消息的方法(不能有返回值,参数的类型和消息类型一致)
public void handler(String m){
System.err.println("延迟消息--->"+m);
}
}
注意:
RabbitMQ 3.6.x 之前,我们一般采用死信队列+TTL过期时间来实现延迟队列,通过TTL+死信队列完成。
从RabbitMQ 3.6.x 开始,RabbitMQ 官方直接提供了延迟队列插件,可以下载放置到 RabbitMQ 根目录下的 plugins 下,这样就可以很轻松的实现延迟队列效果了。
1、安装插件 链接:下载相对应的版本即可
下载ez后
2、上传到服务器上执行docker cp rabbitmq_delayed_message_exchange-3.8.9-0199d11c.ez rabbitmq:/plugins
3、执行:docker exec -it rabbitmq /bin/bash
4、进入插件目录执行 rabbitmq-plugins enable rabbitmq_delayed_message_exchange-3.8.9-0199d11c
5、执行:exit
6、执行:docker restart rabbitmq
使用:
package com.zhk.consumer.test;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.CustomExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class Config {
/**
* TODO 1、定义交换机 delayed.exchange.user.register
* 2、定义队列 delayed.queue.user.register
* 3、路由KEY delayed.key.user.register
* 4、队列绑定交换机路由
*/
/**
* TODO 定义交换机
*/
@Bean
public CustomExchange delayedExchangeUserRegister(){
/**
* String name, 交换机名称
* String type, 交换机消息类型(x-delayed-message)
* boolean durable, 是否持久化
* boolean autoDelete,是否删除
* Map<String, Object> arguments // 队列中的消息什么时候会自动被删除?
* arguments.put("x-message-ttl",10000); //设置过期时间
* arguments.put("x-expires", 10000); //x-expires用于当多长时间没有消费者访问该队列的时候,该队列会自动删除,
* arguments.put("x-max-length", 4); //x-max-length:用于指定队列的长度,如果不指定,可以认为是无限长,例如指定队列的长度是4,当超过4条消息,前面的消息将被删除,给后面的消息腾位
*/
Map<String, Object> arguments=new HashMap<>();
arguments.put("x-delayed-type","direct");
return new CustomExchange("delayed.exchange.user.register","x-delayed-message",
true,false,arguments);
}
/**
* TODO 定义队列 delayed.queue.user.register
*/
@Bean
public Queue delayedQueueUserRegister(){
return new Queue("delayed.queue.user.register");
}
/**
* TODO 3 绑定 交换机 队列 路由KEY
* @return
*/
@Bean
public Binding bindDelayedQueueDelayedExchangeUserRegister(
Queue delayedQueueUserRegister,
CustomExchange delayedExchangeUserRegister){
return BindingBuilder.bind(delayedQueueUserRegister).to(delayedExchangeUserRegister).with("delayed.key.user.register").noargs();
}
}
package com.zhk.consumer.test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping
public class Test {
@Autowired
private RabbitTemplate rabbitTemplate;
@RequestMapping
public String send() {
rabbitTemplate.convertAndSend("delayed.exchange.user.register", "delayed.key.user.register", "demo", msg->{
msg.getMessageProperties().setDelay(2000);
return msg;
});
return "success";
}
}
package com.zhk.consumer.test;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;
@Service
public class MQListener {
@RabbitListener(queues = {"delayed.queue.user.register"})
public void sendEmail(String uname){
System.out.println(uname);
}
}
8、特点
RabbitMQ保证消息幂等性
幂等性:无论一个消息,接收多少次,也不会出现重复消费的问题比如:支付,用户手速过快,导致快速的多次的发起支付请求,但是只能有一次进行。
MQ有可能会出现一个消息发送多次,导致消息的重复性
什么情况下,会出现消息的重复?
1.网络抖动
2.网络闪断
3.端点异常 服务端或者发送端或者消费端
消息出现非幂等性,就会导致消息的重复
解决方案:
1.唯一ID+指纹码
唯一ID 可以使用:雪花算法等等 这种分布式唯一ID生成器
指纹码:就是一段密文,加密规则各不相同
常用:时间戳+随机码+唯一ID+业务ID 采用一定加密技术 进行密文生成
2.基于Redis的原子性实现
Redis的原子性,自增啥的都可以,关键Redis支持集群
使用cloud stream 装配桥
配置配置文件
<!--stream rabbit-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
发送
@Autowired
private StreamBridge streamBridge;
streamBridge.send("method-in-0", t); //方法名自定义,发送值
log.info("消息:{}发送成功",method);
监听
//这里接收rabbitmq的条件是参数为Consumer 并且 方法名和supplier方法名相同
//这里的返回值是一个匿名函数 返回类型是consumer 类型和提供者的类型一致
//supplier发送的exchange是 method 这里只需要用method方法名即可
@Component
public class TemplateMqListener {
@Bean
public Consumer<TemplateBean> method() {
return templateBean -> {
};
}
}
配置文件
spring:
cloud:
stream:
binders:
default-rabbit-binder:
type: rabbit
environment:
spring:
rabbitmq:
host: rabbitmq
port: 5672
username: guest
password: guest
virtual-host: /
==================================
设置分组管道等配置 上述是简单配置 只要按照固定格式就行
spring:
cloud:
stream:
function:
definition: send;mq;
binders:
defaultRabbit:
type: rabbit
environment:
spring:
rabbitmq:
addresses: rabbitmq # 服务器IP
port: 5672 # 服务器端口
username: guest # 用户名
password: guest # 密码
virtual-host: /
bindings: #服务的整合处理
send-in-0: #生产者通道的名称 需和生产者 代码里面 通道名称一致
destination: studyExchange #studyExchange #这个名字是一个通道的名称
content-type: application/json #设置消息类型,本次为json,文本则设置"text/plain"
send-out-0: #消费者通道的名称
destination: studyExchange #studyExchange 自定义一个通道的名称
content-type: application/json #设置消息类型,本次为json,文本则设置"text/plain"
group: ijustecA # 自定义 分组名称
mq-out-0: #消费者通道的名称
destination: mqtest #studyExchange 自定义一个通道的名称
content-type: application/json #设置消息类型,本次为json,文本则设置"text/plain"
group: testb # 自定义 分组名称
mq-in-0: #生产者通道的名称 需和生产者 代码里面 通道名称一致
destination: mqtest #studyExchange #这个名字是一个通道的名称
content-type: application/json #设置消息类型,本次为json,文本则设置"text/plain"