RabbitMQ
1.入门
1.概念
- MQ:消息队列,是一种跨进程通信机制,用于上下游传递消息
- RabbitMQ:消息中间件,接受,存储并转发消息
2.优点
-
流量消峰:提高系统稳定型
优点:避免消息量大时造成服务器宕机
缺点:访问速度下降
-
应用解耦:提高系统容错性和可维护性
当子系统出现故障时,要处理的消息会缓存在MQ中,直到恢复,同时该过程中主系统不会故障
-
异步处理:提升用户体验和系统吞吐量
A服务发送给B服务后,B服务处理完将发消息给MQ,再由MQ发给A
-
缺点
系统可用性降低,复杂度提高,一致性问题
3.相关概念
-
Broker :接收和分发消息的应用,RabbitMQ Server 就是 Message Broker
-
Virtual Host :出于多租户和安全因素设计的,把 AMQP 的基本组件划分到一个虚拟的分组中,类似于网络中的 NameSpace 概念
-
Connection :provider/ consumer 和 broker 之间的 TCP 连接。
-
Channel :在 connection 内部建立的逻辑连接,如果应用程序支持多线程,通常每个 thread 创建单独的 channel 进行通讯,AMQPmethod 包含了 channel id帮助客户端和 message broker 识别 channel,所以channel 之间是完全隔离的。Channel 作为轻量级的 Connection 极大减少了操作系统建立 TCP connection 的开销
-
Exchange :message 到达 broker 的第一站,根据分发规则,匹配查询表中的 Routing Key,分发消息到 Queue 中去。
常用的类型有: Direct (Point-To-Point) Topic (Publish-Subscribe) Fanout (MultiCast)
-
Queue :消息最终被送到这里等待 consumer 取走。
-
Binding : Exchange 和 Queue 之间的虚拟连接,binding 中可以包含 routing key。
4.四大核心概念
- 生产者
- 消费者
- 交换机
- 队列
5.工作模式
-
简单模式(“Hello,World”)
-
工作模式(work queues)
-
发布与订阅模式(Publish/Subscribe)
-
路由模式(routing)
-
主题模式(topics)
-
发布确认模式(RPC)
2.安装(Linux)
1.官网
- https://www.rabbitmq.com/getstarted.html
2.创建好相关目录,并加入相关安装包
rpm -ivh erlang-21.3-1.el7.x86_64.rpm
yum install socat -y [从远端下载并安装]
rpm -ivh rabbitmq-server-3.8.8-1.el7.noarch.rpm
3.RabbitMQ命令
1.启动,关闭,重启
- /sbin/service rabbitmq-server start
- service rabbitmq-server stop
- service rabbitmq-server restart
2.查看MQ状态
- /sbin/rabbitmqctl status
3.开机自启动
- chkconfig rabbitmq-server on
4.安装管理界面
- rabbitmq-plugins enable rabbitmq management
5.关闭防火墙
- systemctl stop firewalld
- service iptables stop
6.监控平台界面
- http://ip地址:15672
7.添加用户
- 在linux中的/etc/rabbitmqm目录下新建文件:touch rabbitmq.config
- 文件中:[{rabbit,[{loopback_users,[]}]}]
4.用户管理
1.超级管理员(Administrator)
- 可登陆管理控制台,可查看所有的信息,并且可以对用户,策略进行操作。
2.监控者(Monitoring)
- 可登陆管理控制台,同时可以查看 Rabbitmq 节点的相关信息(进程数,内存使用情况,磁盘使用情况等)
3.策略制定者(Policymaker)
- 可登陆管理控制台, 同时可以对 policy 进行管理。但无法查看节点的相关信息(上图红框标识的部分)。
4.普通管理者(Management)
- 仅可登陆管理控制台,无法看到节点信息,也无法对策略进行管理。
5.其他(Others)
- 无法登陆管理控制台,通常就是普通的生产者和消费者。
6.查看用户列表
- /sbin/rabbitmqctl list_users
7.创建用户
- rabbitmqctl add user [用户名] [密码]
8.删除用户
- rabbitmqctl delete user Username
9.修改密码
- rabbitmqctl change_password Username Newpassword
10.设置角色
- rabbitmqctl set_user_tags 用户名 角色名1 角色名2
5.用户权限
1.介绍
- 指用户对exchange,queue的操作权限,包括配置,读写
- 配置权限:exchange,queue的声明和删除
- 读写权限:从queue获取消息,向exchange发送消息以及queue和exchange的绑定
2.操作指令
-
查看用户权限: /sbin/rabbitmqctl list_user_permissions 用户名
-
设置权限:rabbitmqctl set_permissions -p [主机] [用户名] [配置权限] [写权限] [读权限]
例如: rabbitmqctl set permissions -p “/” andy “." ".” “.*”
-
清除权限:rabbitmqctl clear permissions [-p VHostPath] 用户名
6.简单模式
1.创建个虚拟主机
- http://192.168.227.131:15672网页的Virtual Hosts创建
2.导入依赖
-
<dependency> <groupId>com.rabbitmq</groupId> <artifactId>amqp-client</artifactId> <version>5.6.0</version> </dependency>
3.编码步骤
-
创建连接工厂 :ConnectionFactory factory = new ConnectionFactory();
-
设置相关参数
factory.setHost("192.168.227.131"); //ip地址 factory.setPort(5672); //端口默认值 factory.setVirtualHost("myhost"); //虚拟机名 factory.setUsername("andy"); //用户名 factory.setPassword("123"); //密码
端口号默认为:5672 ,否则会报错
-
创建连接Connection: Connection connection = factory.newConnection( );
-
创建Channel: Channel channel = connection.createChannel();
-
创建队列Queue: channel.queueDeclare(“myqueue”,true,true,false,null);
参数一:queue(队列名)
参数二:durable(是否持久化)当mq重启后,消息是否还在
false:不持久化,创建者关闭连接后会自动删除队列(关闭通道不会)
true:消息会持久化,创建者退出后队列和消息都能保存
参数三:autoData:是否自动删除(true:没有消费者时自动删除)
参数四:exclusive:是否独占(只有一个消费者监听)
true:只能有一个人使用该队列
参数五:参数值
-
发送消息(简单模式:basicPublish): channel.basicPublish(“”,“myqueue”,null,msg.getBytes());
- exchange:交换机名称(简单模式下交换机使用默认)
- routingKey:路由名称(简单模式下使用对列名)
- props:消息配置信息(可配置消息持久化)
- body:发送消息数据
4.消费者
-
创建连接工厂
-
创建连接
-
创建通道
-
调用channel.basicConsumer(queueName,true,deliverCallback,cancelCallback)
-
1.消费哪个消息
-
2.消费成功后是否自动应答,true:自动应答,false:手动应答
-
3.消费未成功的回调方法(自己声明)
DeliverCallback deliverCallback = (consumerTag,message)->{ System.out.println(new String(message.getBody())); };
-
4.消费取消消费的回调方法(自己声明)
CancelCallback cancelCallback = consumerTag ->{ System.out.println("消息消费被中断"); };
-
7.工作队列模式
1.介绍
- 工作队列又称任务队列,避免立即执行资源密集型任务,而不得不等待它完成.把任务封装为消息并发送到队列,当多个工作线程时,这些工作线程将一起处理任务
2.消息应答
- 消费者在接受消息并处理完毕后,告诉RabbitMQ处理完成,RabbitMQ便可以删除该消息
- 自动应答:消息被接受后便认为传输成功,适用在消费者可以高效并处理这些消息时使用
3.应答方法
- Channel.basicAck(肯定确认):RabbitMQ确定消息传送并处理成功,可以删除该消息
- Channel.basicNack(否定确认)
- Channel.basicReject(否定确认):比上面少了参数:是否批量应答
4.Multiplc解释
- 是否开启批量应答
- true:批量应答Channel上没应答的消息(例如:传送消息 5 6 7 8,若当前为8且应答,则5~8之间未应答的都会应答)
- false:不批量(只应答8)
5.消息自动重新入队
- 某个消费者故障导致消息未确认,此时若有其他消费者可处理,RabbitMQ则分发给其他消费者
6.消息手动应答
-
消费者
//采用手动应答 channel.basicConsume(name,false,deliverCallback,cancelCallback); DeliverCallback deliverCallback = (consumerTag, message)->{ /** 1.消息标记Tag 2.是否批量应答 */ channel.basicAck(message.getEnvelope().getDeliveryTag(),false); };
7.RabbitMQ持久化
-
队列持久化
- 如果没有持久化,则在RabbitMQ重启或宕机会删除队列
- 创建队列时,durable参数为true则启用持久化
- 若队列本来不是持久化,则要删除后再重新创建,不然会报错
-
消息持久化
- 消息持久化不等于队列持久化,若是没持久化,则是保存在内存中
- 在生产者调用basicPublish方法时的basicProperties参数设置持久化
- basicProperties:MessageProperties.PERSISTENT_TEXT_PLAIN
- ==注意:==不能完全保证不会丢失消息,在保存到磁盘时有个间隔点,此时还没保存到磁盘
-
不公平分发
- 避免某个消费者A处理速度大大高于消费者B,而导致A处理完便处于空闲状态
- Channel.basicQos(1);//将变为不公平分发(能者多劳)
- 在消费者接受消息前设置
-
预取值
- 指定消费者最大堆积多少消息
- 在消费者接受消息前设置:Channel.basicQos(X) //x为最大数量,但不能为 0或1
8.发布确认
1.原理
- 生产者将通道设置成confirm,则所有在该通道发布的消息都会被指派一个唯一ID(从1开始);当消息投递到队列后,broker发送一个确认给生产者(包含唯一ID);如果消息和队列持久化,则确认消息是在消息写入磁盘后发出的.
- 好处:该模式是异步,生产者可以在等待通道返回确认时继续发送消息;消息确认后,生产者可通过回调方法处理消息;若因RabbitMQ自身原因导致消息丢失,则会发送nack消息,生产者同样可在回调方法中处理.
2.策略
- 前提:队列和消息都要持久化
- 开启发布确认方法
- 默认:不开启
- 在声明队列之前,创建完通道之后:Channel.confirmSelect()
3.单个确认发布
- 是一种同步确认发布方式,只有一个消息被确认发布后,后续消息才能继续发布,如果在指定时间内返回消息,则会抛异常
- 缺点:发布速度慢,若是没确认,则会阻塞后续消息的发布,最多提供每秒不超过数百条发布消息的吞吐量
- 开启发布确认:channel.confirmSelect();
- 获取返回值:boolean flag = channel.waitForConfirms();
4.批量确认发布
- 先发布一批,再一起确认可以极大提高吞吐量
- 缺点:当发生故障时,不知道哪个消息出问题
- 等一批数量足够便:channel.waitForConfirms();
5.异步确认发布
-
利用回调函数达到消息可靠性传递
-
缺点:比较复杂 优点:可靠性和效率高
-
在开启消息确认之后,确认消息之前添加监听器和回调方法
//准备消息监听器,查看哪些失败哪些成功 //确认成功 ConfirmCallback ackCallBack = (deliveryTag,multiple)->{ System.out.println("确认消息:"+deliveryTag); }; //失败:1.消息的标记,2.是否为批量确认 ConfirmCallback nackCallBack = (deliveryTag,multiple)->{ System.out.println("未确认消息:"+deliveryTag); }; channel.addConfirmListener(ackCallBack,nackCallBack);
-
如何处理未确认(并发链路队列)
-
开启确认消息之后,创建一个线程安全有序的哈希表(可以轻松将序号与消息关联,根据序号批量删除条目)
ConcurrentSkipListMap<Long,String> confirms = new ConcurrentSkipListMap<>();
-
消息发送之后,记录所有要发送的消息
confirms.put(channel.getNextPublishSeqNo(),message);
-
删除掉已确认的消息(在消息确认方法中)
//删除已确认 ConcurrentSkipListMap<Long,String> confirmed = confirms.headMap(deliverTag); confirmed.clear(); //批量删除 confirms.remove(); //单个删除
-
6.三种对比
单独发布 | 同步等待确认,操作简单,吞吐量有限 |
---|---|
批量发布 | 批量同步等待确认,简单,合理吞吐量,但出现问题难推断 |
异步处理 | 最佳性能和资源使用,实现复杂 |
9.交换机
1.介绍
- RabbitMq核心思想:生产者发送消息不会直接到队列,而是发送到交换机,再由交换机推送到队列
- Exchanges的类型
- 直接:direct
- 主题:topic
- 标题:headers
- 扇出:fanout
- 临时队列:具有随机名称的非持久化队列,一旦断开消费者连接,队列会自动删除
- 创建方法:String queueName = channel.queueDeclare().getQueue
- 绑定:交换机与队列捆绑(可以设置RoutingKey)
- 可通过RoutingKey指定发送给哪个队列
2.无名exchange
- 默认的交换机类型,在发送消息时,用(“”)空字符串标识
- channel.basicPublish(“”,“myqueue”,null,msg.getBytes()); 第一个参数
3.发布订阅模式(Fanout)
-
交换机接收到消息后,广播到所有它绑定的队列(系统有默认的Fanout交换机)
-
//创建fanout交换机 channel.exchangeDeclare("logs","fanout"); //生成临时队列,队列名称随机,消费者断开后删除 String queue = channel.queueDeclare().getQueue(); //绑定 channel.queueBind(queue,"logs",""); //回调方法 DeliverCallback deliverCallback = (consumerTag,message)->{ System.out.println("{SYS01}"+new String(message.getBody())); }; channel.basicConsume(queue,true,deliverCallback,consumerTag->{});
4.路由模式(direct)
- 队列与交换机的绑定要指定一个RoutingKey (路由 key).消息的发送方在向 Exchange 发送消息时,也必须指定消息的 RoutingKey.Exchange 根据消息的 Routing Key 进行判断,只有队列的 Routingkey 与消息的 Routing key 完全—致,才会接收到消息。
- 创建direct交换机:channel.exchangeDeclare(交换机名, BuiltinExchangeType.DIRECT);
- 绑定队列:channel.queueBind(队列名,交换机名,routingKey);
5.主体模式(topic)
-
发送的RoutinKey是一个单词列表,以点号分隔开,最多不超过255个字节
-
替换符
-
*(星号):代替一个单词
-
#:代替0个或多个单词
*.rabbit.* #.orange.*
-
-
不匹配任何绑定:队列不接收,并且被丢弃
-
同一队列匹配两个,只接受一次消息
10.死信队列
1.介绍
- 死信:无法被消费的消息
- 当消息出现异常时,便投入死信队列
- 应用场景:为了保证订单业务数据不丢失,当用户下单成功却未在指定时间支付时.
2.死信来源
-
消息TTL(存活时间)过期
-
队列达到最大长度(队列满)
-
消息被拒(消息应答时为reject或nack并且queue==false)
3.设置过期时间
1.消费者C1
-
声明死信和普通交换机(都为direct)
-
声明死信和普通队列
-
普通队列
//设置死信交换机 map.put("x-dead-letter-exchange",deadExchange); //设置Routingkey map.put("x-dead-letter-routing-key","lisi"); //创建队列 channel.queueDeclare(normalQueue,false,false,false,map);
-
死信队列
channel.queueDeclare(deadQueue,false,false,false,null);
-
-
绑定路由
channel.queueBind(normalQueue,normalExchange,"zhangsan"); channel.queueBind(deadQueue,deadExchange,"lisi");
-
接受消息(普通队列)
//消息确认回调方法 DeliverCallback deliverCallback = (c, m)->{ System.out.println("{ConsumerA}接收的消息:"+new String(m.getBody(),"UTF-8")); }; channel.basicConsume(normalQueue,true,deliverCallback,c->{});
2.消费者C2
-
接受消息(死信队列)
DeliverCallback deliverCallback = (c,m)->{ System.out.println("{消费者B}收到的消息:"+new String(m.getBody(),"UTF-8")); }; channel.basicConsume(deadQueue,true,deliverCallback,c->{});
3.生产者
-
设置消息存活时间(单位为毫秒)
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build();
-
发送消息
channel.basicPublish(normalExchange,"zhangsan",properties,msg.getBytes());
4.设置队列最大值
-
去掉过期时间
-
在普通队列的参数里添加
map.put("x-max-length",6);
-
删除掉已存在的普通队列
5.消息被拒
-
去掉设置的队列最大值,删除掉已存在队列
-
消息回调方法中
DeliverCallback deliverCallback = (c, m)->{ String s =new String(m.getBody(),"UTF-8"); if(s.equals("msg5")){ System.out.println("{ConsumerA}被拒绝的消息:"+s); //拒绝该消息,且不返回队列 channel.basicReject(m.getEnvelope().getDeliveryTag(),false); } System.out.println("{ConsumerA}接收的消息:"+s); //同意消息 channel.basicAck(m.getEnvelope().getDeliveryTag(),false); };
-
一定要关闭自动应答
channel.basicConsume(normalQueue,false,deliverCallback,c->{});
11.延迟队列
1.介绍
- 概念:队列内部有序,用来存放需要指定时间被处理的元素的队列
- 应用场景:订单十分钟内未支付,30秒内输入验证码
2.SpringBoot整合RabbitMQ
-
导入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.7.0</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.7.0</version> </dependency>
-
相关配置(application.properties)
spring.rabbitmq.host=192.168.227.131 spring.rabbitmq.port=5672 spring.rabbitmq.username=andy spring.rabbitmq.password=123
-
接口文档
@Configuration @EnableSwagger2 public class SwaggerConfig { @Bean public Docket webApiConfig(){ return new Docket(DocumentationType.SWAGGER_2) .groupName("webApi") .apiInfo(webApiInfo()) .select() .build(); } private ApiInfo webApiInfo(){ return new ApiInfoBuilder() .title("rabbitMq接口文档") .description("描述rabbitmq微服务接口定义") .version("1.0") .contact(new Contact("enjoy6288","http://www.baixxx.com","23xxxxx@qqcom")) .build(); } }
3.队列TTL
4.配置类代码
-
声明交换机
//声明普通交换机 @Bean("XExchange") public DirectExchange xExchange(){ return new DirectExchange(XExchange); } //声明死信交换机 @Bean("YExchange") public DirectExchange yExchange(){ return new DirectExchange(YExchange); }
-
声明队列
@Bean("normalQA") //队列A public Queue queueA(){ Map<String,Object> map = new HashMap<>(3); map.put("x-dead-letter-exchange",YExchange); map.put("x-dead-letter-routing-key","YY"); map.put("x-message-ttl",10000); return QueueBuilder.durable(normalQA).withArguments(map).build(); } @Bean("normalQB") //队列B public Queue queueB(){ Map<String,Object> map = new HashMap<>(3); map.put("x-dead-letter-exchange",YExchange); map.put("x-dead-letter-routing-key","YY"); map.put("x-message-ttl",30000); return QueueBuilder.durable(normalQB).withArguments(map).build(); } @Bean("deadQC") //死信队列 public Queue deadQC(){ return QueueBuilder.durable(deadQC).build(); }
设置过期时间的参数是long类型,不能用字符串
-
交换机与队列绑定
@Bean public Binding queueABindX(@Qualifier("normalQA") Queue QA, @Qualifier("XExchange")DirectExchange XE){ return BindingBuilder.bind(QA).to(XE).with("XA"); } @Bean public Binding queueBBindX(@Qualifier("normalQB") Queue QA,@Qualifier("XExchange")DirectExchange XE){ return BindingBuilder.bind(QA).to(XE).with("XB"); } @Bean public Binding queueCBindY(@Qualifier("deadQC") Queue QA,@Qualifier("YExchange")DirectExchange XE){ return BindingBuilder.bind(QA).to(XE).with("YY"); }
使用@Qualifier注解时注意别导错包,不然会报错
5.生产者
- http://localhost:8080/ttl/sendMsg/这是消息 (输入该网址)
@Slf4j
@RestController
@RequestMapping("/ttl")
public class TTLController {
@Autowired
private RabbitTemplate template;
@RequestMapping("/sendMsg/{message}")
public void sendMsg(@PathVariable String message){
log.info("当前时间:{},发送一条消息给队列{}",new Date().toString(),message);
template.convertAndSend("X","XA","来自ttl为10s的队列:"+message);
template.convertAndSend("X","XB","来自ttl为30s的队列:"+message);
}
}
6.消费者
@Slf4j
@Component
public class QueueConsumer {
@RabbitListener(queues = "QC")
public void getMessage(Message message, Channel channel){
String msg = new String(message.getBody());
log.info("当前时间:{},收到死信队列消息:{}",new Date().toString(),msg);
}
}
@RabbitListener(queues=“死信队列名”)
7.优化延迟队列
-
以上存在的问题:每新增一个时间需求,就要新增一个队列
-
解决:新增一个没有设置过期时间的通用队列,在生产者发消息时才设置过期时间
-
配置类
@Bean("normalQD") public Queue normalQD(){ Map<String,Object> map = new HashMap<>(3); map.put("x-dead-letter-exchange",YExchange); map.put("x-dead-letter-routing-key","YY"); return QueueBuilder.durable(normalQD).withArguments(map).build(); } @Bean public Binding queueCBindY(@Qualifier("deadQC") Queue QA,@Qualifier("YExchange")DirectExchange XE){ return BindingBuilder.bind(QA).to(XE).with("YY"); }
-
生产者
@RequestMapping("/sendTTL/{msg}/{ttl}") public void sendTTL(@PathVariable String msg,@PathVariable String ttl){ log.info("当前时间:{},发送一条设置了过期时间:{}(毫秒)的消息给队列:{}",new Date().toString(),ttl,msg); template.convertAndSend("X","XC","来自无ttl的队列:"+msg,m->{ m.getMessageProperties().setExpiration(ttl); return m; }); }
==m.getMessageProperties().setExpiration(ttl);==设置过期时间
-
存在的问题:由于消息在队列是按顺序的,==RabbitMQ只会检测第一个是否过期.==若第一个过期时间长与第二个,则会造成第二个消息在第一个结束后才执行(队列A:20秒.队列B:2秒,先发A的话B在20秒后才被接收到,先发B则不会)
8.使用插件实现延迟队列
- 代码架构图
1.添加插件
-
官网下载:https://www.rabbitmq.com/community-plugins.html
rabbitmq_delayed_message_exchange-3.8.0.ez文件
-
放置文件:/usr/lib/rabbitmq/lib/rabbitmq_server-3.8.8/plugins
-
安装插件:rabbitmq-plugins enable rabbitmq_delayed_message_exchange
2.配置类
-
@Bean public CustomExchange delayedExchange(){ Map<String,Object> map = new HashMap(); //设置交换机类型 map.put("x-delayed-type","direct"); //参数列表:交换机名,交换机类型,是否持久化,是否自动删除,参数 return new CustomExchange(EXCHANGE,"x-delayed-message",true,false,map); } @Bean public Queue delayedqueue(){ return new Queue(QUEUE); } @Bean public Binding queueBindingExchange(@Qualifier("delayedqueue")Queue queue, @Qualifier("delayedExchange")CustomExchange exchange){ return BindingBuilder.bind(queue).to(exchange).with(ROUTING_KEY).noargs(); }
-
不同点:(1)交换机参数只设置了交换机类型;(2)新建队列: new Queue(QUEUE);
3.生产者
-
@RequestMapping("/sendDelayed/{msg}/{ttl}") public void sendTTL(@PathVariable String msg,@PathVariable Integer ttl){ log.info("当前时间:{},发送一条设置了过期时间:{}(毫秒)的消息给队列:{}",new Date().toString(),ttl,msg); template.convertAndSend("delayed.exchange","delayed.routingKey","来自无ttl的队列:"+msg,m->{ m.getMessageProperties().setDelay(ttl); return m; }); }
-
不同点:m.getMessageProperties().setDelay(ttl)后面是setDelay()方法,参数要Integer类型
4.消费者
-
@RabbitListener(queues = "delayed.queue") public void getDelayedMsg(Message msg){ String message = new String(msg.getBody()); log.info("当前时间:{},收到的消息:{}",new Date().toString(),message); }
-
没啥区别,但记得添加@Slf4j和@Component注解!!!
9.总结
- 基于死信的情况下:在队列设置ttl;基于延迟插件:在交换机中设置ttl
- 延迟队列可实现:消息可靠发送,死信队列保证消息至少消费一次以及未被处理的不会被丢弃
- 可以解决单点故障问题,不会因单个节点挂掉导致队列不可用或消息丢失
12.发布确认Plus版
1发布确认springBoot版本
-
配置类
@Bean public DirectExchange confirmExchange(){ return new DirectExchange(exchange); } @Bean public Queue confirmQueue(){ return QueueBuilder.durable(queue).build(); } @Bean public Binding confirmBinding(@Qualifier("confirmExchange")DirectExchange exchange ,@Qualifier("confirmQueue")Queue q){ return BindingBuilder.bind(q).to(exchange).with(routingKey); }
-
生产者
@RequestMapping("/sendMessage/{msg}") public void sendMessage(@PathVariable("msg")String msg){ template.convertAndSend("confirm.exchange","confirm.key",msg); log.info("发送消息内容为:{}",msg); }
-
消费者
@RabbitListener(queues="confirm.queue") public void getMsg(Message msg){ String m = new String(msg.getBody()); log.info("接收到的消息为:{}",m); }
-
测试地址:http://localhost:8080/confirm/sendMessage/你好!
-
回调接口
-
在配置文件中添加:spring.rabbitmq.publisher-confirm-type=correlated
none:禁用发布确认模式(默认值)
correlated:发布消息成功后触发回调方法
simple:第一个效果:和correlated类似
第二个效果:发布消息成功后使用rabbitTemplate调用waitForConfirms或waitConfirmsOrDie方法,等待broker返回发送结果,根据结果判断下一步.waitConfirmsOrDie返回false会关闭channel
-
创建回调方法类实现RabbitTemplate.ConfirmCallback
@Autowired private RabbitTemplate template; //注入该 @PostConstruct public void init(){ template.setConfirmCallback(this); } /** * 交换机确认回调方法 * @ correlationData:保存回调消息的ID和相关信息 * @ ack:交换价收到消息(接收到为:true;失败为:false) * @ cause:原因(成功为null,失败则是失败原因) */ @Override public void confirm(CorrelationData correlationData, boolean ack, String cause) { String id = correlationData!=null?correlationData.getId():""; if(ack){ log.info("交换机收到id为:{}的消息",id); } else{ log.info("交换机没收到id为:{}的消息,原因:{}",id,cause); } }
注意:要记得注入该回调方法,并且是在实例化回调方法且注入RabbitTemplate之后,否则报空指针异常;并且要加==@Component注解==
-
生产者
CorrelationData correlationData = new CorrelationData("1"); //创建个对象 template.convertAndSend("confirm.exchange","confirm.key",msg,correlationData);
-
测试结果
交换机出错:会报错,找不到对应交换机
routingKey出错:不会报错,回调方法收到信息,但是消费者接收不到消息!!!
-
2.回退消息
-
Mandatory参数:==当消息传递过程中不可到达目的地时返回消息给生产者
在配置文件中开启回退消息:spring.rabbitmq.publisher-returns=true
-
回调接口:上面的回调方法类再实现对应方法
@Override public void returnedMessage(ReturnedMessage returnedMessage) { String msg = new String( returnedMessage.getMessage().getBody()); log.info("消息:{},被交换机:{}退回,退回原因:{},路由Key:{}", msg,returnedMessage.getExchange(),returnedMessage.getReplyText(),returnedMessage.getRoutingKey() ); }
在消息不可达时返回给生产者,只有不可达目的地才回退
记得注入到template中:template.setReturnsCallback(this);
-
结果
3.备份交换机
-
代码架构图
当交换机无法发送消息给消费者时,便会把无法发送的消息发给备份交换机,备份交换机再发给备份队列和报警队列
-
配置类修改
-
创建一个Fanout类型交换机和两个队列
-
修改已创建的交换机的代码
@Bean("confirmExchange") public DirectExchange confirmExchange(){ return ExchangeBuilder.directExchange(exchange).durable(true) .withArgument("alternate-exchange",backupExchange).build(); }
参数:1.创建的交换机名;2.是否持久化;3.备份的交换机
-
-
创建报警消费者
-
删除掉原来创建的交换机
-
测试结果
1.当消息不可达时,会从报警消费者出输出
2.若是mandatory参数与备份交换机同时开启,消息会从备份交换机处输出,备份交换机优先级更高
13.RabbitMQ其他知识点
1.幂等性
1.概念:
用户对同一操作发起一次或多次请求的结果一致,不会产生副作用.(例如:用户因为网络原因造成付款成功后返回结果失败,第二次点击付款成功后,却扣两次钱)
2.消息重复消费
若消费者返回ack时网络中断,则MQ可能会再将该消息发给其他消费者或者重连后再一次消费该消息.
3.解决思路
一般使用全局ID或写个唯一标识(比如时间戳/UUID),每次消费时用该id判断是否已消费过
4.主流幂等性操作
-
唯一Id+指纹码机制,利用数据库主键去重
指纹码:规则或时间戳加别的服务给到的唯一信息码(优点:实现简单;缺点:在高并发时,单数据库有写入性能瓶颈)
-
利用redis原子性实现(优先这个)
利用redis执行setnx命令,具有天然幂等性
2.优先级队列
1.应用场景
- 在订单催收时,可以设置哪个订单优先接受
2.优先级队列长度
- 0~255,越大越先执行
3.设置优先级
-
在创建队列之前,设置长度
map.put("x-max-priority",10); channel.queueDeclare("hello",true,false,false,map); 填入参数
-
在发送消息之前设置消息优先级
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder() .priority(5).build(); channel.basicPublish("","hello",properties,msg.getBytes()); //发送消息
注意:队列和消息都要设置优先级,消费者要等消息已发送到队列才能消费,这样才有机会对消息进行排序
3.惰性队列
-
消息保存在内存中还是磁盘上
正常情况:消息保存在内存;惰性队列:保存在磁盘
-
应用场景:消费者下线,宕机或因维护而关闭
-
设置方法(声明队列时)
map.put("x-queue-mode","lazy") //设置为惰性模式 channel.queueDeclare("hello",true,false,false,map); 填入参数
-
优点:当有大量消息存储时,占用的内存远小于普通队列
缺点:读取速度慢,要先从磁盘读取消息到内存(根据索引)
4.RabbitMQ集群
- 将单个RabbitMQ集群在一起
1.搭建步骤(3台主机)
-
修改3台主机名称: vim /etc/hostname
-
配置各个节点的host文件:vim /etc/hosts(ip地址 主机名)
192.168.172.1 node1
192.168.172.2 node2
192.168.172.3 node3
-
使各个节点cookie文件使用同一值(在第一台机器)
scp/var/lib/rabbitmq/.erlang.cookie root@node2:/var/lib/rabbitmq/.erlang.cookie
scp/var/lib/rabbitmq/.erlang.cookie root@node3:/var/lib/rabbitmq/.erlang.cookie
-
重启服务:rabbitmq-server -detached
-
在节点2和3各执行
rabbitmqctl stop_app (只关闭RabbitMQ服务)
rabbitmqctl reset
rabbitmqctl join_cluster rabbit@node1(节点3则为:node2)
rabbitmqctl start_app
-
集群状态:rabbitmqctl cluster_status
-
重新创建用户并设置角色与权限
-
解除节点(在节点1执行)
rabbitmqctl stop_app rabbitmqctl reset rabbitmqctl start_app rabbitmqctl cluster_status rabbitmqctl forget_cluster_node rabbit@node2(要删除的节点)
5.镜像队列
- 作用:避免集群中一个节点宕机造成消息丢失
- 在RabbitMQ管理界面的Admin->Policies设置
6.Haproxy实现负载均衡
- 通过Haproxy和keepalive解决生产者只发消息给一个RabbitMQ节点的问题
- 步骤
- 普通情况下,生产者走主机,由主机转发到RabbitMQ集群
- 若主机宕机了,keepalive将ip漂移到备机,再由备机转发
- 并且每隔一段时间会查询主机状态,看是否漂移回去
7.FederationExchange
-
作用:解决两个broker相距太远而导致消息延迟
-
实现:安装对应插件
rabbitmq-plugins enable rabbitmq_federation
rabbitmq-plugins enable rabbitmq_federation_management
-
在下游节点要先准备交换机
-
在下游节点配置上游节点
-
配置策略
8.FederationQueue
- 和Exchange配置类似
9.shovel
-
Shovel可靠,持续地从一个broker中队列(源端)拉取数据并转发到另一个Broker
-
开启插件:rabbitmq-plugins enable rabbitmq_shovel
rabbitmq-plugins enable rabbitmq_shovel_management
root@node3:/var/lib/rabbitmq/.erlang.cookie -
重启服务:rabbitmq-server -detached
-
在节点2和3各执行
rabbitmqctl stop_app (只关闭RabbitMQ服务)
rabbitmqctl reset
rabbitmqctl join_cluster rabbit@node1(节点3则为:node2)
rabbitmqctl start_app
-
集群状态:rabbitmqctl cluster_status
-
重新创建用户并设置角色与权限
-
解除节点(在节点1执行)
rabbitmqctl stop_app rabbitmqctl reset rabbitmqctl start_app rabbitmqctl cluster_status rabbitmqctl forget_cluster_node rabbit@node2(要删除的节点)
5.镜像队列
- 作用:避免集群中一个节点宕机造成消息丢失
- 在RabbitMQ管理界面的Admin->Policies设置
6.Haproxy实现负载均衡
- 通过Haproxy和keepalive解决生产者只发消息给一个RabbitMQ节点的问题
- 步骤
- 普通情况下,生产者走主机,由主机转发到RabbitMQ集群
- 若主机宕机了,keepalive将ip漂移到备机,再由备机转发
- 并且每隔一段时间会查询主机状态,看是否漂移回去
7.FederationExchange
-
作用:解决两个broker相距太远而导致消息延迟
-
实现:安装对应插件
rabbitmq-plugins enable rabbitmq_federation
rabbitmq-plugins enable rabbitmq_federation_management
-
在下游节点要先准备交换机
-
在下游节点配置上游节点
-
配置策略
8.FederationQueue
- 和Exchange配置类似
9.shovel
-
Shovel可靠,持续地从一个broker中队列(源端)拉取数据并转发到另一个Broker
-
开启插件:rabbitmq-plugins enable rabbitmq_shovel
rabbitmq-plugins enable rabbitmq_shovel_management