1.1什么是MQ消息中间件
MQ全称negMessage Queue (消息队列),是在消息的传输过程中保存信息的容器,他是一ing用程序和应用程序之间的通信方法
1.2为什么使用MQ
在实际开发中,可以将一些无需返回且好事的操作哦提取出来,进行异步处理,二这种异步处理的方式,大大的节省了服务器的请求响应时间,从而提高了系统的运行效率
1.3MQ的三大好处
1.3.1应用解耦
这个很好理解,一个项目中有好多的子系统,一旦其中一个子项目出现了错误,就会连累其他的子系统一起报错,导致整个系统出现瘫痪等问题,如以下问题
一旦库存系统出现错位,就会导致下单异常,导致出现一堆的问题,当转变为消息队列时,系统之间的调用问题会减少很多,比如物流系统因为出现错误,但是不会影响用户的订单问题,只会将用的订单放在消息队列中等待物流系统修复完成,发送货物
1.3.2异步提速
以上操作要完成下单需要花费的时间为:20+300+300+300=920ms用户点击完下单,需要等待很长时间响应,速度太慢了
使用MQ可以解决这个问题,
用户点击完成下单按钮后,只需等待25ms就能得到下单响应了(20+5)
提升了用户体验和形同的吞吐量(单位时间内处理请求的数目)
1.3.3削峰填谷
削峰填谷意思就是说,每个系统他都会有一段时间为高峰期,但是他的项目配置又不能使这个服务器处理这么多请求,但是又不能为了这一段的高峰期而,花费大价钱多配置服务器,所以就使用了一个队列,将这些请求,全部放入其中,并且无限向后排队,每次服务器只会获取他所能处理请求的数量,削的高峰期,填的低峰期
使用MQ后,可以提高系统的稳定性
1.4使用MQ的劣势
1.4.1系统的可用性降低
系统引入的外部依赖越多,系统稳定性越差,一旦MQ宕机,就会对业务照成影响
如何保证MQ的高可用?
1.4.2系统复杂度提高
MQ的加入大大增加了系统的复杂度,以前系统间是同步的远程调用,现在是通过MQ进行异步调用
如何保证消息没有被重复消费?怎么处理消息丢失情况?那么保证消息传递的说是顺序性?
1.4.3一致性问题
A系统处理完业务,通过MQ给B、C、D、三个系统发消息数据,如果b系统c系统处理成功,d系统处理失败,
如何保证消息数据处理的一致性?
1.5常见的MQ组件
如RabbitMQ,RocketMQ,ACtiveMQ,Kafka,ZeroMQ等,需要结合自身需求以及MQ产品特征
2.1RabbitMQ的概述
2007 年发布,是一个在 AMQP(高级消息队列协议)基础上完成的,可复用的企业消息系统,是当前最主流的消息中间件之一。 . RabbitMQ是一个由erlang开发的AMQP(Advanced Message Queue 高级消息队列协议 )的开源实现,由于erlang 语言的高并发特性,性能较好,本质是个队列,FIFO 先入先出,里面存放的内容是message . RabbitMQ是一个消息中间件:它接受并转发消息。你可以把它当做一个快递站点,当你要发送一个包裹时,你把你的包裹放到快递站,快递员最终会把你的快递送到收件人那里,按照这种逻辑RabbitMQ是一个快递站,一个快递员帮你传递快件。RabbitMQ与快递站的主要区别在于,它不处理快件而是接收,存储和转发消息数据
2.2RabbitMQ的原理
名词解释:
Broker:接收和分发消息的应用,RabbinMQ Server就是Message Broker
Connection:publisher / consumer 和 broker之间的TCP连接
Channel:如果每一次访问RabbinMQ都会建立一个 TCP Connection,在消息量大的时候,开销是巨大的,运行效率也会十分低下,Channel 是在 connection 内部建立的逻辑连接,如果应用程序支持多线程,通常每个thread创建单独的 channel 进行通讯,AMQP method 包含了channel id 帮助客户端和message broker 识别 channel,所以 channel 之间是完全隔离的。Channel 作为轻量级的 Connection 极大减少了操作系统建立 TCP connection 的开销.
Exchange:message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发消息到queue 中去。常用的类型有:direct (point-to-point), topic (publish-subscribe) and fanout (multicast)
Queue:消息最终被送到这里等待 consumer 取走
Binding:exchange 和 queue 之间的虚拟连接,binding 中可以包含 routing key。Binding 信息被保存到 exchange 中的查询表中,用于 message 的分发依据
Virtual host:出于多租户和安全因素设计的,把 AMQP 的基本组件划分到一个虚拟的分组中,类似于网络中的 namespace 概念。当多个不同的用户使用同一个 RabbitMQ server 提供的服务时,可以划分出多个vhost,每个用户在自己的 vhost 创建 exchange/queue 等
重点
RabbitMQ 提供了 6 种工作模式:简单模式、work queues、Publish/Subscribe 发布与订阅模式、Routing 路由模式、Topics 主题模式、RPC 远程调用模式(远程调用,不太算 MQ;暂不作介绍)。 官网对应模式介绍:RabbitMQ Tutorials — RabbitMQ
3.1simple 简单模式
就只有一个生产者,一个队列,一个消费者
p:生产者,也就是客户、用户、发送请求或消息的程序
c:消费者,消息的接收者,也就是程序、项目、服务器,需要一致开启,等待queue里面的请求
queue:消息队列,图画中红色部分、也就是削峰填谷里,将高峰期削掉那一步放入的可以无限排队的队列、可以缓存消息,消费者从中获取信息
生产者:
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import javax.xml.soap.SOAPConnectionFactory;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
public class HelloPro {
public static void main(String[] args) throws IOException, TimeoutException {
//1设置连接对象的信息
ConnectionFactory factory = new ConnectionFactory();
//设置rabbitmq主机地址
factory.setHost("192.168.220.223");
//端口号
//账号
//密码
//设置虚拟主机
//2获取连接对象
Connection connection = factory.newConnection();
//3获取channel信道
Channel channel = connection.createChannel();
//4创建队列,如果存在就不创建,如果不存在则创建
/*
* String queue ,队列的名称,按照命名规则就可以
* boolean durable。 是否持久化
* boolean exclusive ,是否独占--当前chanel是否独占该队列
*boolean autoDelete 是否自动删除该队列
* Map《String,Object》arguments,该队列的属性参数 --null
* */
//hello为一个队列,如果没有就创建,有了就直接使用
channel.queueDeclare("hello",true,false,false,null);
//5发送消息
/*
* String exchange 交换机的名称--像简单模式没有交换机
* String routingKey 路由key,想简单模式,默认给定为队列的名称
* BasicProperties props 消息的属性 -- 现在给定null
* byte[] body --消息的内容
* */
String str="请求已发送";
//“”交换机,hello队列,null消息的属性,发送的请求
channel.basicPublish("","hello",null,str.getBytes());
//关闭资源
channel.close();
conntion.close();
}
}
消费者:
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class HelloPro {
public static void main(String[] args) throws IOException, TimeoutException {
//1设置连接对象的信息
ConnectionFactory factory = new ConnectionFactory();
//设置rabbitmq主机地址
factory.setHost("192.168.220.223");
//配置密码账号。。。
//2获取连接对象
Connection connection = factory.newConnection();
//3获取channel信道
Channel channel = connection.createChannel();
//4创建队列,如果存在就不创建,如果不存在则创建
//hello 队列名称,是否持久化为true,是否独占:false,是否删除:false,属性参数:null
channel.queueDeclare("hello",true,false,false,null);
//5监听消息
DefaultConsumer callback = new DefaultConsumer(channel) {
//一定重写该方法。
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body);
System.out.println("消息的内容:" + msg);
}
};
/*
String queue, 监听的队列名
boolean autoAck, 是否自动确认。
Consumer callback: 回调函数---当队列中有消息时就会自动触发该对象
*/
//hello 队列名,是否自动确认:true,回调函数:callback
channel.basicConsume("hello",true,callback);
//消费者不能关闭资源,需要一致监听消息队列
}
}
生产者每向队列发送一次请求,消费者就会在后台监听到队列里面有了请求,就会执行相应的方法
3.2Work queues(工作模式)
Work Queues :与入门程序的简单模式相比,多了一个或一些消费端,或者多个消费端共同消费同一个队列中的消息
使用场景:对于任务过重,或任务较多情况使用工具队列可以提高任务处理的的速度
差距只是多了一个消费者
总结:在一个队列中如果有多个消费者,那么消费者之间对于同一个消息的关系是--竞争--的关系
Work Queues对于任务过重或任务较多的情况使用工作队列可以提高任务处理的速度,如:短信服务器部署多个,只需要有一个节点成功发送即可
3.3 Publish/Subscribe(发布订阅模式)
在订阅模式中,多了一个中转器的东西(X,交换机)
p:生产者,发送消息的服务器,但是不在发送给队列(解决了工作模式中,消费者对请求的争夺)
X:Exchange 交换机 x 接收生产者发送的消息,另外,可以使用这个交换机对消息进行额外的操作,如递交给某个特别队列,或者将消息丢弃,操作方法有三种:
1.Fanout:广播,将消息交给所有绑定到交换机的队列
2.Direct:定向,把消息交给符合指定routing key 的队列
3.Topic:通配符,将消息嫁给符合路由模式的队列
c:消费者,服务器一直开放,需要一直关注队列的变化
生产者:
public class PublishSubscribeProduct {
public static void main(String[] args) throws Exception {
//1.设置连接对象的信息
ConnectionFactory factory=new ConnectionFactory();
factory.setHost("192.168.223.158");//设rabbitMq主机的地址:默认127.0.0.1
//2. 获取连接对象
Connection connection=factory.newConnection();
//3. 获取Channel信道
Channel channel = connection.createChannel();
//4. 创建交换机
channel.exchangeDeclare("fanout_exchange",BuiltinExchangeType.FANOUT,true);
//5. 创建队列
channel.queueDeclare("fanout_queue01",true,false,false,null);
channel.queueDeclare("fanout_queue02",true,false,false,null);
//6.交换机和队列绑定
/*
String queue,队列名
String exchange,交换机名
String routingKey: 路由key 因为广播模式没有路由key 所以填空
*/
channel.queueBind("fanout_queue01","fanout_exchange","");
channel.queueBind("fanout_queue02","fanout_exchange","");
//7. 发送消息
String msg="这时一个发布订阅工作模式";
channel.basicPublish("fanout_exchange", "", null, msg.getBytes());
//8.关闭资源
channel.close();
connection.close();
}
}
消费者:
1.交换机需要与队列进行绑定,绑定之后,一个消息可以被多个消费者收到
2.发布订阅模式与工作队列模式的区别:
--工作队列模式不用定义交换机,而发布/订阅莫斯需要定义交换机
--发布/订阅模式的生产放是面向交换机发送信息,工作队列模式的生产方式面向队列发送消息(底层会默认使用交换机)
--发布/订阅模式需要设置队列和交换机的绑定,工作队列模式不需要设置,实际上工作队列模式会将队列绑定到默认的交换机
3.4Routing(路由模式)
——队列与交换机的绑定,不能任意绑定,而是要指定一个路由key(RoutingKey)
——p在往x发送消息的同时,也需要指定消息的路由key
——交换机(Exchange)不在把消息交还给每一个绑定的队列,而是根据消息的路由key进行判断,只有队列的队列的key和消息的key完全一致,消费者才会收到消息
P:生产者,向交换机发送信息,会指定一个路径/路由key
X:交换机,接收生产者的消息,然后将消息提交给与队列路由key相同的队列
上图中,c1所对应的为error路径的
C1:消费者,其所在队列指定了需要路由key为error消息
C2:消费者,其所在队列指定了需要路由key,为info,error,warning的消息
生产者:
public class DirectProduct04 {
public static void main(String[] args)throws Exception {
//设置连接对象
ConnectionFactory factory=new ConnectionFactory();
//设置主机ip地址。。。端口号,账号,密码,虚拟主机都是默认值,可以设置
factory.setHost("192.168.223.158");
//获取连接对象
Connection connection = factory.newConnection();
//获取channel信道
Channel channel = connection.createChannel();
//创建交换机 名,类型,持久化
channel.exchangeDeclare("direct_exchange", BuiltinExchangeType.DIRECT,true);
//创建队列 队列名,。。。
channel.queueDeclare("direct_queue01",true,false,false,null);
channel.queueDeclare("direct_queue02",true,false,false,null);
//交换机和队列绑定 队列名,交换机名,路由key:这个error就是队列的key,跟消息的key比较,相同就会消费
channel.queueBind("direct_queue01","direct_exchange","error");
channel.queueBind("direct_queue02","direct_exchange","error");
channel.queueBind("direct_queue02","direct_exchange","info");
channel.queueBind("direct_queue02","direct_exchange","warning");
String msg="你好,李焕英";
channel.basicPublish("direct_exchange","info",null,msg.getBytes());
}
}
3.5 Topics(主题模式)
主题类型和direct相比,都是可以根据路由key把消息路由到不同的队列,只不过主题模式的交换机可以将队列在绑定路由key时使用通配符
路由key一般都是一个或者多个单词组成的,多个单词以.分割,如item.aaa
#匹配一个或多个词,*只匹配一个词,
如 in.# 可以匹配in.aaa.ccc/in.bbb.ccc ,但是in.*只能匹配in.aaa
信息自带的一个路由key,交换机用这个路由key和队列的路由key,进行比较,范围就是以上的,如果两者都在这个范围之内,交换机就会将或者消息/请求,发给这个队列的消费者处理
交换机向队列发送消息,使用绑定的信息,帮助交换机判断要将信息发向那个队列
生成者:
public class TopicPublish { public static void main(String[] args) throws Exception { //获取连接对象 ConnectionFactory factory=new ConnectionFactory(); //连接主机地址ip factory.setHost("192.168.223.158"); //获取连接对象 Connection connection = factory.newConnection(); //获取channel信道 Channel channel = connection.createChannel(); //创建交换机 交换机名,类型,持久化 channel.exchangeDeclare("topic_exchange", BuiltinExchangeType.TOPIC,true); //创建队列 队列名,持久化,独占,删除,属性 channel.queueDeclare("topic_queue01",true,false,false,null); channel.queueDeclare("topic_queue02",true,false,false,null); //交换机和队列绑定bind , 队列名 , 交换机名 ,绑定条件(所有符合条件的都会进行bind) channel.queueBind("topic_queue01","topic_exchange","*.orange.*"); channel.queueBind("topic_queue02","topic_exchange","*.*.rabbit"); channel.queueBind("topic_queue02","topic_exchange","lazy.#"); String msg="你好,李焕英"; channel.basicPublish("topic_exchange","lazy.orange",null,msg.getBytes()); } }
消费者不做改动
topic主题模式可以实现Pub/Sub发布与订阅模式和Routing路由模式的功能,只是Topic在配置routing key 的时候可以使用通配符,线的更加灵活
4.springboot和rabbing整合
(1)生产者端
1.创建生产者springboot工程
2.引入start,依赖坐标
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
编写yml配置文件,基本信息配置
server.port=8888 #rabbit的信息 spring.rabbitmq.virtual-host=/ #端口号 spring.rabbitmq.port=5672 #账号 spring.rabbitmq.username=guest #密码 spring.rabbitmq.password=guest #IP地址 spring.rabbitmq.host=ip地址
3.定义交换机,队列以及绑定关系的配置类
4.注入RabbitTemplate,调用方法,完成消息发送
@SpringBootTest class SpringbootRabbitProductApplicationTests { //springboot封装了一个工具类。该类提供了相应的方法。 @Autowired private RabbitTemplate rabbitTemplate; @Test void contextLoads() { //String exchange, String routingKey, Object message rabbitTemplate.convertAndSend("topic_exchange","lazy.orange","今天是个好日志"); } }
(2)消费者端
1.创建消费者SpringBoot工程
2.引入start,依赖坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
3.编写yml配置文件,基本信息配置
server.port=8888
#rabbit的信息
spring.rabbitmq.virtual-host=/
#端口号
spring.rabbitmq.port=和生产者的端口号不一样
#账号
spring.rabbitmq.username=guest
#密码
spring.rabbitmq.password=guest
#IP地址
spring.rabbitmq.host=ip地址
3.第一监听类,使用@RabbitListener注解完成队列监听
@Component
public class MyRabbitListener {
//把收到的消息封装到一个Message对象中
@RabbitListener(queues = {"topic_queue01"})
public void myListener01(Message message){
byte[] body = message.getBody();
String msg=new String(body);
System.out.println("收到的消息内容:"+msg);
System.out.println("根据消息写自己的业务代码");
}
}
小结:
SpringBoot提供了快速整合RabbitMQ的方式
基本信息再yml中配置,队列交换机以及绑定关系在配置类中使用Bean的方式配置
生产端直接注入RabbinTemplate完成消息发送
消费端直接使用@RabbitKistener完成消息接收
5.保证消息的稳定性
5.1消息的可靠传递
在开发环境中,由于一些外部因素,或者不可抗拒因素,导致rabbitmq,在RabbitMQ重启期间生产者消息投递失败,导致消息丢失,需要手动处理和回复,于是,就需要考虑mq 的消息传递的安全性,特别是如果mq集群不可用的时候,无法投递的雄安锡该如何处理
在使用mq的时候,作为消息发送方希望杜绝任何消息丢失或者投递失败场景,mq为我们提供了两种方式用来控制消息的投递可靠性模式
confirm 确认模式
return 退出模式
消息从producer到exchange则会返回一个confirmCallback
消息从exchange--》queue投递失败则会返回一个returnCallback
可以利用这两个callback控制消息的可靠性投递
confirm和return的实现
1.设置ConnectionFactory的publisher-confirm-type: correlated开启 确认模式
1.2使用rabbitTemplate.setConfirmCallback设置回调函数,当消息发送到exchange后回调confirm方法,在方法中判定ack,如果为true,则发送成功,如果为false,则发送失败,需要处理
2.设置ConnectionFactory的publisher-returns="true" 开启 退回模式
2.2使用rabbitTemplate.setReturnCallback设置退回函数,当消息从exchange路由到queue失败后执行回调函数returnedMessage。
5.2ACK确认机制
多个消费者同时收取消息,收取消息到一半,突然某个消费者挂掉,要保证此条消息不丢失,就需要acknowledgement机制,就是消费者消费完需要返回给交换机一个消息,表示它已完成这条消息的处理,交换机/服务器收到消费者的完成信息,才会删除这条请求
这样就会解决,既是一个消费者出了问题,没有同步消息给服务器,还有其他的消费端去消费,保证了消息的不丢失(因为消息者没有给服务器传回信息,服务器则不会删除消息)
总结:
保证消息的可靠性:
1、消息持久化:mq的消息默认存储在内存中,一旦服务器意外挂掉,消息就会丢失
持久化需要注意三个方面
Exchange 设置持久化 Queue 设置持久化 Message持久化发送:发送消息设置发送模式deliveryMode=2,代表持久化消息
2、生产方确认Confirm和Return机制
3、ACK确认机制
4、设置集群镜像模式
6.延迟队列
6.1TTL
TTL又称Time To Live 存活时间
指设置当前信息的存活时间,达到设定的存活时间后,如果没有被消费,就会被删除
mq可以对消息设置过期时间,也可以对整个队列(Queue)设置过期时间
总结:
设置队列的过期时间需要使用的参数:x-message-ttl
会对整个队列消息统一过期
设置消息过期时间使用参数:expiration ms
当该消息在队列头部时,会单独判断这个消息是否会过期
如果消息和队列都设置过期时间,以时间短的为准
6.2死信队列
死信队列:DLX Dead Letter Exchange(死信交换机)当消息成为DeadMessage后,可以被诚信发送到另一台交换机,就是死信交换机 DLX
死信消息是什么
1、队列长度达到限制
2、消费者拒绝消费者消息,basicNack/basicReject,并且不把消息重新放入原目标队列,requeue=false;
3、原队列存在消息过期设置,消息到达超时时间未被消费
6.3延迟队列
延迟队列,即消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费,
需求:
1、下单后,30分钟未支付,取消订单,回滚库存
2、新用户注册成功7天,发送短信问候
实现方式:
1、定时器
2、延迟队列
通过消息队列完成延迟队列的功能
在RabbitMQ中并未提供延迟队列功能
但是可以使用延迟+死信队列的组合实现延迟队列的效果
总结:
1、延迟队列 指消息进入队列后,可以被延迟一定时间,再进行消费
2、RabbitMQ没有提供延迟队列功能,但是可以使用,TTL+DLX来使用延迟队列效果
7.rabbitmq如何保证幂等性
在编程中,一个幂等操作的特点是其任意多次执行所产生的结果与一次执行所产生的结果相同,在mq中国由于网络故障或客户端延迟雄安飞mq自动重试过程中可能会导致消息的重复消费,如何解决消息的幂等性?
方法:
1、生成全局ip,存入redis或者数据库,在消费者消费消息之前,查询以下该消息是否有消费过
2、如果该消息已经消费过,则告诉mq消息已经消费,将该消息丢弃(手动ack)
3、如果没有消费过,将该消息进行消费并将消费写进redis后者数据库中
举个例子:
拼多多买商品,客户下完单之后,需要为用户累加积分,又需要保证积分不会重复列加,那么再MQ消费消息之前,先去数据库查询该消息是否已经消费,如果已经消费那么直接删除消息
生产者:
//发送消息的生产者
@Component
@Slf4j
public class ScoreProducer implements RabbitTemplate.ConfirmCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
//定义交换机
private static final String SCORE_EXCHANGE = "ykq_score_exchaneg";
//定义路由键
private static final String SCORE_ROUTINNGKEY = "score.add";
//订单完成
public String completeOrder() {
String orderId = UUID.randomUUID().toString();
System.out.println("订单已完成");
//发送积分通知
Score score = new Score();
score.setScore(100);
score.setOrderId(orderId);
String jsonMSg = JSONObject.toJSONString(score);
sendScoreMsg(jsonMSg, orderId);
return orderId;
}
//发送积分消息
@Async
public void sendScoreMsg(String jsonMSg, String orderId) {
this.rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.convertAndSend(SCORE_EXCHANGE, SCORE_ROUTINNGKEY, jsonMSg, message -> {
//设置消息的id为唯一
message.getMessageProperties().setMessageId(orderId);
return message;
});
}
@Override
public void confirm(CorrelationData correlationData, boolean ack, String s) {
if (ack) {
log.info(">>>>>>>>消息发送成功:correlationData:{},ack:{},s:{}", correlationData, ack, s);
} else {
log.info(">>>>>>>消息发送失败{}", ack);
}
}
}
消费者
//消费者
@Component
@Slf4j
public class ScoreConsumer {
@Autowired
private ScoreMapper scoreMapper;
@RabbitListener(queues = {"ykq_score_queue"})
public void onMessage(Message message, @Headers Map<String, Object> headers, Channel channel) throws IOException {
String orderId = message.getMessageProperties().getMessageId();
if (StringUtils.isBlank(orderId)) {
return;
}
log.info(">>>>>>>>消息id是:{}", orderId);
String msg = new String(message.getBody());
Score score = JSONObject.parseObject(msg, Score.class);
if (score == null) {
return;
}
//执行前去数据库查询,是否存在该数据,存在说明已经消费成功,不存在就去添加数据,添加成功丢弃消息
Score dbScore = scoreMapper.selectByOrderId(orderId);
if (dbScore != null) {
//证明已经消费消息,告诉mq已经消费,丢弃消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
return;
}
Integer result = scoreMapper.save(score);
if (result > 0) {
//积分已经累加,删除消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
return;
} else {
log.info("消费失败,采取相应的人工补偿");
}
}
}