RabbitMQ学习(二 )----RabbitMQ的使用以及消息持久化

使用场景

在我们秒杀抢购商品的时候,系统会提醒我们稍等排队中,而不是像几年前一样页面卡死或报错给用户。

像这种排队结算就用到了消息队列机制,放入通道里面一个一个结算处理,而不是某个时间断突然涌入大批量的查询新增把数据库给搞宕机,所以RabbitMQ本质上起到的作用就是削峰填谷,为业务保驾护航。

为什么选择RabbitMQ

现在的市面上有很多MQ可以选择,比如ActiveMQ、ZeroMQ、Appche Qpid,那问题来了为什么要选择RabbitMQ?

  1. 除了Qpid,RabbitMQ是唯一一个实现了AMQP标准的消息服务器;
  2. 可靠性,RabbitMQ的持久化支持,保证了消息的稳定性;
  3. 高并发,RabbitMQ使用了Erlang开发语言,Erlang是为电话交换机开发的语言,天生自带高并发光环,和高可用特性;
  4. 集群部署简单,正是应为Erlang使得RabbitMQ集群部署变的超级简单;
  5. 社区活跃度高,根据网上资料来看,RabbitMQ也是首选;

工作机制

生产者、消费者和代理

在了解消息通讯之前首先要了解3个概念:生产者、消费者和代理。

生产者:消息的创建者,负责创建和推送数据到消息服务器;

消费者:消息的接收方,用于处理数据和确认消息;

代理:就是RabbitMQ本身,用于扮演“快递”的角色,本身不生产消息,只是扮演“快递”的角色。

消息发送原理

首先你必须连接到Rabbit才能发布和消费消息,那怎么连接和发送消息的呢?

你的应用程序和Rabbit Server之间会创建一个TCP连接,一旦TCP打开,并通过了认证,认证就是你试图连接Rabbit之前发送的Rabbit服务器连接信息和用户名和密码,有点像程序连接数据库,使用Java有两种连接认证的方式,后面代码会详细介绍,一旦认证通过你的应用程序和Rabbit就创建了一条AMQP信道(Channel)。

信道是创建在“真实”TCP上的虚拟连接,AMQP命令都是通过信道发送出去的,每个信道都会有一个唯一的ID,不论是发布消息,订阅队列或者介绍消息都是通过信道完成的。

为什么不通过TCP直接发送命令?

对于操作系统来说创建和销毁TCP会话是非常昂贵的开销,假设高峰期每秒有成千上万条连接,每个连接都要创建一条TCP会话,这就造成了TCP连接的巨大浪费,而且操作系统每秒能创建的TCP也是有限的,因此很快就会遇到系统瓶颈。

如果我们每个请求都使用一条TCP连接,既满足了性能的需要,又能确保每个连接的私密性,这就是引入信道概念的原因。

image

你必须知道的Rabbit

想要真正的了解Rabbit有些名词是你必须知道的。

包括:ConnectionFactory(连接管理器)、Channel(信道)、Exchange(交换器)、Queue(队列)、RoutingKey(路由键)、BindingKey(绑定键)。

ConnectionFactory(连接管理器):应用程序与Rabbit之间建立连接的管理器,程序代码中使用;

Channel(信道):消息推送使用的通道;

Exchange(交换器):用于接受、分配消息;

Queue(队列):用于存储生产者的消息;

RoutingKey(路由键):用于把生成者的数据分配到交换器上;

BindingKey(绑定键):用于把交换器的消息绑定到队列上;

看到上面的解释,最难理解的路由键和绑定键了,那么他们具体怎么发挥作用的,请看下图:

RabbitMQ学习(二 )----RabbitMQ的使用以及消息持久化_第1张图片
image

关于更多交换器的信息,我们在后面再讲。

消息持久化

Rabbit队列和交换器有一个不可告人的秘密,就是默认情况下重启服务器会导致消息丢失,那么怎么保证Rabbit在重启的时候不丢失呢?答案就是消息持久化。

当你把消息发送到Rabbit服务器的时候,你需要选择你是否要进行持久化,但这并不能保证Rabbit能从崩溃中恢复,想要Rabbit消息能恢复必须满足3个条件:

  1. 投递消息的时候durable设置为true,消息持久化,代码:channel.queueDeclare(x, true, false, false, null),参数2设置为true持久化;
  2. 设置投递模式deliveryMode设置为2(持久),代码:channel.basicPublish(x, x, MessageProperties.PERSISTENT_TEXT_PLAIN,x),参数3设置为存储纯文本到磁盘;
  3. 消息已经到达持久化交换器上;
  4. 消息已经到达持久化的队列;

