RabbitMQ
消息队列
什么MQ?
概念:
·Mq本质是个队列,满足FIFO先进先出规则,队列中存放Message,一种跨进程的通信机制,用于上下游传递消息。
·互联网架构中MQ是一种常见的上下游“逻辑解耦+物理解耦”的消息通信服务。
·使用MQ,消息发送上游只需要依赖MQ,不依赖与其他服务。
为什么使用MQ
流量消峰
流量消峰:说白了就是解决某一时刻,请求访问量剧增的问题,假如某个系统正常的并发数1万次每秒,突然在某一时刻并发数达到了2万次每秒的并发数,如果处理不当,用户体验感将会极差,服务有雪崩的问题,使用MQ作为缓冲就可以将某一时刻的并发访问数分散开来处理,分时间段响应这些并发请求;
应用解耦
应用解耦:简单理解就是解决扇出链路中某个服务异常导致整体服务性能变差或崩溃问题,例如A服务的完成需要调用B、C、D等服务,假如当中的某个服务出现了异常,那么A服务也会陷入异常状态;引入MQ后,在A服务与其他服务之间添加一个消息队列,即使A服务后边的某个服务出现了问题也不会影响A服务的正常使用,而没有被消费的消息会缓存在MQ中,等服务正常后就会去消费这条未被消费的消息。很好的提升了用户体验感
异步处理
异步处理:可以借助IO模型来理解,就是说A服务要调用B服务,但是B服务在处理时需要花费一定的时间才能完成,那么B服务是否已经完成了,对于A服务来说是不知道的,它要么通过不断轮询的方式来查看B的执行状态,要么提供一个callback接口让B完成后调用接口通知A;在引入MQ后,这两种形式显得比较笨重,我们只需要插入一个消息总线来作为二者的通信媒介,A调用B服务后,只需要监听MQ中是否有B处理完成的消息,B在处理完成后也只需要给MQ发送一条 消息即可,极大的减小的系统性能的开支
MQ分类
1、ActiveMQ
优点:单机吞吐量万级,时效性ms级,可用性高,基于主从架构实现高可用性,较低的概率丢失数据
缺点:维护越来越少,高吞吐量场景使用较少
2、Kafka:大数据常用的消息中间件,吞吐量高达百万级tps,大数据领域采用实时计算以及日志采集而被大规模使用
优点:
·性能卓越,单机写入TPS约百万条每秒,吞吐量,时效性极高
·分布式的消息中间件,一个数据有多个副本,少数机器宕机不会导致数据丢失、服务不可用
·消费者通过pull方式获取消息,消息有序,可保证每条消息被消费且只被消费一次(可控的)
·有优秀的第三方界面管理,在日志领域比较成熟,主要支持简单的MQ功能
缺点:
·Kafka 单机超过 64 个队列/分区,Load 会发生明显的飙高现象
·队列越多,load 越高,发送消 息响应时间变长,
·使用短轮询方式,实时性取决于轮询间隔时间,消费失败不支持重试;
·支持消息顺序, 但是一台代理宕机后,就会产生消息乱序,社区更新较慢;
3、RocketMQ:阿里巴巴产品,底层语言java,参照了Kafka并进行了改进
优点:
·单机吞吐量十万级,可用性高,分布式架构,消息可以做到0丢失
·MQ功能比较完善,扩展性好,支持10亿级别的消息堆积且不会导致性能下降
缺点:
·支持客户端语言不多,目前仅支持java以及C++(还不太成熟)
·没有在MQ核心中实现JMS等接口,有些系统需要迁移大量代码
4、RabbitMQ:是一个在AMQP(高级消息队列协议)基础上完成的,可复用的企业消息系统,是当前最主流的消息中间件
优点:
·由于erlang语言的高并发特性,性能好
·吞吐量到万级,MQ功能比较完备、健壮、稳定、易用、跨平台、支持多语言
·支持AJAx文档齐全
·好用,社区活跃度高
缺点:
·商业版需要收费,学习成本高
MQ的选择
·Kafka,适合使用在大量数据的互联网的数据业务,特别是日志采集功能上优选Kafka
·RocketMQ,适用于金融互联网领域以及并发场景比较高的应用领域
·RabbitMQ,适用数据量不大,要求功能比较完备的场景
RabbitMQ
什么是RabbitMQ
·概念:
·消息中间件,负责消息的接收,存储与转发,
·是一套开源(MPL)的消息队列服务软件,是由 LShift 提供的一个 Advanced Message Queuing Protocol (AMQP) 的开源实现,由以高性能、健壮以及可伸缩性出名的 Erlang 写成。
四大核心
·生产者:消息发送者
·交换机:负责消息的接收以及推送,决定将接收到的消息推送到特定的队列中、多个队列、丢弃
·队列:
·RabbitMQ内部使用的一种数据结构,负责存储流经RabbitMQ和应用程序的消息
·仅受主机内存和磁盘的限制约束,本质是一个消息缓冲区
·许多生产者可 以将消息发送到一个队列,许多消费者可以尝试从一个队列接收数据
·消费者:接收消息的应用程序,消费者与中间件大多时候不在同一个机器上,同一个应用程序可以是生产者也可以是消费者
RabbitMQ结构图:
核心模块
RabbitMQ工作原理图:
名词介绍
Broker:接收和分发消息的应用,RabbitMQ Server就是Message Broker
Virtual host:
·出于多租户和安全因素设计的,将AMQP的基本组件划分到一个虚拟分组中,类似网络中的namespace概念。
·当多个不同的用户使用同一个RabbitMQ Server提供的服务时,可划分出多个vhost,每个用户在自己的vhost创建exchange/queue等
Connection:publisher/consumer 和 broker 之间的 TCP 连接
Channel:
·如果每一次访问 RabbitMQ 都建立一个 Connection,在消息量大的时候建立 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)、fanout(multicast)
Queue:存储消息的地方
Binding:exchange 和 queue 之间的虚拟连接,binding 中可以包含 routing key,Binding 信息被保 存到 exchange 中的查询表中,用于 message 的分发依据
安装(linux)
1.官网地址 https://www.rabbitmq.com/download.html
2.文件上传 上传到/usr/local/software 目录下(如果没有 software 需要自己创建)
3.安装文件(分别按照以下顺序安装)
**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 服务 chkconfig rabbitmq-server on
启动服务 /sbin/service rabbitmq-server start
查看服务状态 /sbin/service rabbitmq-server status
停止服务(选择执行) /sbin/service rabbitmq-server stop
开启 web 管理插件 rabbitmq-plugins enable rabbitmq_management
用默认账号密码(guest)访问地址 http://47.115.185.244:15672/出现权限问题
4.添加一个新的用户
创建账号 rabbitmqctl add_user admin 123
设置用户角色 rabbitmqctl set_user_tags admin administrator
设置用户权限 set_permissions [-p ]
**rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"**
用户 user_admin 具有/vhost1 这个 virtual host 中所有资源的配置、写、读权限
当前用户和角色 rabbitmqctl list_users
5.再次利用 admin 用户登录
6.重命令 关闭应用的命令为 rabbitmqctl stop_app
清除的命令为 **rabbitmqctl reset**
重新启动命令为 rabbitmqctl start_app
实战案例
·在下图中,“ P”是我们的生产者,“ C”是我们的消费者。中间的框是一个队列-RabbitMQ 代 表使用者保留的消息缓冲区
HelloWord
1.pom
<dependencies>
<!--rabbitmq 依赖客户端-->
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.8.0</version>
</dependency>
<!--操作文件流的一个依赖-->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
</dependencies>
2.MessageProducer
/**
生产者:
1、创建一个connectionFactory工厂
2、利用connectionFactory工厂设置mq主机、账号、密码
3、通过connectionFactory.newConnection()创建一个Connection
4、通过connection创建一个Channel
5、通过channel.queueDeclare来指定消息推送队列
6、添加消息
7、通过 channel.basicPublish()【 message.getBytes()将消息转换为二进制流便于在网络中传输】将消息推送到指定的QUEUE中
*/
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
public class MessageProducer {
private final static String QUEUE_NAME = "Hello";
public static void main(String[] args) throws Exception {
//创建一个连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setUsername("guest");
factory.setPassword("guest");
//channel实现了自动close接口,自动关闭不需要显示关闭
try(Connection connection = factory.newConnection();
Channel channel = connection.createChannel()
) {
/**
* 生成一个队列
* 1.队列名称
* 2.队列里面的消息是否持久化 默认消息存储在内存中
* 3.该队列是否只供一个消费者进行消费 是否进行共享 true 可以多个消费者消费
* 4.是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true 自动删除
* 5.其他参数
*/
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
String message = "hello word!";
/**
* 发送一个消息
* 1.发送到那个交换机
* 2.路由的 key 是哪个,
* 3.其他的参数信息
* 4.发送消息的消息体
*/
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println("message send successful");
}
}
}
3.MessageConsumer
/**
消费者:
1、创建一个ConnectionFactory
2、通过factory来设置mq主机、账户、密码
3、通过Factory.newConnection创建一个Connection
4、通过connection.createChannel创建一个Channel
5、通过channel.basicConsume()来接收消息,
*/
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class MessageConsumer {
private final static String QUEUE_NAME = "Hello";
public static void main(String[] args) throws Exception{
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setUsername("guest");
factory.setPassword("guest");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
System.out.println("等待接收消息....");
//推送的消息如何进行消费的接口回调
DeliverCallback deliverCallback=(consumerTag, delivery)->{
String message= new String(delivery.getBody());
System.out.println(message);
};
//取消消费的一个回调接口 如在消费的时候队列被删除掉了
CancelCallback cancelCallback=(consumerTag)->{
System.out.println("消息消费被中断");
};
/**
* 消费者消费消息
* 1.消费哪个队列
* 2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
* 3.消费成功
* 3.消费者未成功消费的回调
*/
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}
运行结果:
Producer:
consumer:
work Queues
什么是Work Queues
·概念:工作队列(又称任务队列)主要思想是避免立即执行资源密集型任务,而不得不等待它完成。相反我们安排任务在之后执行,把任务封装为消息并将其发送到队列,在后台运行的工作进程弹出任务并最终执行作业,当有多个工作线程时,这些工作线程将一起处理这些任务。
·简单的说就是利用MQ作为任务缓存区,避免所有的任务全部都被塞进内存,等待cpu调度,损耗内存资源;
实战案例
轮询分发消息
1、model
rabbitmq-work-queues
2、pom
<dependencies>
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
</dependency>
<!--操作文件流的一个依赖-->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</dependency>
</dependencies>
3、编写main入口
CONSUMER,CONSUMER1(唯一的区别就是类名不一样)
package com.wzs.rabbitmq.consumer;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.wzs.rabbitmq.util.ChannenUtil;
import java.io.IOException;
import java.util.logging.Logger;
public class MessageConsumer1 {
private static Channel channel= ChannenUtil.getChannnel();
private static final Logger log = Logger.getLogger(MessageConsumer1.class.getName());
public static void main(String[] args) throws IOException {
DeliverCallback deliverCallback=(consumerTag,deliver)->{
String message = new String(deliver.getBody());
System.out.println("consumer1在消费:"+message);
};
CancelCallback cancelCallback=(consumerTag)->{
log.info("消息传递中断!");
};
channel.basicConsume(ChannenUtil.QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}
PRODUCER
package com.wzs.rabbitmq.producer;
import com.rabbitmq.client.Channel;
import com.wzs.rabbitmq.consumer.MessageConsumer;
import com.wzs.rabbitmq.util.ChannenUtil;
import java.io.IOException;
import java.util.logging.Logger;
public class ProducerMian {
private static Logger log = Logger.getLogger(MessageConsumer.class.getName());
public static void main(String[] args) {
Channel channel= ChannenUtil.getChannnel();
try {
int count =0;
while(true) {
String message = "你好!欢迎来到work队列!"+count;
channel.queueDeclare(ChannenUtil.QUEUE_NAME,false,false,false,null);
channel.basicPublish("", ChannenUtil.QUEUE_NAME, null, message.getBytes());
log.info("消息发送成功!");
count++;
if(count>5){
break;
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
4、编写业务代码
UTIL(工具类)
package com.wzs.rabbitmq.util;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class ChannenUtil {
public static final String QUEUE_NAME="wuzeshun";
public static Channel getChannnel(){
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setUsername("guest");
factory.setPassword("guest");
try {
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
return channel;
} catch (IOException e) {
throw new RuntimeException(e);
} catch (TimeoutException e) {
throw new RuntimeException(e);
}
}
}
5、运行测试
消息应答
为什么引入消息应答:
·对于消费者而言,在接收到某条消息后开始处理,若在处理期间发生故障挂掉了,由于MQ在发送完一条消息后就会将其标记为删除,这就会导致生产者后续发送给已挂掉的消费者的消息以及未处理完毕的消息丢失,因为该消费者由于故障无法接收消息。
·概念:为了保证消息在发送过程中不丢失,消费者在接收到消息并且处理该消息之后,告诉MQ该消息已消费,可以被删除了;
自动应答
·消息发送后会立即被认为已传送成功,在这种模式下需要在高吞吐量和数据传输安全性方面做权衡。这种模式仅适用于在消费者可以高效并以某种速率能够处理这些消息的情况下使用。
原因:
·若消息在接收前,消费者出现连接故障或channel关闭问题就会导致消息丢失,在这种情况下,消费者可以传递过载消息,没有对传递的消息数量进行限制,这会带来消息积压问题(消费者处理无法及时消息),最终导致内存耗尽,这些消费者线程被操作系统杀死;
消息应答方法
channel.basicAck()//用于肯定确认,rabbitMQ已经知道该消息发送成功并已经被处理,可被丢弃了
A:channel.basicNack()//用于否定确认
B:channel.basicReject()//用于否定确认
/**
两个消息否认对比:
B比A少一个参数
B不处理该消息了就直接拒绝,可以将其丢弃
*/
Multiple的解释
·手动应答的好处:可以批量应答并减少网络拥堵
·channel.basicAck(deliveryTag , true(multiple));
·multiple的Boolean取值意义:
·TRUE:代表批量应答channel上未应答的消息
·FALSE:只应答当前消息,
·图示:
消息自动重新入队
·概念:若某个消费者由于故障(channel关闭、连接关闭或Tcp连接丢失),导致消息未发送ack确认,RabbitMQ将了解到消息为完全处理,则会将这条消息重新排入队列中;此时若其他消费者能够处理该消息,那么RabbitMQ就会立即将其分发给它进行处理;
·作用:即使消费者存在偶尔死亡也能够保证消息不会丢失
·图示:
实战案例:
producer:y与上一实例相同未做改变
consumer大体两个基本一样,下面是修改的部分
//consumer 迅速反馈
DeliverCallback deliverCallback=(consumerTag,deliver)->{
String message = new String(deliver.getBody());
try {
Thread.sleep(1000);
log.info(message);
//消息标记tag,是否批量处理
channel.basicAck(deliver.getEnvelope().getDeliveryTag(),false);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
};
//consumer1 休眠30秒模仿处理时间超时长场景
DeliverCallback deliverCallback=(consumerTag,deliver)->{
String message = new String(deliver.getBody());
try {
Thread.sleep(30000);
log.info("consumer1在消费:"+message);
//消息标记tag,是否批量处理
channel.basicAck(deliver.getEnvelope().getDeliveryTag(),false);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
};
运行结果:
RabbitMQ持久化
如何保证生产者生产的消息不丢失?
·RabbitMQ在默认情况下:退出或崩溃时,它会将队列以及消息忽略掉,这种情况下生产者生产的消息容易被丢失;
·解决办法:告知RabbitMQ不要这么做并将队列以及消息标记为持久化
队列实现持久化:
/**此时的队列是非持久化的,也就是说RabbitMQ重启时队列会被删掉
想要持久化的队列只要将durable参数的Boolean值设置为true即可
*/
channel.queueDeclare(ChannenUtil.QUEUE_NAME,false,false,false,null);
注意事项:若当前以存在队列,且不是持久化的,那么必须先将其删掉或重新创建新的队列,否则启动程序将会报错
错误类型:
消息实现持久化:
/**
实现消息持久化需要将:MessageProperties.PERSISTENT_TEXT_PLAN属性添加上
*/
channel.basicPublish("", ChannenUtil.QUEUE_NAME, null, message.getBytes());
//消息持久化版:
channel.basicPublish("", ChannenUtil.QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
·对于简单的任务队列而言,目前的持久化操作已经足够保证消息不会丢失,但是在面对复杂的业务场景时这是远远不够的。也就是说将消息标记为可持久化并不能完全保证消息不会被丢失,因为消息并不是马上会被写入磁盘而是先被缓存,要是此时发生故障依旧会导致消息被丢失
不公平分发
·此之前采用的消息分发策略是:轮询分发,也就是说RabbitMQ会将消息平摊到队列中各个消费者上去处理;
·这种场景下容易出现这样的问题:
·能够快速处理的消费者会比较悠闲,而慢的则十分繁忙,面对这种情景RabbitMQ仍旧按轮询机制分发消息的话就显得不是很友好,特别是在高并发场景下,可能会导致某个服崩盘
·怎么解决这种问题呢?
int prefetchCount = 1;
//设置消息分发机制为:非公平的分发机制
channel.basicQos(prefetchCount);
·图示:
·什么意思呢?
·说白了就是当MQ给消费者发送消息的时候,要是当前的消费者没有处理完已接收的消息或没有给RabbitMQ发送ACK确认的话,RabbitMQ就不会再将消息分发给这个消费者处理,转而将其发送给空闲的消费者处理
·若所有的消费者都处于繁忙状态,而生产者任然在不断地给队列发送消息,那么就会造成消息大量积压、队列被撑满的问题,此时要么重新创建添加新的worker,要么改变其他存储任务的策略
预取值
·消息的发送是异步的,因此channel上的消息任何时候都不止有一个消息;
·而来自消费者的手动或自动确认也是异步的,因此在channel中存在一个未确认的消息缓存区;
·自动应答传输消息的速率是最佳的,但在这种场景下已传递的消息而尚未被消费的消息也会增加,从而增加了RAM的消耗
·我们应当小心使用这种有无限预处理的自动或手动确认模式,消费者如果消费了大量的消息而没有确认的话,会导致消费者在连接节点的内存消耗变大;
·所以我们希望开发者能够限制未确认的消息缓冲区大小来避免缓冲区中存在无限制的未确认消息问题,一般通过设置basicQos()的参数来限制缓冲区大小;
·basicQos()的参数值的大小决定了缓冲区中允许的未确认消息的最大数量,一旦超过就会RabbitMQ就会停止向该channel推送消息,直到该缓冲区中的一个向RabbitMQ发送ACK确认后才能继续向该channel推送消息
·消息应答与Qos的预取值对用户吞吐量有重大影响,通常增加预取值会提高向消费者传递消息的速度;
·所以寻找一个合适的Qos值是一个反复试验的过程,面对不同的负载Qos的值也不同,通常100到300内的值是可以提供最佳的吞吐量,且不会给消费者带来太大的风险;
·预取值为1是最保守的,同时吞吐量也将会变得的很低,特别是在消费者连接延迟很严重或等待时间较长的环境中,对于大多数的应用来说,稍微高一些的值将是最佳的。
·图示:
发布确认
·原理:
·首先生产者将信道设置成confirm模式,一旦信道进入confirm模式,那么所有在上面发布的消息都会被指派一个唯一的ID(从1开始);
·一旦消息被投递到所有匹配的队列后,broker就会发送一个确认消息给生产者(包含消息的id),告知生产者消息已经到达队列;
·若消息是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker回传给生产者的确认消息中delivery-tag域包含了确认消息的序列号;
·此外broker也可以设置basicAck()的multiple域,表示这个序列号之前的消息都已经得到了处理。
·confirm最大的好处就是它是异步的,一旦发布消息,生产者应用程序就可以在等信道返回确认消息的同时继续发送下一条消息;
·当消息最终得到确认后,生产者可通过回调方法来处理该确认消息,若RabbitMQ因为自身内部的错误导致消息丢失,就会发送一条Nack消息,且同样被消费者在回调方法中处理。
发布确认的策略
开启发布确认的方法
·发布确认默认是未开启的,开启需要调用confirmSelect()方法
Channel channel = connection.createChannel();
channel.confirmSelect();
单个确认发布:
·一种简单的确认方式以及同步确认发布方式,即发布一条消息后需要等待其被确认发布后才能发布后续的消息,waitForConfirmOrdie(long)方法只有在消息被确认的时候才返回,在等待指定的时间范围内该消息未被确认就会抛出异常。
·缺陷:发布速度特别慢,因为如果没有确认发布的消息就会阻塞所有后续消息的发布,吞吐量:每秒不超过数百条发布消息;
public static void publishMessageIndividually()throws Exception{
try(Channel channel =ChannenUtil.getChannnel()){
String queueName= UUID.randomUUID().toString();
channel.queueDeclare(queueName,false,false,false,null);
//开启发布确认
channel.confirmSelect();
long begin =System.currentTimeMillis();
for(int i= 0; i < MESSAGE_COUNT;i++){
String message=i+"条消息";
channel.basicPublish("",queueName,null,message.getBytes());
if(channel.waitForConfirms()){
log.info("消息发送成功");
}
}
long end=System.currentTimeMillis();
log.info("发布"+MESSAGE_COUNT+"个单独确认消息,耗时:"+(end-begin)+"ms");
}
}
批量确认分布
·阻塞式的同步批量消息发布,与单个确认发布相比,其每次发布和等待确认的消息条数变多了而已(每次发布一批,等待一批消息的确认),在速度和吞吐量上有了一定的提升;
·缺点就是当发生故障导致发布出现问题时,无法定位发生问题的消息,因此需要将整个批处理保存在内存中,以记录重要的信息而后重新发布消息。
public static void publishMessageIndividuallyB()throws Exception{
int basicSize=5;//批量处理数
int messageCount=0;//统计数
try(Channel channel =ChannenUtil.getChannnel()){
String queueName= UUID.randomUUID().toString();
channel.queueDeclare(queueName,false,false,false,null);
//开启发布确认
channel.confirmSelect();
long begin =System.currentTimeMillis();
for(int i= 0; i < MESSAGE_COUNT;i++){
String message=i+"条消息";
channel.basicPublish("",queueName,null,message.getBytes());
messageCount++;
if(messageCount==basicSize){
channel.waitForConfirms();
messageCount=0;
}
}
//确保没有漏掉为确认的消息
if(messageCount>0){
channel.waitForConfirms();
}
long end=System.currentTimeMillis();
log.info("发布"+MESSAGE_COUNT+"个批量确认消息,耗时:"+(end-begin)+"ms");
}
}
异步确认发布
·性价比高、可靠性、效率都比上面两个高很多,利用回调函数来达到消息可靠性传递,中间件也是通过函数回调开保证消息是否投递成功
·实现原理图:
public static void publishMessageAsync()throws Exception{
try(Channel channel = ChannenUtil.getChannnel()){
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName,false,false,false,null);
//开启发布确认
channel.confirmSelect();
/**
* 创建一个线程安全有序的哈希表(高并发场景适用)
* 1、将序号与消息进行关联
* 2、通过序列号即可完成批量删除
* 3、支持并发访问
*/
ConcurrentSkipListMap<Long,String> outstandingConfirms = new ConcurrentSkipListMap<>();
/**
* 确认收到消息的回调
* 1、消息序列号
* 2、true可以确定小于等于当前的序列号消息
* false确认当前序列号
*/
ConfirmCallback AckCallback = (sequenceNumber, multiple)->{
if(multiple){
//返回小于等于当前序列号的未确认消息
ConcurrentNavigableMap<Long, String> confirmed=outstandingConfirms.headMap(sequenceNumber,true);
//清楚该部分未确认消息
confirmed.clear();
}else {
//只清除当前序列号的消息
outstandingConfirms.remove(sequenceNumber);
}
};
/**
* 未确认收到消息的回调
* 1、消息序列号
* 2、true可以确定小于等于当前的序列号消息
* false确认当前序列号
*/
ConfirmCallback nackCallback = (sequenceNumber, multiple)->{
String message = outstandingConfirms.get(sequenceNumber);
System.out.println("发布的消息:"+message+"未被确认,序列号"+sequenceNumber);
};
/**
* 添加一个异步确认的监听器
* 1、确认收到消息的回调
* 2、未收到消息的回调
*/
channel.addConfirmListener(AckCallback,null);
long begin = System.currentTimeMillis();
for(int i = 0; i< MESSAGE_COUNT ; i++){
String message="条消息"+i;
/**
* channel.getNextPublishSeqNo()获取下一个消息的序列号
* 通过序列号与消息进行一个关联
* 全部是未确认的消息体
*/
outstandingConfirms.put(channel.getNextPublishSeqNo(),message);
channel.basicPublish("",queueName,null,message.getBytes());
}
long end = System.currentTimeMillis();
System.out.println("发布"+MESSAGE_COUNT+"个异步确认消息,耗时"+(end-begin)+"ms");
}
}
测试结果:
结论:
单独发布消息
同步等待确认,简单,但吞吐量非常有限。
批量发布消息
批量同步等待确认,简单,合理的吞吐量,一旦出现问题但很难推断出是那条 消息出现了问题。
异步处理:
最佳性能和资源使用,在出现错误的情况下可以很好地控制,但是实现起来稍微难些
如何处理异步未确认消息
最好的解决的解决方案就是把未确认的消息放到一个基于内存的能被发布线程访问的队列, 比如说用 ConcurrentLinkedQueue 这个队列在 confirm callbacks 与发布线程之间进行消息的传 递。
交换机
·发布/订阅:消息传达给多个消费者
·简单日志系统:
·发送日志消息:发布消息
·消费者:接收消息并打印
·概念:
·RabbitMQ消息传递模型的核心思想是:生产者只负责生产消息并将消息推给交换机,而不关注具体发送给那个队列;
·交换机负责接收生产者发送的消息并将消息推入队列,且交换机必须知道如何处理收到的消息;
·交换机也决定者该消息的去向(特定队列、多队列或者丢弃)
·图示:
Exchanges类型
·直接direct
·主题topic
·扇出fanout
无名交换机:
//该方法的第一各参数就是交换机的名称,当设置为空字符串时,使用的是默认的交换机或者无名字的交换机,消息路由能发送到队列其实是由routingKey(bindingKey)绑定key指定的【若它存在的话】
channel.basicPublish("",...)
零时队列
·队列名称:消费者消费那个队列的消息是通过队列名称绑定的
·当我们连接到Rabbit时,需要需要创建一个全新的空队列
·可以创建一个具有随机名称的队列或者让服务器随机选择一个队列名称来提供服务
·一旦断开与消费者的连接,队列就回删除
·零时队列的创建方式:
String queueName =channel.queueDeclare().getQueue();
web界面创建结果:
绑定(bindings)
什么是binding
·是exchange与queue之间的桥梁,告知我们exchange和哪个队列进行了绑定关系
图示:X与Q1、Q2进行了绑定关系
Fanout
什么是Fanout
·简单的exchange类型,负责将接收到的所有消息广播到它知道的所有队列中
·默认的exchange类型:
实战案例
·图示:
Logs与零时队列的绑定关系:
java代码:
//consumer1
package com.wzs.rabbitmq;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @author WZS
*/
public class Consumer1 {
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = ChannelUtil.getChannel();
//设置交换机
channel.exchangeDeclare(ChannelUtil.EXCHANGE_NAME,"fanout");
//创建临时队列,
String queueName = channel.queueDeclare().getQueue();
//绑定队列
channel.queueBind(queueName,ChannelUtil.EXCHANGE_NAME,"");
//准备接收消息并打印消息
DeliverCallback deliverCallback = (message1,message2)->{
System.out.println("广播消息是:"+new String(message2.getBody()));
};
//接收消息
channel.basicConsume(queueName,true,deliverCallback, message->{});
}
}
//consumer2
package com.wzs.rabbitmq;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class consumer2 {
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = ChannelUtil.getChannel();
//设置交换机
channel.exchangeDeclare(ChannelUtil.EXCHANGE_NAME,"fanout");
//创建临时队列
String queueName = channel.queueDeclare().getQueue();
//绑定队列
channel.queueBind(queueName,ChannelUtil.EXCHANGE_NAME,"");
//准备接收消息并保存在本地文件
DeliverCallback deliverCallback = (M1,M2)->{
File file = new File("D:\\rabbitmq.txt");
FileUtils.writeStringToFile(file,new String(M2.getBody()),"UTF-8");
System.out.println("写入成功!");
};
//接收消息
channel.basicConsume(queueName,true,deliverCallback,M->{});
}
}
package com.wzs.rabbitmq;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class ChannelUtil {
public static final String EXCHANGE_NAME="logs";
public static Channel getChannel(){
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setUsername("guest");
factory.setPassword("guest");
try {
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
return channel;
} catch (IOException e) {
throw new RuntimeException(e);
} catch (TimeoutException e) {
throw new RuntimeException(e);
}
}
}
注意:在设置交换机的时候要看好函数参数列表,避免出现队列创建无效问题
Direct exchange
·direct能够增加其消息类型的灵活性,也就是说在fanout类型的交换机下什么类型的消息都会被广播出去,但在direct下可以指定类型的消息进行推送(error、info、warning)
·工作方式:消息只被发送到绑定的routingKey队列中去
图示:
· 在这种绑定情况下生产者发布消息到 exchange 上:
·绑定键为 orange 的消息会被发布到队列 Q1
·绑定键为 black/green 消息会被发布到队列 Q2
·其他消息类型的消息将被丢弃。
多重绑定
如果 exchange 的绑定类型是 direct,且它绑定的多个队列的 key 都相同,那么它与fanout类型的exchange的表现将是相似的,跟广播差不多
实战案例
·console
package com.wzs.rabbitmq;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import java.io.IOException;
public class DirectConsoleMian {
public static void main(String[] args) throws IOException {
Channel channel = ChannelUtil.getChannel();
//设置交换机
channel.exchangeDeclare(ChannelUtil.EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
//创建队列
String queueName="console";
channel.queueDeclare(queueName,false,false,false,null);
channel.queueBind(queueName,ChannelUtil.EXCHANGE_NAME,"waring");
channel.queueBind(queueName,ChannelUtil.EXCHANGE_NAME,"info");
//准备接收消息并在控制台打印
System.out.println("waiting the message from producer");
//回调函数
DeliverCallback deliverCallback = (mTag,message)->{
System.out.println("the key is :"+message.getEnvelope().getRoutingKey()+" \t the message is:"+new String(message.getBody()));
};
//接收消息
channel.basicConsume(queueName,true,deliverCallback,tag->{});
}
}
·disk
package com.wzs.rabbitmq;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
public class DirectDiskMian {
public static void main(String[] args) throws IOException {
Channel channel = ChannelUtil.getChannel();
//设置交换机
ChannelUtil.setExchangeClass(channel,"direct");
//创建队列
String queueName="disk";
channel.queueDeclare(queueName,false,false,false,null);
//绑定队列
channel.queueBind(queueName,ChannelUtil.EXCHANGE_NAME,"error");
//接收消息并准备写入磁盘
System.out.println("waiting for writing to disk");
//回调函数
DeliverCallback deliverCallback = (mTag,message)->{
File file = new File("D:\\DirectLogs.txt");
String mess= new String(message.getBody());
FileUtils.writeStringToFile(file,"消息是:"+mess+"绑定的键值是:"+message.getEnvelope().getRoutingKey(),"utf-8");
System.out.println("write successful");
};
channel.basicConsume(queueName,true,deliverCallback,tag->{});
}
}
·producer
package com.wzs.rabbitmq;
import com.rabbitmq.client.Channel;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
/**
* @author WZS
*/
public class EmailLogSendMian {
public static void main(String[] args) throws IOException {
Channel channel = ChannelUtil.getChannel();
//创建一个消息mapper
Map<String,String> meaasgeMap = new HashMap<>();
meaasgeMap.put("error","this is log of error about system");
meaasgeMap.put("waring","this message about waring");
meaasgeMap.put("info","simple info");
meaasgeMap.put("debug","debug info");
//发送消息
for(String k:meaasgeMap.keySet()){
String message= meaasgeMap.get(k);
channel.basicPublish(ChannelUtil.EXCHANGE_NAME,k,null,message.getBytes(StandardCharsets.UTF_8));
System.out.println("sending message is:"+message);
}
}
}
·util增加交换机绑定方法
public static void setExchangeClass(Channel channel,String className) throws IOException {
channel.exchangeDeclare(EXCHANGE_NAME,className);
}
Topics
·说白了就是在Direct类型的基础上添加了更加细致的类比映射,如:info.base、info.advantage等
·要求:
·发送到topic类型的exchange上的消息的routing_key不能随便写,必须满足一定要求
·必须是单词列表,且以点号分隔开
·单词最多不能超过255个字节
· 可以代替一个单词*
·#可以代替零个或多个单词
匹配案例:
Q1–>绑定的是中间ORange带3个单词的字符串*.orange. *
Q2–>绑定的是最后一个单词是rabbit的3个单词的* .*.rabbit
第一个单词是lazy的多单词(lazy.#)
注意:
·当一个队列绑定的键值是#,那么这个队列接收所有数据,就有点像fanout
·若一个队列绑定的键值中没有#和*出现,那么该队列绑定的类型就是direct了
实战案例
·FirstQueue
package com.wzs.rabbitmq;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import java.io.IOException;
public class FirstQueueMain {
public static void main(String[] args) throws IOException {
Channel channel = ChannelUtil.getChannel();
//设置交换机
ChannelUtil.setExchangeClass(channel,"topic");
//创建队列
String queueName="FirstQueue";
channel.queueDeclare(queueName,false,false,false,null);
//绑定交换机
channel.queueBind(queueName,ChannelUtil.EXCHANGE_NAME,"*.orange.*");
//准备接收消息
DeliverCallback deliverCallback = (mTag,message)->{
System.out.println("the message is"+new String(message.getBody())+"\t the queue_bind key is:"+message.getEnvelope().getRoutingKey());
};
channel.basicConsume(queueName,true,deliverCallback,tag->{});
}
}
·SecondQueue
package com.wzs.rabbitmq;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import java.io.IOException;
public class SecondQueueMian {
public static void main(String[] args) throws IOException {
Channel channel = ChannelUtil.getChannel();
//设置交换机
ChannelUtil.setExchangeClass(channel,"topic");
//创建队列
String queueName = "secondQueue";
channel.queueDeclare(queueName,false,false,false,null);
//绑定队列
channel.queueBind(queueName,ChannelUtil.EXCHANGE_NAME,"*.*.rabbit");
channel.queueBind(queueName,ChannelUtil.EXCHANGE_NAME,"lazy.#");
//等待接收消息并打印消息
DeliverCallback deliverCallback = (mtag,message)->{
System.out.println("the message is :"+new String (message.getBody())+"\t message key is:"+message.getEnvelope().getRoutingKey());
};
//接收消息
channel.basicConsume(queueName,true,deliverCallback,tag->{});
}
}
·Producer
package com.wzs.rabbitmq;
import com.rabbitmq.client.Channel;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
public class MessageSendMian {
public static void main(String[] args) throws IOException {
Channel channel = ChannelUtil.getChannel();
//创建消息map
HashMap<String,String> messageMap = new HashMap<>();
messageMap.put("quick.orange.rabbit","被队列 Q1Q2 接收到");
messageMap.put("lazy.orange.elephant","被队列 Q1Q2 接收到");
messageMap.put("quick.orange.fox","被队列 Q1 接收到");
messageMap.put("lazy.brown.fox","被队列 Q2 接收到");
messageMap.put("lazy.pink.rabbit","虽然满足两个绑定但只被队列 Q2 接收一次");
messageMap.put("quick.brown.fox","不匹配任何绑定不会被任何队列接收到会被丢弃");
messageMap.put("quick.orange.male.rabbit","是四个单词不匹配任何绑定会被丢弃");
messageMap.put("lazy.orange.male.rabbit","是四个单词但匹配 Q2");
//消息发送
for(String key: messageMap.keySet()){
String message = messageMap.get(key);
channel.basicPublish(ChannelUtil.EXCHANGE_NAME,key,null,message.getBytes(StandardCharsets.UTF_8));
System.out.println("send message is:"+message+" \t the routing_key is:"+key);
}
}
}
·工具类未变,知识交换机名称变了
死信队列
什么是死信队列?
·概念:说白了就是用来处理那些无法被消费者消费的消息的队列
·无法消费的消息:生产者将消息直接投递到broker或者queue中,消费者取出这些消息并进行处理,但某些时刻由于特定原因导致queue中的某些消息无法被消费,且没有相应的处理方法,那么这些消息就成了死信
·应用场景:保证业务的消息数据不丢失,当消息发生异常则将消息投入死信队列中;如:已下单但在规定时间内未完成付款的订单则会被自动取消
死信的来源
·消息TTL过期
·队列达到最大长度(队列满了,无法添加数据到mq中)
·消息被拒绝(basic.reject或basic.nack)且requeue=false
实战案例
架构图:
消息队列常见的参数表:
TTl过期
consumer1
package com.wzs.rabbitmq;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class Consumer01 {
private static final String NORMAL_EXCHANGE="normal-exchange";
private static final String DEATH_EXCHANGE="death-exchange";
public static void main(String[] args) throws IOException {
Channel channel = ChannelUtil.getChannel();
//设置普通交换机
channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
//设置死信交换机
channel.exchangeDeclare(DEATH_EXCHANGE, BuiltinExchangeType.DIRECT);
//创建死信队列
String dqueueName="dqueue";
channel.queueDeclare(dqueueName,false,false,false,null);
//绑定死信队列与死信交换机
channel.queueBind(dqueueName,DEATH_EXCHANGE,"liwei");
//创建普通队列map参数,目的是为了将无法消费的消息投递到死信队列
Map<String,Object> argument = new HashMap<>();
argument.put("x-dead-letter-exchange", DEATH_EXCHANGE);
argument.put("x-dead-letter-routing-key","liwei");
//创建普通队列,并设置map参数
String nqueueName="nqueue";
channel.queueDeclare(nqueueName,false,false,false,argument);
//绑定普通队列与普通交换机
channel.queueBind(nqueueName,NORMAL_EXCHANGE,"wzs");
//接收消息回调函数
DeliverCallback deliverCallback = (consumerTag,message)->{
System.out.println("Consumer01 receive message is :"+ new String(message.getBody()));
};
channel.basicConsume(nqueueName,true,deliverCallback,tag->{});
}
}
consumer2
package com.wzs.rabbitmq;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import java.io.IOException;
public class Consumer02 {
private static final String DEAD_EXCHANGE="death-exchange";
public static void main(String[] argv) throws Exception {
Channel channel = ChannelUtil.getChannel();
channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
String deadQueue = "dqueue";
channel.queueDeclare(deadQueue, false, false, false, null);
channel.queueBind(deadQueue, DEAD_EXCHANGE, "death");
System.out.println("等待接收死信队列消息.....");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println("Consumer02 接收死信队列的消息" + message);
};
channel.basicConsume(deadQueue, true, deliverCallback, consumerTag -> {
});
}
}
producer
package com.wzs.rabbitmq;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class DeathProducerMian {
private static final String NORMAL_EXCHANGE="normal-exchange";
public static void main(String[] args) throws IOException {
Channel channel = ChannelUtil.getChannel();
channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
//设置TTL时间
AMQP.BasicProperties properties =
new AMQP.BasicProperties().builder().expiration("5000").build();
//发送消息
for (int i = 0; i < 10; i++) {
String message="info"+i;
channel.basicPublish(NORMAL_EXCHANGE,"wzs",properties,message.getBytes(StandardCharsets.UTF_8));
System.out.println("send message:"+message);
}
}
}
最大长度(最大消息数)
·代码总体不变,以下是变更部分,测试前把原先创建的队列先清除
/**
1、对于生产者来说,需要变更的是吧ttl参数去掉,将 channel.basicPublish()方法的properties参数置为null
2、consumer来说添加一个新的argument参数
*/
//设置普通队列并创建map参数,设置最大消息长度
Map<String,Object> argument = new HashMap<>();
argument.put("x-dead-letter-exchange",D_EXCHANGE);
argument.put("x-dead-letter-routing-key","li");
argument.put("x-max-length",6);
String N_queue="queue";
channel.queueDeclare(N_queue,false,false,false,argument);
//绑定队列
channel.queueBind(N_queue,N_EXCHANGE,"wzs");
消息被拒
·仅仅改动Consumer01
package com.wzs.rabbitmq;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* @author WZS
*/
public class ConsumerMian01 {
private static final String N_EXCHANGE="normal-exchange";
private static final String D_EXCHANGE="dead-exchange";
public static void main(String[] args) throws IOException {
Channel channel = ChannelUtil.getChannel();
//设置普通交换机
channel.exchangeDeclare(N_EXCHANGE, BuiltinExchangeType.DIRECT);
//设置死信交换机
channel.exchangeDeclare(D_EXCHANGE,BuiltinExchangeType.DIRECT);
//创建死信队列
String D_queue="dead";
channel.queueDeclare(D_queue,false,false,false,null);
//将死信队列与死信交换机进行绑定
channel.queueBind(D_queue,D_EXCHANGE,"li");
//设置普通队列并创建map参数,设置最大消息长度
Map<String,Object> argument = new HashMap<>();
argument.put("x-dead-letter-exchange",D_EXCHANGE);
argument.put("x-dead-letter-routing-key","li");
String N_queue="queue";
channel.queueDeclare(N_queue,false,false,false,argument);
//绑定队列
channel.queueBind(N_queue,N_EXCHANGE,"wzs");
System.out.println("waiting receive message");
DeliverCallback deliverCallback = (consumerTag,message)->{
String mag= new String(message.getBody());
//检测拒收消息
if(mag.equals("info5")||mag.equals("info8")||mag.equals("info9")){
System.out.println("CMER01 refuse receive message is:"+mag);
//设置false不允许重新排入队列,若存在死信队列就将该消息推入死信队列
channel.basicReject(message.getEnvelope().getDeliveryTag(),false);
}else {
//正常放行消息
System.out.println("CMER01 receive message is:"+mag);
channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
}
};
boolean AutoAck=false;
channel.basicConsume(N_queue,AutoAck,deliverCallback,Tag->{});
}
}
延迟队列
什么是延迟队列?
·概念:延时队列内部是有序的,其延时属性是该队列的主要特性,延时队列中的元素时希望在指定时间到了或之前取出和处理
·存储需要在指定时间被处理的元素队列
使用场景
1、订单自动取消(用户在规定时间内为完成支付)
2、定时业务(登录提醒,店铺维护提醒,退款申请时效自动处理,预定会议)
即在某一业务的开始之前或之后的指定时间点完成另一项业务,说白了就是解决定时任务在高并发、大量数据且时效性极强的场景下带来的性能问题
订单业务逻辑图:
Rabbitmq中的TTL
什么是TTL?
·TTL是Rabbitmq中的一个消息或者队列的属性,表明一条消息或者该队列中的消息的最大存活时间
·单位:ms
·也就是说一条设置了TTL的消息或者设置了TTL属性的队列中,在规定的TTL内,这些消息为被消费,则就会成为死信;若同时配置了消息的TTL和队列的TTL属性,则以最小的TTL为工作值;
设置TTL的两种方式
//第一种设置消息的TTL
rabbitTemplate.covertAndSend(exchange,routingKey,message,
correlationData->{correlationData.getMessageProperties().setExpiration(ttlTime);
return correlationData; });
//第二种方式设置队列的TTL
args.put("x-message-ttl",5000);
return QueueBuilder.durable(QUEUE_NAME).withArgument(args).build
两种方式的区别
·队列属性设置了TTL,一旦消息过期就会被丢弃,若设置了死信队列就会被丢到该队列中
·消息TTL,即使消息过期也不会立即将消息丢弃,因为消息是否过期是在即将投递到消费者之前判定的,若当前存在消息积压,则过期消息会存放更长时间
·若未设置TTL那么消息将永远不会过期,设置为0表示除非此时消息可以直接投递到消费者处理,否则消息将被丢弃;
整合SpringBoot
队列TTL
·代码架构图
·队列配置:
QA:
·TTL 10s
·exchange 1、type :direct 2、name:XA
QB:
·TTL 40s
·exchange 1、type:direct 2、name:XB
QD(死信队列):
·exchange 1、type:direct 2、name:YD
·搭建步骤:
1、创建SpringBoot项目
2、写pom
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--RabbitMQ 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!--RabbitMQ 测试依赖-->
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
3、启动类
package com.wzs;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringBootRabbitMqApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootRabbitMqApplication.class, args);
}
}
4、写yml
spring:
rabbitmq:
host: localhost
username: guest
password: guest
port: 5672
server:
port: 8080
5、业务类
config:
swaggerconfig
package com.wzs.config;
import javafx.scene.control.OverrunStyle;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@Configuration
//开启Swagger2
@EnableSwagger2
public class SwaggerConfig {
//创建Docket
@Bean
public Docket webApiConfig(){
return new Docket(DocumentationType.SWAGGER_2).groupName("webApi")
.apiInfo(webApiInfo())
.select()
.build();
}
//配置API信息
private ApiInfo webApiInfo(){
return new ApiInfoBuilder()
.title("rabbitmq接口文档")
.description("rabbitmq微服务文档接口定义")
.version("1.0")
.contact(new Contact("enjoy6288","http://atguigu.com","2435799568.@qq.com"))
.build();
}
}
Ttlconfig
package com.wzs.config;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class TtlConfig {
private static final String XCHANGE_NAME="X";
private static final String YCHANGE_NAME="Y";
private static final String QUEUE_A="QA";
private static final String QUEUE_B="QB";
private static final String QUEUE_D="QD";
//配置普通交换机
@Bean("xchange")
public DirectExchange xChange(){
return new DirectExchange(XCHANGE_NAME);
}
//搭建死信交换机
@Bean("ychange")
public DirectExchange yChange(){
return new DirectExchange(YCHANGE_NAME);
}
//创建死信队列
@Bean("queueD")
public Queue QueueD(){
return new Queue(QUEUE_D);
}
//创建普队列
@Bean("queueA")
public Queue QueueA(){
Map<String, Object> argument = new HashMap<>(6);
argument.put("x-dead-letter-exchange",YCHANGE_NAME);
argument.put("x-dead-letter-routing-key","YD");
argument.put("x-message-ttl",10000);
return QueueBuilder.durable(QUEUE_A).withArguments(argument).build();
}
//创建普队列
@Bean("queueB")
public Queue QueueB(){
Map<String, Object> argument = new HashMap<>(6);
argument.put("x-dead-letter-exchange",YCHANGE_NAME);
argument.put("x-dead-letter-routing-key","YD");
argument.put("x-message-ttl",40000);
return QueueBuilder.durable(QUEUE_B).withArguments(argument).build();
}
//普通队列绑定交换机
@Bean
public Binding QueueABinding(@Qualifier("queueA")Queue queueA,@Qualifier("xchange")DirectExchange xchange){
return BindingBuilder.bind(queueA).to(xchange).with("XA");
}
@Bean
public Binding QueueB_Binding(@Qualifier("queueB")Queue queueB,@Qualifier("xchange")DirectExchange xchange){
return BindingBuilder.bind(queueB).to(xchange).with("XB");
}
//死信队列绑定死信交换机
@Bean
public Binding QueueDBinding(@Qualifier("queueD")Queue queueD,@Qualifier("ychange")DirectExchange ychange){
return BindingBuilder.bind(queueD).to(ychange).with("YD");
}
}
controller
package com.wzs.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
@RestController
@Slf4j
public class MessageSendController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("send/{message}")
public void SendMessage(@PathVariable("message")String message){
log.info("时间:{},发送一条消息给两个队列,消息为:{}",new Date(),message);
rabbitTemplate.convertAndSend("X","XA","消息来自ttl为10s的队列,消息体为:"+message);
rabbitTemplate.convertAndSend("X","XB","消息来自ttl为40s的队列,消息体为:"+message);
}
}
service
package com.wzs.service;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.io.UnsupportedEncodingException;
import java.util.Date;
@Component
@Slf4j
public class Consumer {
//打开Rabbitmq监听器
@RabbitListener(queues = "QD")
//死信队列接收消息
public void reciveD(Message message, Channel channel) throws UnsupportedEncodingException {
String msg= new String(message.getBody(),"utf-8");
log.info("时间:{},收到死信队列的消息为:{}",new Date(),msg);
}
}
6、测试
config配置类下的源码解析(不全):
DirectExchange类:
/**
通过源码可见该类有三种类型的构造方法,且内部是直接调用父类的构造器来完成当前类的实例化的;
1、第一种只有一个属性,交换机名
2、增加两种Boolean参数,分别是:是否持久化,是否自动删除
3、增加一个Map参数,看了源码好像没看到实际应用情况,只有两个方法处理,分别是...add(),...remove(),还有一个方法是赋属性值的,
*/
//看到默认的交换机名称是一个空字符,也就是说在创建一个交换机时要是未提供名称就会将其置为空字符
public static final DirectExchange DEFAULT = new DirectExchange("");
public DirectExchange(String name) {super(...);}
public DirectExchange(String name, boolean durable, boolean autoDelete){super(...);}
public DirectExchange(String name, boolean durable, boolean autoDelete, Map<String, Object> arguments){super(...);}
public final String getType()
QueueBuilder类:是一个final类
/**
下列队列创建过程:
1、调用QueueBuilder类的durable方法,该方法会调用SetDurable方法将该队列的durable属性的Boolean值设置为true
2、WitArgument方法会在调用该类的getOrCreateArguments().putAll(arguments)方法将map中的参数全部添加,并在build的时候设置队列属性
*/
QueueBuilder.durable(QUEUE_A).withArguments(argument).build();
BindingBuilder类:final类
/**
DestinationConfigurer 内部类
DestinationConfigurer(String name, Binding.DestinationType type)包含当前对象姓名,以及类型参数构造方法,
*/
public static DestinationConfigurer bind(Queue queue) {
return new DestinationConfigurer(queue.getName(), DestinationType.QUEUE);
}
public static DestinationConfigurer bind(Exchange exchange) {
return new DestinationConfigurer(exchange.getName(), DestinationType.EXCHANGE);
}
//与那个交换机绑定,参数:当前对象的名字,类型,交换机名,routingKey,map参数
public Binding to(FanoutExchange exchange) {
return new Binding(this.name, this.type, exchange.getName(), "", new HashMap());
}
//设置routingKey的其他保持和当前对象的一致,是static final class DirectExchangeRoutingKeyConfigurer类的方法,是BindingBuilder类的一个内部类
public Binding with(String routingKey) {
return new Binding(this.destination.name, this.destination.type, this.exchange, routingKey, Collections.emptyMap());
}
·不足:目前想要增加一个需求那么就需要重新添加一个新的队列,每加一个需求就加一个队列的成本过高
延迟队列的优化
·架构图
·配置类更改
//优化的延迟队列
@Bean("queueC")
public Queue queueC(){
Map<String,Object> argument = new HashMap<>(6);
argument.put("x-dead-letter-exchange","Y");
argument.put("x-dead-letter-routing-key","YD");
return QueueBuilder.durable(QUEUE_C).withArguments(argument).build();
}
@Bean
public Binding QueueCBinding(@Qualifier("queueC")Queue queueC,@Qualifier("xchange")DirectExchange xchange){
return BindingBuilder.bind(queueC).to(xchange).with("XC");
}
·生产者更改
@GetMapping("send/{message}/{ttl}")
public void SendMessage1(@PathVariable("message")String message,@PathVariable("ttl")String timeTtl){
log.info("时间:{},发送一条消息给两个队列,消息为:{}",new Date(),message);
rabbitTemplate.convertAndSend("X","XC","消息体为:"+message,correlationData->{
correlationData.getMessageProperties().setExpiration(timeTtl);
return correlationData;
});
}
结果:
·注意:虽然优化了队列创建问题,但是MQ只检测第一个消息是否过期,也就是说后面的消息想要被检测要等前面的消息检测完毕,不管后面消息的ttl是否比前面的长/短的情况
RabbitMQ插件实现延迟队列
·经过优化后的延迟队列解决了频繁创建队列的问题,但同时引入了新的问题,就是无法实现在消息粒度的TTL,也就是说无法使其在设置的TTL时间及时死亡,因而无法作为通用的延迟队列。
·linux下rabbitMQ插件的安装:
·下载地址:https://www.rabbitmq.com/community-plugins.html 【rabbitmq_delayed_message_exchange】
·进入安装目录的plgins目录并执行以下命令:
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
实战案例
delayConfig:
package com.wzs.delaymq;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* @author WZS
*/
@Configuration
public class DelayConfig {
private static final String DELAY_EXCHANGE_NAME="delay.exchange";
private static final String DELAY_QUEUE_NAME="delay.queue";
private static final String DELAY_QUEUE_ROUTINGKEY="delay.routingkey";
@Bean("delayqueue")
public Queue DelayQueue(){
return new Queue(DELAY_QUEUE_NAME);
}
@Bean("delayexchange")
public CustomExchange DelayExchange(){
Map<String,Object> args = new HashMap<>(3);
args.put("x-delayed-type", "direct");
return new CustomExchange(DELAY_EXCHANGE_NAME,"x-delayed-message",true,false,args);
}
@Bean
public Binding DelayBinding(@Qualifier("delayqueue") Queue delayqueue, @Qualifier("delayexchange") CustomExchange delayexchange){
return BindingBuilder.bind(delayqueue).to(delayexchange).with(DELAY_QUEUE_ROUTINGKEY).noargs();
}
}
delayProducer
@GetMapping("send/delay/{message}/{ttl}")
public void DelayMessageSend(@PathVariable("message")String message,@PathVariable("ttl")String ttl){
rabbitTemplate.convertAndSend("delay.exchange","delay.routingkey","产生的消息是:"+message,CorrelationData->{
CorrelationData.getMessageProperties().setDelay(Integer.valueOf(ttl));
return CorrelationData;
});
System.out.println("延时时间为:"+ttl+"ms消息message:"+message+"时间:"+ new Date());
}
delayConsumer
@RabbitListener(queues = "delay.queue")
public void reciveDelayMessage(Message message) throws UnsupportedEncodingException {
String msg= new String(message.getBody(),"utf-8");
log.info("时间:{},收到延迟队列的消息为:{}",new Date().toString(),msg);
}
·测试结果
总结
·延迟队列使用场景:任务需延时处理
·好处:
·消息可靠发送
·消息可靠传递
·死信队列可保证消息至少被消费一次以及未被正确处理的消息不会被丢弃
·rabbitmq的集群可以很好的解决单点故障问题,不会因为单点挂掉而导致延时队列不可用或消息丢失
·延时队列的选择:java的DelayQueue,Redis的Zset,Quartz或者Kafka的时间轮询,根据具体的业务场景选择合适的工具是开发者的必备属性之一
发布确认高级篇
·生产环境中,由于实际业务环境错综复杂,有些因素会导致rabbitmq重启,在rabbitmq重启期间,生产者生产的消息投递会失败,导致消息丢失,因而总是需要手动处理这一部分问题;那么针对这一极端情况的解决方法是否能够优化呢?
整合SpringBoot
确认机制
·架构图
·配置文件属性说明
publisher-confirm-type:NONE(default)
·NONE:禁用发布确认模式
·CORRELATED:发布消息成功到交换机后会触发回调方法
·SIMPLE
·与correlated效果一样,触发回调函数
·消息成功发布后,使用rabbitmqTemplate调用waitForConfirm或waitForConfirmOrDie方法等待broker节点返回结果,根据返回结果来判定下一步的逻辑
·区别:waitForConfirmOrDie方法的返回值若为false则会关闭channel,那么往后的消息无法发送到broker
实战案例
·yml
spring:
rabbitmq:
#新加的配置
publisher-confirm-type: correlated
·config
package com.wzs.confirm;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ConfirmConfig {
private static final String CONFIRM_EXCHANGE="confirm.exchange";
private static final String CONFIRM_QUEUE="confirm.queue";
@Bean("confirmqueue")
public Queue CongirmQueue(){
return QueueBuilder.durable(CONFIRM_QUEUE).build();
}
@Bean("confirmexchange")
public DirectExchange confirmExchange(){
return new DirectExchange(CONFIRM_EXCHANGE);
}
@Bean
public Binding QueueBinding(@Qualifier("confirmqueue")Queue queue,@Qualifier("confirmexchange") DirectExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("wzs");
}
}
·callback
package com.wzs.confirm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class ConfirmCallback implements RabbitTemplate.ConfirmCallback {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String s) {
String id=correlationData!=null?correlationData.getId():"";
if(ack){
log.info("exchange receive message id is {}",id);
}
else
{
log.info("exchange no receive message id is {},the reason is {}",id,s);
}
}
}
·consumer
package com.wzs.confirm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.io.UnsupportedEncodingException;
@Component
@Slf4j
public class ConfirmConsumer {
@RabbitListener(queues="confirm.queue")
public void RecevieMessage(Message message) throws UnsupportedEncodingException {
String msg= new String(message.getBody(),"utf-8");
log.info("receive message is "+msg);
}
}
·producer
package com.wzs.confirm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.PostConstruct;
@RestController
@Slf4j
public class ConfirmProducer {
@Autowired
RabbitTemplate rabbitTemplate;
@Autowired
ConfirmCallback confirmCallback;
//依赖注入 rabbitTemplate 之后再设置它的回调对象
@PostConstruct
public void init(){
rabbitTemplate.setConfirmCallback(confirmCallback);
}
@GetMapping("cirm/send/{message}")
public void sendMessageCirm(@PathVariable("message")String message){
CorrelationData correlationData1 = new CorrelationData("1");
rabbitTemplate.convertAndSend("confirm.exchange","wzs",message,correlationData1);
CorrelationData correlationData2 = new CorrelationData("2");
rabbitTemplate.convertAndSend("confirm.exchange","Li",message,correlationData2);
log.info("producer send message is :"+message);
}
}
·运行结果
回退消息
·Mandatory参数
·仅在开启生产者确认机制的情况下,交换机接收到消息后会直接给生产者发送确认消息,若发现消息不可路由直接被丢弃,此时的生产者是不知道消息被丢弃的
·Mandatory参数作用,简单理解就是处理那些无法路由而被直接丢弃的消息(生产者不知道消息被丢弃),通过设置该参数可以将那些不可到达目的地的消息返回给生产者
实战案例
·改动代码
/**
添加了一个回退控制类,
并在init方法中设置Mandatory参数为true
设置 rabbitTemplate.setReturnCallback方法为添加配置类的实例对象;
*/
package com.wzs.confirm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class ReplyConfig implements RabbitTemplate.ReturnCallback{
@Override
public void returnedMessage(Message message, int replyCode, String reply_reason, String exchange, String routingKey) {
log.error("消息{},被交换机{}退回,退回原因是{},路由Key是{}",new String(message.getBody()),exchange,reply_reason,routingKey);
}
}
//消息生产者
@Autowired
ReplyConfig replyConfig;
//依赖注入 rabbitTemplate 之后再设置它的回调对象
@PostConstruct
public void init(){
//设置确认回调函数
rabbitTemplate.setConfirmCallback(confirmCallback);
/**
* true:
* 交换机无法将消息进行路由时,会将该消息返回给生产者
* false:
* 如果发现消息无法进行路由,则直接丢弃
*/
rabbitTemplate.setMandatory(true);
//设置回退回调函数
rabbitTemplate.setReturnCallback(replyConfig);
}
·测试结果
备份交换机
·mandatory参数以及回退消息使得我们可以知道那条消息是无法被投递的并对其进行处理
·问题:
·当这些个被捕获的消息,我们不知道如何处理时,仅仅依靠记录日志、发送警报通知并以手动处理的方式解决时就会造成生产者变得愈加复杂,且手动粘贴日志容易出错,还需要额外添加处理被退回消息的逻辑(服务内机器数量比较庞大)
·那么怎么解决以上问题并保证消息不丢失呢?
什么是备份交换机?
·说白就是处理正常交换机无法路由时退回给生产者的消息,因为这些消息无法直接通过死信队列来接收处置,需要借助一个额外的交换机将其路由到死信队列或者备份队列中去,保证消息不会被丢失的同时很好的解决了上述问题
·备份交换机的类型一般为:Fanout;目的充分利用利用现有队列来处理这些消息(高并发场景中被回退的消息同一时刻内数量过大direct或topic不能很好的应对)
·实战案例
·修改部分
/**
CONFIG
*/
@Bean("normal.exchange")
public DirectExchange NormalExchange(){
ExchangeBuilder exchange = ExchangeBuilder.directExchange(EXCHANGE_NAME).durable(true).withArgument("alternate-exchange",BACKEXCHANGE_NAME);
return (DirectExchange) exchange.build();
}
//声明备份队列
@Bean("back.queue")
public Queue BackQueue(){
return QueueBuilder.durable(BACKQUEUE).build();
}
//声明警告队列
@Bean("back.warring_queue")
public Queue BackQueueWarring(){
return QueueBuilder.durable(BACKQUEUE_WARRING).build();
}
//创建备份交换机
@Bean("back.exchange")
public FanoutExchange BackExchange(){
return ExchangeBuilder.fanoutExchange(BACKEXCHANGE_NAME).build();
}
//绑定备份队列,备份交换机
@Bean
public Binding BackBinding(@Qualifier("back.queue")Queue queue,@Qualifier("back.exchange") FanoutExchange fanoutExchange){
return BindingBuilder.bind(queue).to(fanoutExchange);
}
//绑定备份队列,备份交换机
@Bean
public Binding BackBindingWarring(@Qualifier("back.warring_queue")Queue queueWarring,@Qualifier("back.exchange") FanoutExchange fanoutExchange){
return BindingBuilder.bind(queueWarring).to(fanoutExchange);
}
//报警消费者
public class WarringConsumer {
@RabbitListener(queues="back.warring_queue")
public void ReceiveWarringQueueMessage(Message message) throws UnsupportedEncodingException {
log.info("接警不可被路由的消息是{}",new String(message.getBody(),"utf-8"));
}
}
·测试结果
·注意当mandatory参数与备份交换机同时设置时,备份交换机的优先级是比mandatory参数更高的
RabbitMq进阶知识
幂等性
什么是幂等性?
·在同一操作中,发送的请求次数不会对结果造成影响,即不会因为点击多次而产生副作用
·案例说明:在支付场景中,用户购买商品后支付,扣款成功,但受网络影响,在钱已经扣了的情况下,用户再次点击按钮仍然继续扣费成功,此时用户同一单号的流水出现了两次扣款记录。在单应用系统中,只要把数据操作放入事务中即可,发生错误立即回滚,但仍旧会出现一些网络异常的问题。
消息重复消费
·消费重复现象:就是说在MQ把消息发送给消费者消费时,由于网络异常,迟迟没有收到消息的ACK,此时该条消息会被重新发送给其他消费者,或等到网络恢复时再次发送给这个消费者,从而造成该消息被同一个消费者或其他消费者再次消费的现象
·解决措施:MQ消费者的幂等性解决一般使用全局ID或给消息写个唯一标识【时间戳、UUID、自定义生成ID】,在消费消息时先通过该id来判断是否已被消费。
消费端幂等性的保障
·说白了就是解决业务高峰期【海量订单生成】时消息可能被多次消费问题,常用的两个保障幂等性操作:
·唯一ID+指纹码机制,利用数据库主键去重
·利用redis的原子性实现(setnx)
唯一ID+指纹码机制
·指纹码:一些规则(自定义/遵从大多数的)或时间戳+服务给到的唯一信息码,
·特点:
·不一定是系统生成的
·不许保证唯一性
·作用:可通过该ID在数据库中做排重操作
·优势:实现简单(拼接)
·劣势:高并发时,单个数据看会存在写入性能瓶颈【分库分表可提升该性能(非最佳方式)】
redis原子性:redis执行setnx命令,其天然具备幂等性,可以很好的解决重复消费问题
优先级队列
·使用场景:订单催付,在你商城系统中,我们下单后,在规定时间内未完成支付,那么系统会在截止时间之前给我们推送消息提示交易即将终止,对于平台而言,首要考虑的是谁做的好,效益大同时刻内就优先为谁提供催付服务
·MQ之前这个催付消息推送是按照redis的轮询机制来实现的,但是其不能设置优先级,也就是为商户提供的服务的机会是均等的
·MQ可以提供优先级的设施,可以为不同的商户设置不同的优先级,同时刻内平台大商户订单拥有较高的优先级,反之默认的优先级
·优先级设置方式
1、rabbitmqweb页面添加队列
2、Map参数设置队列优先级
Map<String, Object> params = new HashMap();
params.put("x-max-priority", 10);
channel.queueDeclare("hello", true, false, false, params);
3、消息设置优先级
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().priority(5).build();
队列实现优先级必须满足的三个条件:1、队列设置为优先级队列 2、消息设置优先级 3、消费者消费消息要等待消息被推送到队列之后进行
实战案例
·生产者
public static void main(String[] args) throws IOException {
Channel channel = MyChannelUtil.getChannel();
//设置队列优先级
Map<String,Object> argument = new HashMap<>();
argument.put("x-max-priority",10);
channel.queueDeclare("wzs",true,false,false,argument);
//创建消息的priority参数对象
AMQP.BasicProperties properties =
new AMQP.BasicProperties().builder().priority(10).build();
//发送消息
for (int i = 0; i < 10; i++) {
String message="info"+i;
if (i == 5) {
//给第六条消息添加一个更高的优先级
channel.basicPublish("","wzs",properties,message.getBytes(StandardCharsets.UTF_8));
}else {
channel.basicPublish("","wzs",null,message.getBytes(StandardCharsets.UTF_8));
}
System.out.println("send message:"+message);
}
}
·消费者
public static void main(String[] args) throws IOException {
Channel channel= MyChannelUtil.getChannel();
//创建普通队列map参数,目的是为了将无法消费的消息投递到死信队列
//接收消息回调函数
DeliverCallback deliverCallback = (consumerTag,message)->{
System.out.println("Consumer01 receive message is :"+ new String(message.getBody()));
};
channel.basicConsume("wzs",true,deliverCallback,tag->{});
}
·测试结果
producer:
consumer:
惰性队列
什么是惰性队列?
·消息存储在磁盘中(尽可能)
·消费者在消费到某条消息时才会将其加载到内存中
·设计目标:支持更长的队列、也就是支持更多的消息存储
·解决问题:消费者存在异常而无法及时消费消息,导致消息堆积,若堆积过多会影响MQ性能,而惰性队列可以很好的借助其他存储设备来缓解MQ的存储压力
·默认情况下:
·消息由生产者推送到RabbitMQ,队列接收到exchange推送的消息并将消息存储在内存中(尽可能),目的是为了能够更快速的将消息发送给消费者;
·消息持久化时,在写入磁盘的同时会在内存中保留一份备份;
·RabbitMQ释放内存,需要将消息换页到磁盘中,此过程十分耗时且是阻塞式的,也就是说在这个过程中队列无法再接收到新的消息
两种模式
·default(默认,RabbitMQ默认的模式)
·lazy(懒惰)
//两种设置的方法
channel.queueDeclare()方法
policy (同时设置,该方式优先级更高)
//x-queue-mode参数设置队列类型
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-queue-mode", "lazy");
channel.queueDeclare("myqueue", false, false, false, args);
内存开销对比
在发送 1 百万条消息,每条消息大概占 1KB 的情况下,普通队列占用内存是 1.2GB,而惰性队列仅仅 占用 1.5MB