笔记来自:https://www.bilibili.com/video/BV1mN4y1Z7t9?p=1&vd_source=b9b918c3541cd04d8da1944cc702695b
安装
1.下载erlang并安装,地址:http://erlang.org
2.下载mq并安装,地址:http://www.rabbitmq.com/download.html
3.安装完成后,管理后台地址:http://localhost:15672,初始账号和密码:guest/guest
优缺点
优点:解耦、削峰、数据分发
缺点:
- 系统可用性降低;系统引入的外部依赖越多,系统稳定性越差。一旦MQ宕机,就会对业务造成影响
- 系统复杂度提高;MQ的加入大大增加了系统的复杂性,以前系统间是同步的远程调用,现在是通过进行异步调用。如何保证MQ的高可用?如何保证消息没有被重复消费?怎么处理消息丢失情况?怎么保证消息传递的顺序性?
- 一致性问题;A系统处理完业务,通过MQ给BCD三个系统发消息数据,如果B系统,C系统处理成功,D系统处理失败。如何保证消息数据处理的一致性?
对比
ActiveMQ | RabbitMQ | RocketMQ | kafka | |
---|---|---|---|---|
开发语言 | java | erlang | java | scala |
单击吞吐量 | 万级 | 万级 | 十万级 | 十万级 |
时效性 | ms级 | us级 | ms级 | ms级以内 |
可用性 | 高(主从架构) | 高(主从架构) | 非常高(分布式架构) | 非常高(分布式架构) |
功能特性 | 成熟的产品,在很多公司得到应用,有较多的文档;各种协议支持较好 | 基于erlang开发,所以并发能力很强,性能及其好,延时很低;管理界面丰富,缺点是很难进行二次开发 | MQ功能比较完备,扩展性佳 | 只支持主要的MQ功能,像一些消息查询,消息回溯等功能没有提供,毕竟是为大数据准备的,在大数据领域应用广。 |
在Spring Boot项目中使用rabbitMQ
Spring Boot项目中加入如下代码
//Spring Boot项目中引入amqp包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
//配置文件中加入如下配置
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.virtual-host=/
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
一、直接发送消息到队列,不经过交换机
1、简单队列,只有一个消费者
/**
* 简单队列
*/
@Test
public void testSimpleQueue(){
String queueName ="simple.queue";
String message ="testSimpleQueue";
rabbitTemplate.convertAndSend(queueName,message);
}
@RabbitListener(queues = "simple.queue")
public void listenSimpleQueue(String msg) {
System.out.println("消费者收到消息====================="+msg);
}
2、工作队列,多个消费者
/**
* 工作队列
*/
@Test
public void testWorkQueue() throws InterruptedException {
String queueName ="work.queue";
String message ="testWorkQueue";
for (int i = 0; i < 50; i++) {
rabbitTemplate.convertAndSend(queueName,message+i);
Thread.sleep(20);
}
}
@RabbitListener(queues = "work.queue")
public void listenWorkQueue1(String msg) {
System.out.println("消费者work.queue 1收到消息====================="+msg);
}
@RabbitListener(queues = "work.queue")
public void listenWorkQueue2(String msg) throws InterruptedException {
System.out.println("消费者work.queue 2收到消息====================="+msg);
Thread.sleep(100);
}
每个消息只会被消费一次,默认情况下消费者轮流消费,可以通过设置spring.rabbitmq.listener.simple.prefetch=1来确保同一时刻最多投递给消费者1条消息,从而避免消息堆积
二、消息经过交换机
交换机的作用是对消息进行路由,但不具备保存消息的能力。
1.Fanout交换机
会将接收到的消息广播到每一个跟其绑定的队列,所以也叫广播模式
举例,交换机和队列进行绑定如下,
/**
* fanout交换机
*/
@Test
public void testSimpleQueue(){
String exchangeName ="test.fanout";
String message ="hello,everyone!";
rabbitTemplate.convertAndSend(exchangeName,null,message);
}
@RabbitListener(queues = "fanout.queue")
public void listenFanoutQueue1(String msg) {
System.out.println("消费者fanout.queue 收到消息====================="+msg);
}
@RabbitListener(queues = "fanout.queue1")
public void listenFanoutQueue2(String msg) {
System.out.println("消费者fanout.queue 1收到消息====================="+msg);
}
结果:
消费者fanout.queue 收到消息=====================hello,everyone!
消费者fanout.queue 1收到消息=====================hello,everyone!
2.Direct交换机
Direct交换机会将接收到的消息根据规则路由到指定的队列,因此称为定向路由。
每一个队列都与交换机设置一个BindingKey;
发布者发送消息时,指定消息的RoutingKey;
交换机将消息路由到BingingKey与消息RoutingKey一致的队列;
举例,交换机与队列的BindingKey如下,
@Test
public void testDirectQueue(){
String exchangeName ="test.direct";
String message ="hello!";
rabbitTemplate.convertAndSend(exchangeName,"red",message);
rabbitTemplate.convertAndSend(exchangeName,"blue",message);
}
@RabbitListener(queues = "direct.queue1")
public void listenDirectQueue1(String msg) {
System.out.println("消费者direct.queue1 收到消息====================="+msg);
}
@RabbitListener(queues = "direct.queue2")
public void listenDirectQueue2(String msg) {
System.out.println("消费者direct.queue2 收到消息====================="+msg);
}
结果:
消费者direct.queue1 收到消息=====================hello!
消费者direct.queue2 收到消息=====================hello!
消费者direct.queue1 收到消息=====================hello!
3.Topic交换机
与Direct交换机类似,区别在于RoutingKey可以是多个单词的列表,以.分隔。BindingKey可以使用通配符。#代指0个或多个单词,*代指一个单词(比如routingKey是多个单词,就不匹配)
举例,当交换机与队列的BindingKey如下时,
@Test
public void testTopicQueue(){
String exchangeName ="test.topic";
rabbitTemplate.convertAndSend(exchangeName,"japan.news","日本红色预警!");
rabbitTemplate.convertAndSend(exchangeName,"china.news","中国蓝色预警!");
rabbitTemplate.convertAndSend(exchangeName,"china.weather","阳光明媚!");
}
@RabbitListener(queues = "topic.queue1")
public void listenTopicQueue1(String msg) {
System.out.println("消费者topic.queue1 收到消息====================="+msg);
}
@RabbitListener(queues = "topic.queue2")
public void listenTopicQueue2(String msg) {
System.out.println("消费者topic.queue2 收到消息====================="+msg);
}
结果:
消费者topic.queue2 收到消息=====================日本红色预警!
消费者topic.queue1 收到消息=====================中国蓝色预警!
消费者topic.queue2 收到消息=====================中国蓝色预警!
消费者topic.queue1 收到消息=====================阳光明媚!
三、声明队列和交换机
上面操作的时候都是直接在管理后台进行操作的,不方便而且容易出错。
1.基于bean的方式来声明
SpringAMQP提供了几个类,用来声明队列,交换机及其绑定关系:
Queue:用于声明队列,可以用工厂类QueueBuilder构建;
Exchange:用于声明交换机,可以用工厂类ExchangeBuilder构建;
Binging:用于声明队列和交换机的绑定关系,可以用工厂类BingingBuilder构建;
备注,一般就消费者会关心队列,所以队列和交换机一般在消费端声明。
举例,声明Fanout交换机、队列以及绑定关系,
@Configuration
public class FanoutConfig {
@Bean
public FanoutExchange fanoutExchange(){
//方式一
return ExchangeBuilder.fanoutExchange("test.fanout2").build();
//方式二
//return new FanoutExchange("test.fanout2");
}
@Bean
public Queue fanoutQueue3(){
//方式一
//durable表示持久化,如果直接new的话,默认就是会持久化
return QueueBuilder.durable("fanout.queue3").build();
//方式二
//return new Queue("fanout.queue3");
}
@Bean
public Queue fanoutQueue4(){
return new Queue("fanout.queue4");
}
@Bean
public Binding bindFanoutQueue3(Queue fanoutQueue3,FanoutExchange fanoutExchange){
return BindingBuilder.bind(fanoutQueue3).to(fanoutExchange);
}
@Bean
public Binding bindFanoutQueue4(){
return BindingBuilder.bind(fanoutQueue4()).to(fanoutExchange());
}
}
2.基于注解的方式来声明
上面的声明方式,对于direct交换机来说,当routingKey有多个时,绑定的时候要声明很多次,太麻烦了。
所以SpringAMQP还提供了基于@RabbitListener注解来声明队列和交换机的方式:
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue3",durable = "true"),
exchange = @Exchange(name = "test.direct2",type = ExchangeTypes.DIRECT),
key ={"red","blue"}
))
public void listenDirectQueue3(String msg) {
System.out.println("消费者direct.queue3 收到消息====================="+msg);
}
四、消息转换器
当发送的消息是个对象的时候,默认会采用java提供的序列化方式进行序列化,消息长度变得很多,而且有安全问题(比如篡改后就会导致无法反序列化)。
因此建议采用JSON序列化来代替,方式如下:
//引入Jackson依赖
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
//配置MEssageConverter
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
//测试
@Test
public void testSimpleQueue(){
String queueName ="simple.queue";
Map<String,String> map =new HashMap<>(4);
map.put("test","消息测试");
rabbitTemplate.convertAndSend(queueName,map);
}
@RabbitListener(queues = "simple.queue")
public void listenSimpleQueue(Map<String,String> msg) {
for (String s : msg.keySet()) {
System.out.println("key="+s+"value="+ msg.get(s));
}
System.out.println("消费者收到消息====================="+msg);
}
结果:
key=testvalue=消息测试
消费者收到消息====================={test=消息测试}
五、消息可靠性
RabbitMQ消息丢失的情况分为如下几种:
1、生产者发送消息到MQ时,因为网络问题等原因弄丢消息
2、MQ接到消息还未持久化就挂掉了
3、消费者还未处理完挂掉了
1.生产者重连
当网络波动,可能会出现生产者连接MQ失败的情况。可以开启失败重试。注意重试是阻塞式的,对业务性能有要求的,建议禁用重试机制。当然也可以使用异步的方式来做发送消息的操作。
#设置连接超时时间
spring.rabbitmq.connection-timeout=1s
#开启超时重试机制
spring.rabbitmq.template.retry.enable=true
#失败后的初始等待时间
spring.rabbitmq.template.retry.initial-interval=1000ms
#失败后下次的等待时长倍数,下次等待时长=initial-interval*multiplier
spring.rabbitmq.template.retry.multiplier=1
#最大重试次数
spring.rabbitmq.template.retry.max-attempts=3
2.生产者确认
RabbitMQ提供了Publisher Confirm和Publisher Return两种确认机制。
Publisher Confirm 确认消息是否到达交换机,Publisher Return确认消息是否到达队列。
开启确认机制后,在MQ成功收到消息后会返回确认消息给生产者。返回的结果有以下几种情况:
1、消息投递到了MQ,但是路由失败。此时会通过PublisherReturn返回路由异常原因,然后返回ACK。
2、临时消息投递到MQ,并且入队成功,返回ACK。
3、持久消息投递到了MQ,并且入队完成持久化,返回ACK。
4、其他情况都会返回NACK。
代码实现生产者确认,只需在配置文件中配置:
# none:关闭confirm机制 simple:同步阻塞等待MQ的回执消息 correlated:MQ异步回调方式返回回执消息
spring.rabbitmq.publisher-confirm-type=correlated
# 开启Publisher Return机制,一般情况下无需开启,因为大多数问题都是开发人员导致的
spring.rabbitmq.publisher-returns=true
如果是异步回调方式返回回执消息,需要提供回调。
对于ReturnCallback,每个RabbitTemplate只能配置一个,因此需要在项目启动过程中进行配置:
@Slf4j
@Configuration
public class CommonConfig implements ApplicationContextAware {
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) ->{
log.info("消息发送失败,应答码={},原因={},交换机={},路由key={},消息={}",replyCode,replyText,exchange,routingKey,message);
} );
}
}
对于ConfirmCallback,每个消息要单独指定,因为每个消息都要确认是否发送成功。
@Test
public void testSimpleQueue() throws InterruptedException {
String exchangeName ="test.fanout";
String message ="hello,everyone!";
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
correlationData.getFuture().addCallback(new ListenableFutureCallback<CorrelationData.Confirm>() {
@Override
public void onFailure(Throwable throwable) {
//这个失败不用处理,一般不会失败
log.error("消息回调失败",throwable);
}
@Override
public void onSuccess(CorrelationData.Confirm confirm) {
log.info("收到confirm callback回执");
if(confirm.isAck()){
//消息发送成功
log.info("消息发送成功,收到ack");
}else{
//消息发送失败,此处应该进行失败重试
log.error("消息发送失败,收到nack,原因:{}",confirm.getReason());
}
}
});
//当故意把交换机名写错,结果:
消息发送失败,收到nack,原因:channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'test.fanouta' in vhost '/', class-id=60, method-id=40)
如何处理生产者的确认消息?
生产者确认需要额外的网络和系统资源开销,尽量不要使用(个人理解:网络问题会抛异常,程序能感知。如果是无法到达交换机或者无法路由,都属于代码层面的问题)
如果一定要使用,无需开启Publisher-Return机制,因为一般路由失败是自己业务问题
对于nack消息可以有限次数重试,依然失败则记录异常消息
3.MQ的可靠性
在默认情况下,RabbitM0会将接收到的信息保存在内存中以降低消息收发的延迟。这样会导致两个问题
1、一旦MQ宕机,内存中的消息会丢失
2、内存空间有限,当消费者故障或处理过慢时,会导致消息积压,引发MO阻塞(当达到一定数量时,MQ会将早期的消息移到磁盘,也就是Paged Out,这期间无法接收消息)
RabbitMQ实现数据持久化包括3个方面:交换机持久化,队列持久化,消息持久化
使用Spring的话,交换机与队列的durable默认就是true。消息持久化默认deliveryModel就是2。
从RabbitMQ的3.6.0版本开始,增加了Lazy Queue的概念,也就是惰性队列。惰性队列的特征如下:
接收到消息后直接存入磁盘而非内存(内存中只保留最近的消息,默认2048条)
消费者要消费消息时才会从磁盘中读取并加载到内存
支持数百万条的消息存储
在3.12版本后,所有队列都是Lazy Queue模式,无法更改。
设置队列为惰性队列的方式:
//方式一
@Bean
public Queue fanoutQueue3(){
return QueueBuilder.durable("fanout.queue3").lazy().build();
}
//方式二
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue3",durable = "true",arguments = @Argument(name = "x-query-mode",value = "lazy")),
exchange = @Exchange(name = "test.direct2",type = ExchangeTypes.DIRECT),
key ={"red","blue"}
))
public void listenDirectQueue3(String msg) {
System.out.println("消费者direct.queue3 收到消息====================="+msg);
}
4.消费者可靠性
为了确认消费者是否成功处理消息,RabbitMQ提供了消费者确认机制(Consumer Acknowledgement)。当消费者处理消息结束后,应该向RabbitMO发送一个回执,告知RabbitMQ自己消息处理状态。回执有三种可选值:
- ack:成功处理消息,RabbitMQ从队列中删除该消息
- nack:消息处理失败,RabbitMo需要再次投递消息
- reject: 消息处理失败并拒绝该消息,RabbitMQ从队列中删除该消息
SpringAMOP已经实现了消息确认功能。并允许我们通过配置文件选择ACK处理方式,有三种:
-
none:不处理。即消息投递给消费者后立刻ack,消息会立刻从MQ删除。非常不安全,不建议使用
-
manual: 手动模式。需要自己在业务代码中调用api,发送ack或reject,存在业务入侵,但更灵活
-
auto:自动模式。SpringAMOP利用AOP对我们的消息处理逻辑做了环绕增强,当业务正常执行时则自动返回ack,当业务出现异常时,根据异常判断返回不同结果:如果是业务异常,会自动返回nack;如果是消息处理(MessageConversionException)或校验异常,自动返回reject。
//消费方在配置文件中加入如下配置
spring.rabbitmq.listener.simple.acknowledge-mode=auto
//处理消息时抛出异常
@RabbitListener(queues = "simple.queue")
public void listenSimpleQueue(Map<String,String> msg) {
System.out.println("消费者收到消息====================="+msg);
throw new RuntimeException("抛异常");
}
//然后发消息后,会发现消息不断的投递。
当消费者出现异常后,消息会不断requeue(重新入队)到队列,再重新发送给消费者,然后再次异常,再次requeue无限循环,导致mq的消息处理飙升,带来不必要的压力。可以利用Spring的retry机制,在消费者出现异常时利用本地重试。
#开启消费者失败重试
spring.rabbitmq.listener.simple.retry.enabled=true
#初始的等待时长为1s
spring.rabbitmq.listener.simple.retry.initial-interval=1000ms
#下次失败的等待时长倍数,下次的等待时长=multiplier*last-interval
spring.rabbitmq.listener.simple.retry.multiplier=1
#最大重试次数
spring.rabbitmq.listener.simple.retry.max-attempts=3
#true无状态;false有状态,如果业务中包含事务,这里改为false
spring.rabbitmq.listener.simple.retry.stateless=true
在开启重试模式后,重试次数耗尽,如果消息依然失败,则需要有MessageRecoverer接口来处理,它包含三种不同的实现:
-
RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认就是这种方式
-
ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队,不成功还是会在投递
-
RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机(方便后期人工处理,比如发邮件给开发)
RepublishMessageRecoverer用法示例:
@Configuration
//当spring.rabbitmq.listener.simple.retry.enabled=true时,该bean才生效
@ConditionalOnProperty(prefix = "spring.rabbitmq.listener.simple.retry",name="enabled",havingValue = "true")
public class RabbitMQErrorConfiguration {
@Bean
public DirectExchange errorExchange(){
return new DirectExchange("error.direct");
}
@Bean
public Queue errorQueue(){
return new Queue("error.queue");
}
@Bean
public Binding errorDirectQueue(Queue errorQueue,FanoutExchange errorExchange){
return BindingBuilder.bind(errorQueue).to(errorExchange).with("error");
}
@Bean
public MessageRecoverer messageRecoverer(RabbitTemplate rabbitTemplate){
return new RepublishMessageRecoverer(rabbitTemplate,"error.direct","error");
}
}
一句话总结就是,开启消费者确认机制为auto,由Spring确认消息处理成功后返回ack,异常时返回nack。开启消费者失败重试机制,并设置MessageRecover,多次重试失败后将消息投递到异常交换机,交由人工处理。
六、业务幂等性
幂等是指同一个业务,执行一次或多次结果一样。
唯一消息id
给每个消息都设置一个唯一id,利用id区分是否是重复消息:
每一条消息都生成一个唯一的id,与消息一起投递给消费者。
消费者接收到消息后处理自己的业务,业务处理成功后将消息ID保存到数据库。
如果下次又收到相同消息,去数据库查询判断是否存在,存在则为重复消息,放弃处理。
//生成消息ID的方式
@Bean
public MessageConverter messageConverter(){
Jackson2JsonMessageConverter jjmc = new Jackson2JsonMessageConverter();
//配置自动创建id,用于识别不同消息,也可以在业务中基于ID判断是否是重复消息
jjmc.setCreateMessageIds(true);
return jjmc;
}
结合业务做判断
比如首次消费业务状态已改,下次再消费,由于业务状态已经变了,就不需要往下执行了。
七、延迟消息
生产者发送消息时指定一个时间,消费者不会立刻收到消息,而是在指定时间之后才收到消息。实现延迟消息的方案有:
死信交换机、延迟消息插件。
1.死信交换机
当一个队列中的消息满足下列情况之一时,就会成为死信 (dead letter) :
- 消费者使用basic.reject或 basic.nack声明消费失败,并且消息的requeue参数设置为false
- 消息是一个过期消息(达到了队列或消息本身设置的过期时间),超时无人消费
- 要投递的队列消息堆积满了,最早的消息可能成为死信
如果队列通过dead-letter-exchange属性指定了一个交换机,那么该队列中的死信就会投递到这个交换机中。这个交换机称为死信交换机 (Dead Letter Exchange,简称DLX)
2.延迟消息插件
RabbitMO的官方也推出了一个插件,原生支持延迟消息功能。该插件的原理是设计了一种支持延迟消息功能的交换机,当消息投递到交换机后可以暂存一定时间,到期后再投递到队列。
延迟消息过多,也会对服务器造成很大的压力,所以不适用于延迟时间特别长的消息。