持久化工作原理

Rabbit会将你的持久化消息写入磁盘上的持久化日志文件,等消息被消费之后,Rabbit会把这条消息标识为等待垃圾回收。

持久化的缺点

消息持久化的优点显而易见,但缺点也很明显,那就是性能,因为要写入硬盘要比写入内存性能较低很多,从而降低了服务器的吞吐量,尽管使用SSD硬盘可以使事情得到缓解,但他仍然吸干了Rabbit的性能,当消息成千上万条要写入磁盘的时候,性能是很低的。

所以使用者要根据自己的情况,选择适合自己的方式。

2.五种消息模型

RabbitMQ提供了6种消息模型,但是第6种其实是RPC,并不是MQ,因此不予学习。那么也就剩下5种。

但是其实3、4、5这三种都属于订阅模型,只不过进行路由的方式不同。

RabbitMQ学习(二 )----RabbitMQ的使用以及消息持久化_第2张图片

我们通过一个demo工程来了解下RabbitMQ的工作方式:

我们抽取一个建立RabbitMQ连接的工具类,方便其他程序获取连接:

public class ConnectionUtil {
 /**
 * 建立与RabbitMQ的连接
 * @return
 * @throws Exception
 */
 public static Connection getConnection() throws Exception {
 //定义连接工厂
 ConnectionFactory factory = new ConnectionFactory();
 //设置服务地址
 factory.setHost("192.168.25.128");
 //端口
 factory.setPort(5672);
 //设置账号信息,用户名、密码、vhost
 factory.setVirtualHost("/xiaozhou");
 factory.setUsername("xiaozhou");
 factory.setPassword("xiaozhou");
 // 通过工程获取连接
 Connection connection = factory.newConnection();
 return connection;
 }
}

2.1.基本消息模型

官方介绍:

RabbitMQ学习(二 )----RabbitMQ的使用以及消息持久化_第3张图片

RabbitMQ是一个消息代理:它接受和转发消息。 你可以把它想象成一个邮局:当你把邮件放在邮箱里时,你可以确定邮差先生最终会把邮件发送给你的收件人。 在这个比喻中,RabbitMQ是邮政信箱,邮局和邮递员。

RabbitMQ与邮局的主要区别是它不处理纸张,而是接受,存储和转发数据消息的二进制数据块。

RabbitMQ学习(二 )----RabbitMQ的使用以及消息持久化_第4张图片

P(producer/ publisher):生产者,一个发送消息的用户应用程序。

C(consumer):消费者,消费和接收有类似的意思,消费者是一个主要用来等待接收消息的用户应用程序

队列(红色区域):rabbitmq内部类似于邮箱的一个概念。虽然消息流经rabbitmq和你的应用程序,但是它们只能存储在队列中。队列只受主机的内存和磁盘限制,实质上是一个大的消息缓冲区。许多生产者可以发送消息到一个队列,许多消费者可以尝试从一个队列接收数据。

总之:

生产者将消息发送到队列,消费者从队列中获取消息,队列是存储消息的缓冲区。

我们将用Java编写两个程序;发送单个消息的生产者,以及接收消息并将其打印出来的消费者。我们将详细介绍Java API中的一些细节,这是一个消息传递的“Hello World”。

我们将调用我们的消息发布者(发送者)Send和我们的消息消费者(接收者)Recv。发布者将连接到RabbitMQ,发送一条消息,然后退出。


2.1.1.生产者发送消息

public class Send {
​
 private final static String QUEUE_NAME = "simple_queue";
​
 public static void main(String[] argv) throws Exception {
 // 获取到连接以及mq通道
 Connection connection = ConnectionUtil.getConnection();
 // 从连接中创建通道,这是完成大部分API的地方。
 Channel channel = connection.createChannel();
​
 // 声明(创建)队列,必须声明队列才能够发送消息,我们可以把消息发送到队列中。
 // 声明一个队列是幂等的 - 只有当它不存在时才会被创建
 channel.queueDeclare(QUEUE_NAME, false, false, false, null);
​
 // 消息内容
 String message = "Hello World!";
 channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
 System.out.println(" [x] Sent '" + message + "'");
​
 //关闭通道和连接
 channel.close();
 connection.close();
 }
}

