RabbitMQ消息中间件
前言
本文是博主在学习RabbitMQ期间记录的笔记
视频链接
代码已上传到gitee,下载跳转
一、消息中间件是什么?
MQ(message queue)
中间件是介于应用系统和系统软件之间的一类软件,它使用系统软件所提供的基础服务(功能),衔接网络上应用系统的各个部分或不同的应用,能够达到资源共享、功能共享的目的。用一个等式来表示中间件:中间件=平台+通信,这也就限定了只有用于分布式系统中才能叫中间件,同时也把它与支撑软件和实用软件区分开来。
消息中间件是基于队列与消息传递技术,在网络环境中为应用系统提供同步或异步、可靠的消息传输的支撑性软件系统。
1.消息中间件可以解决什么问题
流量消峰
一个系统每秒支持1万个请求,超过的话,就会宕机。这种情况的话,就可以使用MQ来给一瞬间涌入的请求排个队。(宕机,指操作系统无法从一个严重系统错误中恢复过来,或系统硬件层面出问题,以致系统长时间无响应,而不得不重新启动计算机的现象。)
应用解耦
如果耦合调用多个系统,其中一个系统出问题就会造成异常。如果使用消息队列的方式后,可以让消息队列去调用。避免了异常
异步处理
A调用B执行一个任务,B需要一些时间才能完成。A需要知道B什么时候能执行完,A就会隔一会去请求B的查询api查询。有了MQ就会变得更简单,B执行完给MQ发消息,MQ再把消息发送给A。A服务还能及时的得到异步处理成功的消息。
2.MQ的选择
1.Kafka
Kafka 主要特点是基于 Pull 的模式来处理消息消费,追求高吞吐量,一开始的目的就是用于日志收集和传输,适合产生大量数据的互联网服务的数据收集业务。大型公司建议可以选用,如果有日志采集功,肯定是首选 kafka 了。
2.RocketMQ
天生为金融互联网领域而生,对于可靠性要求很高的场景,尤其是电商里面的订单扣款,以及业务削峰,在大量交易涌入时,后端可能无法及时处理的情况。RoketMQ 在稳定性上可能更值得信赖,这些业务场景在阿里双 11 已经经历了多次考验,如果你的业务有上述并发场景,建议可以选择 RocketMQ。
3.RabbitMQ
结合 erlang 语言本身的并发优势,性能好时效性微秒级,社区活跃度也比较高,管理界面用起来十分方便,如果你的数据量没有那么大,中小型公司优先选择功能比较完备的 RabbitMQ。
RabbitMQ六大模式
二、使用步骤
1.安装
RabbitMQ官网地址
在liunx系统上面安装,上传到/usr/local/software
文件下载:提取码:z0z3
文件安装命令
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
rabbitmq常用命令
添加开机启动 RabbitMQ 服务
chkconfig rabbitmq-server on
启动服务
/sbin/service rabbitmq-server start
查看服务状态
/sbin/service rabbitmq-server status
停止服务(选择执行)
/sbin/service rabbitmq-server stop
开启 web 管理插件(RabbitMQ 未启动的时候使用)
rabbitmq-plugins enable rabbitmq_management
用默认账号密码(guest)访问地址 http://47.115.185.244:15672/出现权限问题
rabbitmq创建一个新的用户
创建账号
rabbitmqctl add_user admin 123
设置用户角色
rabbitmqctl set_user_tags admin administrator
设置用户权限
set_permissions [-p <vhostpath>] <user> <conf> <write> <read>
rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"
用户 user_admin 具有/vhost1 这个 virtual host 中所有资源的配置、写、读权限当前用户和角色
当前用户和角色
rabbitmqctl list_users
关闭应用的命令为
rabbitmqctl stop_app
清除的命令为
rabbitmqctl reset
重新启动命令为
rabbitmqctl start_app
2.写一个demo
创建一个maven项目导入依赖
<!--指定 jdk 编译版本-->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
<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>
消息生产者
//发消息
public static void main(String[] args) throws Exception {
//创建一个连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("121.196.105.121"); //RabbitMQ的ip
factory.setUsername("admin"); //用户名
factory.setPassword("123"); //密码
//创建链接
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 world";
/**
* 发送一个消息
* 1.发送到那个交换机
* 2.路由的 key 是哪个
* 3.其他的参数信息
* 4.发送消息的消息体
*/
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println("消息发送完毕");
}
3.消息应答
如果有多个消费者,会按照轮询机制一个一个获取消息。
rabbitMq发送消息后,会将该消息标记为删除(自动应答)。这个时候如果处理消息的消费者挂掉的话,那这个消息就会丢失。
这个时候就需要手动应答了
A.Channel.basicAck(用于肯定确认)RabbitMQ 已知道该消息并且成功的处理消息,可以将其丢弃了
B.Channel.basicNack(用于否定确认)
C.Channel.basicReject(用于否定确认) 与 Channel.basicNack 相比少一个参数不处理该消息了直接拒绝,可以将其丢弃了
/**
* 1.消息标记 tag
* 2.是否批量应答未应答消息
*/
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
/**
* 1.消息标记 tag
* 2.是否批量拒绝未应答消息
* 3.true放入消费队列重试消费,false放入死信队列
*/
channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, true);
/**
* 1.消息标记 tag
* 2.是否批量拒绝未应答消息
*/
channel.basicReject(delivery.getEnvelope().getDeliveryTag(), false);
手动消息应答demo
生产者代码
public class Task02 {
private static final String TASK_QUEUE_NAME = "ack_queue";
public static void main(String[] argv) throws Exception {
try (Channel channel = RabbitMqUtils.getChannel()) {
channel.queueDeclare(TASK_QUEUE_NAME, false, false, false, null);
Scanner sc = new Scanner(System.in);
System.out.println("请输入信息");
while (sc.hasNext()) {
String message = sc.nextLine();
channel.basicPublish("", TASK_QUEUE_NAME, null, message.getBytes("UTF-8"));
System.out.println("生产者发出消息" + message);
}
}
}
}
消费者1
public class Work03 {
private static final String ACK_QUEUE_NAME = "ack_queue";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
System.out.println("C1 等待接收消息处理时间较短");
//消息消费的时候如何处理消息
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody());
SleepUtils.sleep(1); //睡眠1秒
System.out.println("拒绝此消息消息:" + message);
/**
* 1.消息标记 tag
* 2.是否批量拒绝未应答消息
* 3.true放入消费队列重试消费,false放入死信队列
*/
channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, true);
};
//采用手动应答
boolean autoAck = false;
channel.basicConsume(ACK_QUEUE_NAME, autoAck, deliverCallback, (consumerTag) -> {
System.out.println(consumerTag + "消费者取消消费接口回调逻辑");
});
}
}
消费者2
public class Work04 {
private static final String ACK_QUEUE_NAME = "ack_queue";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
System.out.println("C2 等待接收消息处理时间较长");
//消息消费的时候如何处理消息
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody());
SleepUtils.sleep(10); //睡眠10秒
System.out.println("接收到消息:" + message);
/**
* 1.消息标记 tag
* 2.是否批量应答未应答消息
*/
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
};
//采用手动应答
boolean autoAck = false;
channel.basicConsume(ACK_QUEUE_NAME, autoAck, deliverCallback, (consumerTag) -> {
System.out.println(consumerTag + "消费者取消消费接口回调逻辑");
});
}
}
结果:
消息持久化
要想让消息实现持久化需要在消息生产者修改代码,MessageProperties.PERSISTENT_TEXT_PLAIN 添加这个属性。
channel.basicPublish("", TASK_QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes("UTF-8"));
4.Prefetch预取值
可以设置消费者可以获取到多少个未使用的消息。如果某个消费者效率比较高,可以设置高一点。
使用预取值需要下面设置:
channel.basicQos(5); //设置预取值
autoAck = false; //设置为采用手动应答
消费者1
public class PrefetchingMessagesTest {
private static final int prefetchCount=5;
private static final String ACK_QUEUE_NAME = "ack_queue";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
channel.basicQos(prefetchCount);
System.out.println("如果出现[" + prefetchCount + "] 条消息未处理,则出现不再接收任何消息处理的情况====>");
// 开启自动确认
channel.basicConsume(ACK_QUEUE_NAME, false, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
throws IOException {
// 限定消费的条数
System.out.println("消费者2开始处理消息====>" + new String(body, "utf-8"));
System.out.println(consumerTag);
System.out.println(envelope.getDeliveryTag());
// channel.basicAck(envelope.getDeliveryTag(), false);
// 这里也不确认
}
});
}
}
消费者2
public class PrefetchingMessages2Test {
private static final int prefetchCount = 2;
private static final String ACK_QUEUE_NAME = "ack_queue";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
channel.basicQos(prefetchCount);
System.out.println("如果出现[" + prefetchCount + "] 条消息未处理,则出现不再接收任何消息处理的情况====>");
// 开启自动确认
channel.basicConsume(ACK_QUEUE_NAME, false, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
throws IOException {
System.out.println("消费者1开始处理消息====>" + new String(body, "utf-8"));
System.out.println(consumerTag);
System.out.println(envelope.getDeliveryTag());
// channel.basicAck(envelope.getDeliveryTag(), false);
// 直接不确认,分析发现qos作用
}
});
}
}
消费者3
public class PrefetchingMessages3Test {
private static final String ACK_QUEUE_NAME = "ack_queue";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
// 开启自动确认
channel.basicConsume(ACK_QUEUE_NAME, false, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
throws IOException {
// 限定消费的条数
System.out.println("消费者3开始处理消息====>" + new String(body, "utf-8"));
channel.basicAck(envelope.getDeliveryTag(), false);
}
});
}
}
生产者
public class MessageSenderApplication {
private static final String TASK_QUEUE_NAME = "ack_queue";
public static void main(String[] argv) throws Exception {
try (Channel channel = RabbitMqUtils.getChannel()) {
channel.queueDeclare(TASK_QUEUE_NAME, false, false, false, null);
for (int i = 0; i < 10; i++) {
String message = "第" + i + "条消息";
System.out.println("生产者发出消息" + message);
channel.basicPublish("", TASK_QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes("UTF-8"));
}
}
}
}
1.生产者发送十条消息
2.消费者1先消费2条消息。不回应消息(channel.basicAck(envelope.getDeliveryTag(), false);
)
3.消费者2消费5条消息,不回应消息(channel.basicAck(envelope.getDeliveryTag(), false);
)
4.消费者3正常启动
5.关闭消费者1和消费者2
预取消息其实就是消息容错限定(当出现指定数量的消息不确认就再次发送了)
5.消息确认
- 开启发布确认的方法
发布确认默认是没有开启的,如果要开启需要调用方法 confirmSelect,每当你要想使用发布
确认,都需要在 channel 上调用该方法
//开启发布确认
channel.confirmSelect();
//服务端返回 false 或超时时间内未返回,生产者可以消息重发
boolean flag = channel.waitForConfirms();
- 测试代码
public class Task03 {
private static final int MESSAGE_COUNT = 10;
public static void main(String[] argv) throws Exception {
try (Channel channel = RabbitMqUtils.getChannel()) {
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());
//服务端返回 false 或超时时间内未返回,生产者可以消息重发
boolean flag = channel.waitForConfirms();
if (flag) {
System.out.println("消息发送成功");
}
}
long end = System.currentTimeMillis();
System.out.println("发布" + MESSAGE_COUNT + "个单独确认消息,耗时" + (end - begin) +
"ms");
}
}
}
运行结果
3. 批量确认
单个确认非常慢,不如以前先发布一批消息然后一起确认可以极大地提高吞吐量。这种方式的缺点就是:当发生故障导致发布出现问题时,不知道是哪个消息出现问题了
public class Task04 {
private static final int MESSAGE_COUNT = 100;
public static void main(String[] argv) throws Exception {
try (Channel channel = RabbitMqUtils.getChannel()) {
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName, false, false, false, null);
//开启发布确认
channel.confirmSelect();
//批量确认消息大小
int batchSize = 100;
//未确认消息个数
int outstandingMessageCount = 0;
boolean bool = false;
long begin = System.currentTimeMillis();
for (int i = 0; i < MESSAGE_COUNT; i++) {
String message = i + "";
channel.basicPublish("", queueName, null, message.getBytes());
outstandingMessageCount++;
if (outstandingMessageCount == batchSize) {
bool = channel.waitForConfirms();
outstandingMessageCount = 0;
}
}
//为了确保还有剩余没有确认消息 再次确认
if (outstandingMessageCount > 0) {
channel.waitForConfirms();
}
long end = System.currentTimeMillis();
System.out.println(bool+"发布" + MESSAGE_COUNT + "个批量确认消息,耗时" + (end - begin) +
"ms");
}
}
}
- 异步确认
最佳性能和资源使用,在出现错误的情况下可以很好地控制
6.交换机
-
Exchanges概念
RabbitMQ 消息传递模型的核心思想是: 生产者生产的消息从不会直接发送到队列。实际上,通常生产者甚至都不知道这些消息传递传递到了哪些队列中。
相反,生产者只能将消息发送到交换机(exchange),交换机工作的内容非常简单,一方面它接收来自生产者的消息,另一方面将它们推入队列。交换机必须确切知道如何处理收到的消息。是应该把这些消息放到特定队列还是说把他们到许多队列中还是说应该丢弃它们。这就的由交换机的类型来决定。
-
Exchanges 的类型
直接(direct),主题(topic),标题(headers),扇出(fanout) -
无名exchanges
如果没有指定交换机,就会使用默认交互机。channel.basicPublish("", "hello", null, message.getBytes());
第一个参数是交换机的名称。空字符串表示默认或无名称交换机:消息能路由发送到队列中其实是由 routingKey(bindingkey)绑定 key 指定的,如果它存在的话 -
临时队列
临时队列断开消费者的连接,队列会被自动删除。
创建临时队列的方式如下:
String queueName = channel.queueDeclare().getQueue();
创建出来之后长成这样
-
绑定(bindings)
binding 是 exchange 和 queue 之间的桥梁,它告诉我们 exchange 和那个队列进行了绑定关系。比如说下面这张图告诉我们的就是 X 与 Q1 和 Q2 进行了绑定
-
Fanout介绍
Fanout将接收到的所有消息广播到它知道的所有队列中。系统中默认有些exchange 类型
-
Direct exchange 介绍
Fanout 这种交换类型并不能给我们带来很大的灵活性-它只能进行无意识的广播
direct 这种类型来进行替换,这种类型的工作方式是,消息只去到它绑定的routingKey 队列中去
7. 死信队列
1. 死信的概念
死信,顾名思义就是无法被消费的消息,字面意思可以这样理解,一般来说,producer 将消息投递到 broker 或者直接到 queue 里了,consumer 从 queue 取出消息进行消费,但某些时候由于特定的原因导致 queue 中的某些消息无法被消费,这样的消息如果没有后续的处理,就变成了死信,有死信自然就有了死信队列。
应用场景:为了保证订单业务的消息数据不丢失,需要使用到 RabbitMQ 的死信队列机制,当消息消费发生异常时,将消息投入死信队列中。还有比如说:用户在商城下单成功并点击去支付后在指定时间未支付时自动失效。
2. 死信的来源
- 消息 TTL 过期。
- 队列达到最大长度(队列满了,无法再添加数据到 mq 中)。
- 消息被拒绝(basic.reject 或 basic.nack)并且 消息不重新放回对垒(requeue=false)。
8. 延迟队列
1. 延迟队列概念
延时队列,队列内部是有序的,最重要的特性就体现在它的延时属性上,延时队列中的元素是希望在指定时间到了以后或之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间被处理的元素的队列。
2. 延迟队列使用场景
- 订单在十分钟之内未支付则自动取消
- 新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒。
- 用户注册成功后,如果三天内没有登陆则进行短信提醒。
- 用户发起退款,如果三天内没有得到处理则通知相关运营人员。
- 预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议
这些场景都有一个特点,需要在某个事件发生之后或者之前的指定时间点完成某一项任务,如: 发生订单生成事件,在十分钟之后检查该订单支付状态,然后将未支付的订单进行关闭;看起来似乎使用定时任务,一直轮询数据,每秒查一次,取出需要被处理的数据,然后处理不就完事了吗?如果数据量比较少,确实可以这样做,比如:对于“如果账单一周内未支付则进行自动结算”这样的需求, 如果对于时间不是严格限制,而是宽松意义上的一周,那么每天晚上跑个定时任务检查一下所有未支付的账单,确实也是一个可行的方案。但对于数据量比较大,并且时效性较强的场景,如:“订单十分钟内未支付则关闭“,短期内未支付的订单数据可能会有很多,活动期间甚至会达到百万甚至千万级别,对这么庞大的数据量仍旧使用轮询的方式显然是不可取的,很可能在一秒内无法完成所有订单的检查,同时会给数据库带来很大压力,无法满足业务要求而且性能低下。
总结
<font col加粗样式or=#999AAA >提示:这里对文章进行总结:
例如:以上就是今天要讲的内容,本文仅仅简单介绍了pandas的使用,而pandas提供了大量能使我们快速便捷地处理数据的函数和方法。