控制台:

RabbitMQ学习(二 )----RabbitMQ的使用以及消息持久化_第5张图片

2.1.2.管理工具中查看消息

进入队列页面,可以看到新建了一个队列:simple_queue

RabbitMQ学习(二 )----RabbitMQ的使用以及消息持久化_第6张图片

点击队列名称,进入详情页,可以查看消息:

RabbitMQ学习(二 )----RabbitMQ的使用以及消息持久化_第7张图片

在控制台查看消息并不会将消息消费,所以消息还在。

2.1.3.消费者获取消息

public class Recv {
 private final static String QUEUE_NAME = "simple_queue";
​
 public static void main(String[] argv) throws Exception {
 // 获取到连接
 Connection connection = ConnectionUtil.getConnection();
 // 创建通道
 Channel channel = connection.createChannel();
 // 声明队列
 channel.queueDeclare(QUEUE_NAME, false, false, false, null);
 // 定义队列的消费者
 DefaultConsumer consumer = new DefaultConsumer(channel) {
 // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
 @Override
 public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
 byte[] body) throws IOException {
 // body 即消息体
 String msg = new String(body);
 System.out.println(" [x] received : " + msg + "!");
 }
 };
 // 监听队列,第二个参数:是否自动进行消息确认。
 channel.basicConsume(QUEUE_NAME, true, consumer);
 }
}

控制台:

RabbitMQ学习(二 )----RabbitMQ的使用以及消息持久化_第8张图片

这个时候,队列中的消息就没了:

RabbitMQ学习(二 )----RabbitMQ的使用以及消息持久化_第9张图片

我们发现,消费者已经获取了消息,但是程序没有停止,一直在监听队列中是否有新的消息。一旦有新的消息进入队列,就会立即打印.


2.1.4.消息确认机制(ACK)

通过刚才的案例可以看出,消息一旦被消费者接收,队列中的消息就会被删除。

那么问题来了:RabbitMQ怎么知道消息被接收了呢?

如果消费者领取消息后,还没执行操作就挂掉了呢?或者抛出了异常?消息消费失败,但是RabbitMQ无从得知,这样消息就丢失了!

因此,RabbitMQ有一个ACK机制。当消费者获取消息后,会向RabbitMQ发送回执ACK,告知消息已经被接收。不过这种回执ACK分两种情况:

  • 自动ACK:消息一旦被接收,消费者自动发送ACK

  • 手动ACK:消息接收后,不会发送ACK,需要手动调用

大家觉得哪种更好呢?

这需要看消息的重要性:

  • 如果消息不太重要,丢失也没有影响,那么自动ACK会比较方便

  • 如果消息非常重要,不容丢失。那么最好在消费完成后手动ACK,否则接收消息后就自动ACK,RabbitMQ就会把消息从队列中删除。如果此时消费者宕机,那么消息就丢失了。

我们之前的测试都是自动ACK的,如果要手动ACK,需要改动我们的代码:

public class Recv2 {
 private final static String QUEUE_NAME = "simple_queue";
​
 public static void main(String[] argv) throws Exception {
 // 获取到连接
 Connection connection = ConnectionUtil.getConnection();
 // 创建通道
 final Channel channel = connection.createChannel();
 // 声明队列
 channel.queueDeclare(QUEUE_NAME, false, false, false, null);
 // 定义队列的消费者
 DefaultConsumer consumer = new DefaultConsumer(channel) {
 // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
 @Override
 public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
 byte[] body) throws IOException {
 // body 即消息体
 String msg = new String(body);
 System.out.println(" [x] received : " + msg + "!");
 // 手动进行ACK
 channel.basicAck(envelope.getDeliveryTag(), false);
 }
 };
 // 监听队列,第二个参数false,手动进行ACK
 channel.basicConsume(QUEUE_NAME, false, consumer);
 }
}

注意到最后一行代码:

channel.basicConsume(QUEUE_NAME, false, consumer);

如果第二个参数为true,则会自动进行ACK;如果为false,则需要手动ACK。方法的声明:

RabbitMQ学习(二 )----RabbitMQ的使用以及消息持久化_第10张图片

2.1.4.1.自动ACK存在的问题

修改消费者,添加异常,如下:

RabbitMQ学习(二 )----RabbitMQ的使用以及消息持久化_第11张图片

生产者不做任何修改,直接运行,消息发送成功:

RabbitMQ学习(二 )----RabbitMQ的使用以及消息持久化_第12张图片

运行消费者,程序抛出异常。但是消息依然被消费:

RabbitMQ学习(二 )----RabbitMQ的使用以及消息持久化_第13张图片

管理界面:

RabbitMQ学习(二 )----RabbitMQ的使用以及消息持久化_第14张图片

2.1.4.2.演示手动ACK

修改消费者,把自动改成手动(去掉之前制造的异常)

RabbitMQ学习(二 )----RabbitMQ的使用以及消息持久化_第15张图片

生产者不变,再次运行:

RabbitMQ学习(二 )----RabbitMQ的使用以及消息持久化_第16张图片

运行消费者

RabbitMQ学习(二 )----RabbitMQ的使用以及消息持久化_第17张图片

但是,查看管理界面,发现:

RabbitMQ学习(二 )----RabbitMQ的使用以及消息持久化_第18张图片

停掉消费者的程序,发现:

RabbitMQ学习(二 )----RabbitMQ的使用以及消息持久化_第19张图片

这是因为虽然我们设置了手动ACK,但是代码中并没有进行消息确认!所以消息并未被真正消费掉。

当我们关掉这个消费者,消息的状态再次称为Ready

修改代码手动ACK:

RabbitMQ学习(二 )----RabbitMQ的使用以及消息持久化_第20张图片

执行:

RabbitMQ学习(二 )----RabbitMQ的使用以及消息持久化_第21张图片

消息消费成功!


Spring AMQP

依赖和配置

添加AMQP的启动器:


org.springframework.boot
spring-boot-starter-amqp

application.yml中添加RabbitMQ地址:

spring:
rabbitmq:
host: 192.168.56.101
username: leyou
password: leyou
virtual-host: /leyou


监听者

在SpringAmqp中,对消息的消费者进行了封装和抽象,一个普通的JavaBean中的普通方法,只要通过简单的注解,就可以成为一个消费者。

@Component
public class Listener {
​
 @RabbitListener(bindings = @QueueBinding(
 value = @Queue(value = "spring.test.queue", durable = "true"),
 exchange = @Exchange(
 value = "spring.test.exchange",
 ignoreDeclarationExceptions = "true",
 type = ExchangeTypes.TOPIC
 ),
 key = {"#.#"}))
 public void listen(String msg){
 System.out.println("接收到消息:" + msg);
 }
}
  • @Componet:类上的注解,注册到Spring容器

  • @RabbitListener:方法上的注解,声明这个方法是一个消费者方法,需要指定下面的属性:

    • bindings:指定绑定关系,可以有多个。值是@QueueBinding的数组。@QueueBinding包含下面属性:

      • value:这个消费者关联的队列。值是@Queue,代表一个队列

      • exchange:队列所绑定的交换机,值是@Exchange类型

      • key:队列和交换机绑定的RoutingKey

类似listen这样的方法在一个类中可以写多个,就代表多个消费者。

AmqpTemplate

Spring最擅长的事情就是封装,把他人的框架进行封装和整合。

Spring为AMQP提供了统一的消息处理模板:AmqpTemplate,非常方便的发送消息,其发送方法:

RabbitMQ学习(二 )----RabbitMQ的使用以及消息持久化_第22张图片

红框圈起来的是比较常用的3个方法,分别是:

  • 指定交换机、RoutingKey和消息体

  • 指定消息

  • 指定RoutingKey和消息,会向默认的交换机发送消息

3.5.测试代码

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class MqDemo {
​
 @Autowired
 private AmqpTemplate amqpTemplate;
​
 @Test
 public void testSend() throws InterruptedException {
 String msg = "hello, Spring boot amqp";
 this.amqpTemplate.convertAndSend("spring.test.exchange","a.b", msg);
 // 等待10秒后再结束
 Thread.sleep(10000);
 }
}

运行后查看日志:

RabbitMQ学习(二 )----RabbitMQ的使用以及消息持久化_第23张图片

引用(本文章只供本人学习以及学习的记录,如有侵权,请联系我删除)

RabbitMQ系列(二)深入了解RabbitMQ工作原理及简单使用

你可能感兴趣的:(RabbitMQ学习(二 )----RabbitMQ的使用以及消息持久化)