MQ

1.初识MQ1.1 同步和异步通讯1.1.1 同步通讯1.1.2 异步通讯

1.2 技术对比1.3 RabbitMQ的安装

2.RabbitMQ2.1 RabbitMQ基本结构2.2 五种消息模型2.3 动手实现消息队列(了解即可)2.3.1 publisher实现2.3.2.consumer实现

3.SpringAMQP(封装了RabbitMQ)3.1 基本消息队列(BasicQueue)3.1.1 消息发送3.1.2 消息接收

3.2 工作消息队列(WorkQueue)3.2.1 消息发送3.2.2 消息接收3.2.3 消息分配3.2.4 能者多劳3.2.5 总结

3.3 发布订阅3.3.1 广播(Fanout)3.3.1.1 消费者中声明交换机和队列3.3.1.2 消息发送3.3.1.3 消息接收3.3.1.4 问题汇总(重要)3.3.1.4.1 在rabbitMq服务运行期间, 连接、通道、交换机、队列会一直存在吗?3.3.1.4.2 交换机、队列可以持久化吗?3.3.1.4.3 在服务中新增了队列,绑定了交换机,会不会覆盖交换机原先绑定的队列?3.3.1.4.4 生产者往交换机中发消息, 但是此时没有消费者进行消费, 那消息会丢失吗?

3.3.1.5 总结

3.3.2 发布订阅 -- 路由(Direct)3.3.2.1 基于注解声明交换机和队列3.3.2.2 消息发送3.3.2.3 总结

3.3.3 发布订阅 -- 主题(Topic)3.3.3.1 说明3.3.3.2 消息发送3.3.3.3 消息接收3.3.3.4 总结

3.4 消息转换器3.4.1 测试默认转换器3.4.2 配置JSON转换器

3.5 消息确认机制(ACK)3.5.1 自动ACK存在的问题3.5.2 SpringAMQP实现手动ACK3.5.2.1 修改为手动确认

1.初识MQ

1.1 同步和异步通讯

微服务间通讯有同步和异步两种方式:

同步通讯:就像打电话,需要实时响应。 打电话可以立即得到响应,但是你却不能跟多个人同时通话。异步通讯:就像发邮件,不需要马上回复。 发送邮件可以同时与多个人收发邮件,但是往往响应会有延迟

1.1.1 同步通讯

我们之前学习的Feign调用就属于同步方式,虽然调用可以实时得到结果,但存在下面的问题:

同步调用的优点:

时效性较强,可以立即得到结果 同步调用的问题:

耦合度高性能和吞吐能力下降有额外的资源消耗有级联失败问题

1.1.2 异步通讯

异步调用则可以避免上述问题:

  我们以购买商品为例,用户支付后需要调用订单服务完成订单状态修改,调用物流服务,从仓库分配响应的库存并准备发货。

在事件模式中,支付服务是事件发布者(publisher),在支付完成后只需要发布一个支付成功的事件(event),事件中带上订单id。订单服务和物流服务是事件订阅者(Consumer),订阅支付成功的事件,监听到事件后完成自己业务即可。

  为了解除事件发布者与订阅者之间的耦合,两者并不是直接通信,而是有一个中间人(Broker)。发布者发布事件到Broker,不关心谁来订阅事件。订阅者从Broker订阅事件,不关心谁发来的消息。

好处:

吞吐量提升:无需等待订阅者处理完成,响应更快速故障隔离:服务没有直接调用,不存在级联失败问题调用间没有阻塞,不会造成无效的资源占用耦合度极低,每个服务都可以灵活插拔,可替换流量削峰:不管发布事件的流量波动多大,都由Broker接收,订阅者可以按照自己的速度去处理事件 缺点:

架构复杂了,业务没有明显的流程线,不好管理需要依赖于Broker的可靠、安全、性能

1.2 技术对比

MQ即MessageQueue(消息队列), 比较常见的MQ实现:

ActiveMQRabbitMQRocketMQKafka

几种常见MQ的对比:

RabbitMQActiveMQRocketMQKafka公司/社区RabbitApache阿里Apache开发语言ErlangJavaJavaScala&Java协议支持AMQP,XMPP,SMTP,STOMPOpenWire,STOMP,REST,XMPP,AMQP自定义协议自定义协议可用性高一般高高单机吞吐量一般差高非常高消息延迟微秒级毫秒级毫秒级毫秒以内消息可靠性高一般高一般

追求可用性:Kafka、 RocketMQ 、RabbitMQ追求可靠性:RabbitMQ、RocketMQ追求吞吐能力:RocketMQ、Kafka追求消息低延迟:RabbitMQ、Kafka

1.3 RabbitMQ的安装

安装RabbitMQ,参考课前资料:

安装成功后,访问 ip:15672, 然后输入安装时设置的密码, 就可以进入rabbitMq的首页了

2.RabbitMQ

2.1 RabbitMQ基本结构

RabbitMQ中角色

publisher:生产者consumer:消费者exchange:交换机,负责消息路由queue:队列,存储消息virtualHost:虚拟主机,隔离不同租户的exchange、queue、消息的隔离

2.2 五种消息模型

基本消息队列(BasicQueue)工作消息队列(WorkQueue)发布订阅(Publish/Subscribe),又根据交互机类型的不同分为三种

Fanout Exchange 广播Direct Exchange 路由Topic Exchange 主题

RabbitMQ提供了6种消息模型,但是第6种其实是RPC,并不是MQ,因此不予学习。那么也就剩下5种。但是其实3、4、5这三种都属于订阅模型,只不过进行路由的方式不同。

2.3 动手实现消息队列(了解即可)

官方的HelloWorld是基于最基础的消息队列模型来实现的,只包括三个角色:

publisher:消息发布者,将消息发送到队列queuequeue:消息队列,负责接受并缓存消息consumer:订阅队列,处理队列中的消息

2.3.1 publisher实现

package cn.itcast.mq.helloworld;

import com.rabbitmq.client.Channel;

import com.rabbitmq.client.Connection;

import com.rabbitmq.client.ConnectionFactory;

import org.junit.Test;

import java.io.IOException;

import java.util.concurrent.TimeoutException;

public class PublisherTest {

@Test

public void testSendMessage() throws IOException, TimeoutException {

// 1.建立连接

ConnectionFactory factory = new ConnectionFactory();

// 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码

factory.setHost("192.168.202.128");

factory.setPort(5672);

factory.setVirtualHost("/");

factory.setUsername("itcast");

factory.setPassword("123321");

// 1.2.建立连接

Connection connection = factory.newConnection();

// 2.创建通道Channel

Channel channel = connection.createChannel();

// 3.创建队列

String queueName = "simple.queue";

channel.queueDeclare(queueName, false, false, false, null);

// 4.发送消息

String message = "hello, rabbitmq!";

channel.basicPublish("", queueName, null, message.getBytes());

System.out.println("发送消息成功:【" + message + "】");

// 5.关闭通道和连接

channel.close();

connection.close();

}

}

2.3.2.consumer实现

package cn.itcast.mq.helloworld;

import com.rabbitmq.client.*;

import java.io.IOException;

import java.util.concurrent.TimeoutException;

public class ConsumerTest {

public static void main(String[] args) throws IOException, TimeoutException {

// 1.建立连接

ConnectionFactory factory = new ConnectionFactory();

// 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码

factory.setHost("192.168.202.128");

factory.setPort(5672);

factory.setVirtualHost("/");

factory.setUsername("itcast");

factory.setPassword("123321");

// 1.2.建立连接

Connection connection = factory.newConnection();

// 2.创建通道Channel

Channel channel = connection.createChannel();

// 3.创建队列

String queueName = "simple.queue";

channel.queueDeclare(queueName, false, false, false, null);

// 4.订阅消息

channel.basicConsume(queueName, true, new DefaultConsumer(channel){

@Override

public void handleDelivery(String consumerTag, Envelope envelope,

AMQP.BasicProperties properties, byte[] body) throws IOException {

// 5.处理消息

String message = new String(body);

System.out.println("接收到消息:【" + message + "】");

}

});

System.out.println("等待接收消息。。。。");

}

}

3.SpringAMQP(封装了RabbitMQ)

SpringAMQP提供了三个功能:

自动声明队列、交换机及其绑定关系基于注解的监听器模式,异步接收消息封装了RabbitTemplate工具,用于发送消息

在父工程mq-demo中引入依赖

org.springframework.boot

spring-boot-starter-amqp

3.1 基本消息队列(BasicQueue)

最基本的队列模型:一个生产者发送消息到一个队列,一个消费者从队列中取消息。

3.1.1 消息发送

一、首先配置MQ地址,在publisher服务的application.yml中添加配置:

spring:

rabbitmq:

host: 192.168.202.128 # 主机名

port: 5672 # 端口

virtual-host: / # 虚拟主机

username: itcast # 用户名

password: 123321 # 密码

二、然后在publisher服务中编写测试类SpringAmqpTest,并利用RabbitTemplate实现消息发送:

package cn.itcast.mq.spring;

import org.junit.Test;

import org.junit.runner.RunWith;

import org.springframework.amqp.rabbit.core.RabbitTemplate;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.boot.test.context.SpringBootTest;

import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)

@SpringBootTest

public class SpringAmqpTest {

@Autowired

private RabbitTemplate rabbitTemplate;

@Test

public void testSimpleQueue() {

// 队列名称

String queueName = "simple.queue";

// 消息

String message = "hello, spring amqp!";

// 发送消息

rabbitTemplate.convertAndSend(queueName, message);

}

}

3.1.2 消息接收

一、首先配置MQ地址,在consumer服务的application.yml中添加配置:

spring:

rabbitmq:

host: 192.168.202.128 # 主机名

port: 5672 # 端口

virtual-host: / # 虚拟主机

username: itcast # 用户名

password: 123321 # 密码

二、然后在consumer服务的cn.itcast.mq.listener包中新建一个类SpringRabbitListener,通过@RabbitListener指定订阅的队列

package cn.itcast.mq.listener;

import org.springframework.amqp.rabbit.annotation.RabbitListener;

import org.springframework.stereotype.Component;

@Component

public class SpringRabbitListener {

@RabbitListener(queues = "simple.queue")

public void listenSimpleQueueMessage(String msg) throws InterruptedException {

System.out.println("spring 消费者接收到消息:【" + msg + "】");

}

}

3.2 工作消息队列(WorkQueue)

思考:基本消息队列(BasicQueue)的缺点   基本消息队列是一对一(一个生产者对应一个消费者),当消息处理比较耗时的时候,可能生产消息的速度会远远大于消息的消费速度。长此以往,消息就会堆积越来越多,无法及时处理。此时就可以使用work 模型,多个消费者共同处理消息处理,速度就能大大提高了。

Work queues,也被称为(Task queues),任务模型。简单来说就是让多个消费者绑定到一个队列,共同消费队列中的消息。

3.2.1 消息发送

这次我们循环发送,模拟大量消息堆积现象。在publisher服务中的SpringAmqpTest类中添加一个测试方法:

/**

* workQueue

* 向队列中不停发送消息,模拟消息堆积。

*/

@Test

public void testWorkQueue() throws InterruptedException {

// 队列名称

String queueName = "simple.queue";

// 消息

String message = "hello, message_";

for (int i = 0; i < 20; i++) {

// 发送消息

rabbitTemplate.convertAndSend(queueName, message + i);

Thread.sleep(20);

}

}

3.2.2 消息接收

要模拟多个消费者绑定同一个队列,我们在consumer服务的SpringRabbitListener中添加2个新的方法:

其中设置消费者1sleep20毫秒、消费者2sleep200毫秒(模拟任务耗时)

@RabbitListener(queues = "simple.queue")

public void listenWorkQueue1(String msg) throws InterruptedException {

System.out.println("消费者1接收到消息:【" + msg + "】" + LocalTime.now());

Thread.sleep(20);

}

@RabbitListener(queues = "simple.queue")

public void listenWorkQueue2(String msg) throws InterruptedException {

System.err.println("消费者2........接收到消息:【" + msg + "】" + LocalTime.now());

Thread.sleep(200);

}

3.2.3 消息分配

启动ConsumerApplication后,在执行publisher服务中刚刚编写的发送测试方法testWorkQueue。

可以看到两个方法都消费了10次, 但是消费者1很快完成了自己的10条消息, 消费者2却在缓慢的处理自己的10条消息。 也就是说消息是平均分配给每个消费者,并没有考虑到消费者的处理能力。这样显然是有问题的。

3.2.4 能者多劳

在spring中有一个简单的配置,可以解决这个问题。我们修改consumer服务的application.yml文件,添加配置:

spring:

rabbitmq:

listener:

simple:

prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息

重启ConsumerApplication, 进行测试

3.2.5 总结

Work模型的使用:

多个消费者绑定到一个队列,同一条消息只会被一个消费者处理通过设置prefetch来控制消费者预取的消息数量

3.3 发布订阅

可以看到,在订阅模型中,多了一个exchange角色,而且过程略有变化:

Publisher:生产者,也就是要发送消息的程序,但是不再发送到队列中,而是发给X(交换机)Exchange:交换机。一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。Exchange有以下3种类型:

Fanout:广播,将消息交给所有绑定到交换机的队列Direct:定向,把消息交给符合指定routing key 的队列Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列 Queue:消息队列也与以前一样,接收消息、缓存消息。Consumer:消费者,与以前一样,订阅队列,没有变化

Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失!

3.3.1 广播(Fanout)

在广播模式下,消息发送流程是这样的:

可以有多个队列每个队列都要绑定到Exchange(交换机)生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定交换机把消息发送给绑定过的所有队列订阅队列的消费者都能拿到消息

我们的计划是这样的:

创建一个交换机 itcast.fanout,类型是Fanout创建两个队列fanout.queue1和fanout.queue2,绑定到交换机itcast.fanout

3.3.1.1 消费者中声明交换机和队列

消费者中声明交换机(itcast.fanout) 声明方式:在Spring中注册一个FanoutExchange(广播交换机)消费者中声明队列(fanout.queue1、fanout.queue2) 声明方式:在Spring中注册一个Queue(队列)绑定队列和交换机

代码实现 真实开发中,我们一般都事先在rabbitMq的界面中定义好交换机和队列,以及两者的绑定关系,此处我们只是模拟实现一下怎么从代码注册交换机和队列

package cn.itcast.mq.config;

import org.springframework.amqp.core.Binding;

import org.springframework.amqp.core.BindingBuilder;

import org.springframework.amqp.core.FanoutExchange;

import org.springframework.amqp.core.Queue;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

@Configuration

public class FanoutConfig {

/**

* 声明交换机

* @return Fanout类型交换机

*/

@Bean

public FanoutExchange fanoutExchange(){

return new FanoutExchange("itcast.fanout");

}

/**

* 第1个队列

*/

@Bean

public Queue fanoutQueue1(){

return new Queue("fanout.queue1");

}

/**

* 绑定队列和交换机

*/

@Bean

public Binding bindingQueue1(Queue fanoutQueue1, FanoutExchange fanoutExchange){

return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);

}

/**

* 第2个队列

*/

@Bean

public Queue fanoutQueue2(){

return new Queue("fanout.queue2");

}

/**

* 绑定队列和交换机

*/

@Bean

public Binding bindingQueue2(Queue fanoutQueue2, FanoutExchange fanoutExchange){

return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);

}

}

当我们启动服务后,在Exchanges一栏中就出现了我们自定义的队列

我们点开这个交换机后,可以看到跟他进行绑定的队列

3.3.1.2 消息发送

因为有了交换机这个中介, 所以生产者只需要将消息发给交换机即可, 交换机将消息转发给队列

@Test

public void testFanoutExchange() {

// 队列名称

String exchangeName = "itcast.fanout";

// 消息

String message = "hello, everyone!";

rabbitTemplate.convertAndSend(exchangeName, "", message);

}

此处使用的convertAndSend有三个参数

参数一:交换机名称参数二:路由密钥(Fanout时填"",Direct才需要填)参数三:消息

3.3.1.3 消息接收

在consumer服务的SpringRabbitListener中添加两个方法,作为消费者:

@RabbitListener(queues = "fanout.queue1")

public void listenFanoutQueue1(String msg) {

System.out.println("消费者1接收到Fanout消息:【" + msg + "】");

}

@RabbitListener(queues = "fanout.queue2")

public void listenFanoutQueue2(String msg) {

System.out.println("消费者2接收到Fanout消息:【" + msg + "】");

}

生产者只发了一条消息, 但是两个消费者都消费了这条信息

3.3.1.4 问题汇总(重要)

3.3.1.4.1 在rabbitMq服务运行期间, 连接、通道、交换机、队列会一直存在吗?

一、连接和通道的存在,在界面中是没有手动新增功能的, 他两的存在与否, 需要看有没有服务连接了mq服务   1.连接和通道在界面中是没有手动新增功能的, 他两的存在与否, 需要看有没有服务连接了mq服务,例如在生产者中, 我们发完消息会在代码中手动关闭连接和通道, 消息一发完, 连接和通道就消失了

  2.在消费者中, 我们一般是长期开启对Mq服务的监听, 此时连接和通道就一直存在着

  3.交换机和队列在界面中有新增功能,我们既可以在界面中创建,也可以通过连接mq时注册,在RabbitMq服务运行期间,交换机、队列、以及两种的绑定关系是一直存在的

注意:channel的数量是与queue一致的,一个通道往一个队列发送消息

3.3.1.4.2 交换机、队列可以持久化吗?

默认是无法序列化,当rabbitMq服务关闭, 交换机和队列也消失, 但是可以通过配置来实现持久化

3.3.1.4.3 在服务中新增了队列,绑定了交换机,会不会覆盖交换机原先绑定的队列?

不会,在服务中给A交换机绑定了B队列,只会为A新增一个绑定的队列,而不会覆盖原本绑定的队列, 我们如果要解绑交换机的队列,只能去界面解绑

3.3.1.4.4 生产者往交换机中发消息, 但是此时没有消费者进行消费, 那消息会丢失吗?

  交换机和队列只要定义过一次,就会永远存在于RabbitMq. 我们往交换机中发消息, 只要交换机绑定了队列, 就会把消息转发到队列中(如果没有绑定, 消息将丢失), 队列才是缓存消息的地方, 即使当下没有人消费, 消息也会一直存在于队列中, 直到被消费成功

3.3.1.5 总结

生产者只要知道Mq地址以及交换机名称(知道发给谁)消费者只要知道Mq地址以及队列名称(知道从哪儿拿)

一、交换机的作用是什么?

接收publisher发送的消息将消息按照规则路由到与之绑定的队列不能缓存消息,路由失败,消息丢失FanoutExchange的会将消息路由到每个绑定的队列

二、声明队列、交换机、绑定关系的Bean是什么?

QueueFanoutExchangeBinding

3.3.2 发布订阅 – 路由(Direct)

  在Fanout模式中,一条消息,会被所有订阅的队列都消费。但是,在某些场景下,我们希望不同的消息被不同的队列消费。这时就要用到Direct类型的Exchange。 在Direct模型下:

队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key)·消息的发送方在向Exchange发送消息时,也必须指定消息的RoutingKey。Exchange不再把消息交给每一个绑定的队列,而是根据消息的Routing Key进行判断,只有队列的Routingkey与消息的 Routing key完全一致,才会接收到消息

案例需求:

利用@RabbitListener声明Exchange、Queue、RoutingKey在consumer服务中,编写两个消费者方法,分别监听direct.queue1和direct.queue2在publisher中编写测试方法,向itcast. direct发送消息

3.3.2.1 基于注解声明交换机和队列

基于@Bean的方式声明队列和交换机比较麻烦,Spring还提供了基于注解方式来声明。 在consumer的SpringRabbitListener中添加两个消费者,同时基于注解来声明队列和交换机:

@RabbitListener(bindings = @QueueBinding(

value = @Queue(name = "direct.queue1"),

exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT),

key = {"red", "blue"}

))

public void listenDirectQueue1(String msg){

System.out.println("消费者接收到direct.queue1的消息:【" + msg + "】");

}

@RabbitListener(bindings = @QueueBinding(

value = @Queue(name = "direct.queue2"),

exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT),

key = {"red", "yellow"}

))

public void listenDirectQueue2(String msg){

System.out.println("消费者接收到direct.queue2的消息:【" + msg + "】");

}

3.3.2.2 消息发送

在publisher服务的SpringAmqpTest类中添加测试方法:

一、routingKey为red

@Test

public void testSendDirectExchange() {

// 交换机名称

String exchangeName = "itcast.direct";

// 消息

String message = "红色警报!日本乱排核废水,导致海洋生物变异,惊现哥斯拉!";

// 发送消息

rabbitTemplate.convertAndSend(exchangeName, "red", message);

}

两个消费者都能消费到

二、routingKey为yellow 只有消费者2能消费到

3.3.2.3 总结

描述下Direct交换机与Fanout交换机的差异?

Fanout交换机将消息路由给每一个与之绑定的队列Direct交换机根据RoutingKey判断路由给哪个队列如果多个队列具有相同的RoutingKey,则与Fanout功能类似

基于@RabbitListener注解声明队列和交换机有哪些常见注解?

@Queue@Exchange

3.3.3 发布订阅 – 主题(Topic)

3.3.3.1 说明

Topic类型的Exchange与Direct相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型Exchange可以让队列在绑定Routing key 的时候使用通配符!

Routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert 通配符规则:

#:匹配一个或多个词*:匹配不多不少恰好1个词

举例:

item.#:能够匹配item.spu.insert 或者 item.spuitem.*:只能匹配item.spu

解释:

Queue1:绑定的是china.# ,因此凡是以 china.开头的routing key 都会被匹配到。包括china.news和china.weatherQueue2:绑定的是#.news ,因此凡是以 .news结尾的 routing key 都会被匹配。包括china.news和japan.news

案例需求:

并利用@RabbitListener声明Exchange、Queue、RoutingKey在consumer服务中,编写两个消费者方法,分别监听topic.queue1和topic.queue2在publisher中编写测试方法,向itcast. topic发送消息

3.3.3.2 消息发送

/**

* topicExchange

*/

@Test

public void testSendTopicExchange() {

// 交换机名称

String exchangeName = "itcast.topic";

// 消息

String message = "喜报!孙悟空大战哥斯拉,胜!";

// 发送消息

rabbitTemplate.convertAndSend(exchangeName, "china.news", message);

}

3.3.3.3 消息接收

在consumer服务的SpringRabbitListener中添加方法:

@RabbitListener(bindings = @QueueBinding(

value = @Queue(name = "topic.queue1"),

exchange = @Exchange(name = "itcast.topic", type = ExchangeTypes.TOPIC),

key = "china.#"

))

public void listenTopicQueue1(String msg){

System.out.println("消费者接收到topic.queue1的消息:【" + msg + "】");

}

@RabbitListener(bindings = @QueueBinding(

value = @Queue(name = "topic.queue2"),

exchange = @Exchange(name = "itcast.topic", type = ExchangeTypes.TOPIC),

key = "#.news"

))

public void listenTopicQueue2(String msg){

System.out.println("消费者接收到topic.queue2的消息:【" + msg + "】");

}

3.3.3.4 总结

描述下Direct交换机与Topic交换机的差异?

Topic交换机接收的消息RoutingKey必须是多个单词,以 . 分割Topic交换机与队列绑定时的bindingKey可以指定通配符#:代表0个或多个词*:代表1个词

3.4 消息转换器

之前说过,Spring会把你发送的消息序列化为字节发送给MQ,接收消息的时候,还会把字节反序列化为Java对象。 只不过,默认情况下Spring采用的序列化方式是JDK序列化。众所周知,JDK序列化存在下列问题:

数据体积过大有安全漏洞可读性差

3.4.1 测试默认转换器

@Test

public void testSendMap() throws InterruptedException {

// 准备消息

Map msg = new HashMap<>();

msg.put("name", "Jack");

msg.put("age", 21);

// 发送消息

rabbitTemplate.convertAndSend("simple.queue","", msg);

}

发送消息后查看控制台:

3.4.2 配置JSON转换器

显然,JDK序列化方式并不合适。我们希望消息体的体积更小、可读性更高,因此可以使用JSON方式来做序列化和反序列化。

在publisher和consumer两个服务中都引入依赖:

com.fasterxml.jackson.dataformat

jackson-dataformat-xml

2.9.10

配置消息转换器。

在启动类中添加一个Bean即可:

@Bean

public MessageConverter jsonMessageConverter(){

return new Jackson2JsonMessageConverter();

}

3.5 消息确认机制(ACK)

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

那么问题来了:RabbitMQ怎么知道消息被接收了呢? 如果消费者领取消息后,还没执行操作就挂掉了呢?或者抛出了异常?消息消费失败,但是RabbitMQ无从得知,这样消息就丢失了!

一、ACK机制 因此,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。方法的声明:

3.5.1 自动ACK存在的问题

修改消费者,添加异常,如下: 运行消费者,程序抛出异常。但是消息依然被消费:

演示手动ACK 修改消费者,把自动改成手动(去掉之前制造的异常) 生产者不变,再次运行: 运行消费者 但是,查看管理界面,发现: 停掉消费者的程序,发现: 这是因为虽然我们设置了手动ACK,但是代码中并没有进行消息确认!所以消息并未被真正消费掉。

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

修改代码手动ACK: 执行:

3.5.2 SpringAMQP实现手动ACK

一、消息确定机制及其配置

NONE:无应答,rabbitmq默认consumer正确处理所有请求。AUTO:consumer自动应答,处理成功(注意:此处的成功确认是没有发生异常)发出ack,处理失败发出nack。rabbitmq发出消息后会等待consumer端应答,只有收到ack确定信息后才会将消息在rabbitmq清除掉。收到nack异常信息的处理方法由setDefaultRequeueReject()方法设置,这种模式下,发送错误的消息可以恢复。MANUAL:基本等同于AUTO模式,区别是需要人为调用方法确认。

二、SpringAMQP默认消息确定机制 SpringAMQP默认使用了AUTO机制,处理成功发出ack, rammitMq收到ack确定信息后会将消息删除, 处理过程中出了异常, 则发出nack, rammitMq收到nack异常信息会将发送错误的消息重新放回队列

3.5.2.1 修改为手动确认

  改为手动ACK指的是,不论是处理成功或者失败,都不会往rabbitMq发确认信息, 所以rabbitMq会将消息暂存于Unacked, 当方法结束后, 如果都没有手动发确认消息, 则消息会回到Ready

一、在配置文件中修改 acknowledge-mode: manual

spring:

profiles:

active: test

rabbitmq:

host: localhost

port: 5672

username: guest

password: guest

template:

receive-timeout: 2000

reply-timeout: 2000

listener:

simple:

concurrency: 1

max-concurrency: 3

# 消费者预取1条数据到内存,默认为250条

prefetch: 1

# 确定机制

acknowledge-mode: manual

二、代码中进行配置 可以进行配置,也可以在代码中进行配置:

@Configuration

public class RabbitConfig {

@Autowired

private CachingConnectionFactory connectionFactory;

@Bean(name = "singleListenerContainer")

public SimpleRabbitListenerContainerFactory listenerContainerFactory() {

SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();

/* setConnectionFactory:设置spring-amqp的ConnectionFactory。 */

factory.setConnectionFactory(connectionFactory);

factory.setConcurrentConsumers(1);

factory.setMaxConcurrentConsumers(10);

factory.setPrefetchCount(1);

factory.setDefaultRequeueRejected(true);

//手动确认。

factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);

return factory;

}

}

上面两种方式的效果是一样的

三、手动确认

channel.basicAck(msg.getMessageProperties().getDeliveryTag(),false); ack表示确认消息。multiple:false只确认该delivery_tag的消息,true确认该delivery_tag的所有消息channel.basicReject(msg.getMessageProperties().getDeliveryTag(),false); Reject表示拒绝消息。requeue:false表示被拒绝的消息是丢弃;true表示重回队列channel.basicNack(msg.getMessageProperties().getDeliveryTag(),false,false); ack表示拒绝消息。multiple表示拒绝指定了delivery_tag的所有未确认的消息,requeue表示不是重回队列

@Component

@Slf4j

public class CustomerRev {

//消费者处理消息缓慢

@RabbitListener(queues = {"kinson1"})

public void receiver3(Message msg, Channel channel) {

try {

//打印数据

String message = new String(msg.getBody(), StandardCharsets.UTF_8);

log.info("【开始】:{}",message);

if("0".equals(message)){

Thread.sleep(1000);

}else {

Thread.sleep(300000000L);

}

log.info("【结束】:{}", message);

//ack表示确认消息。multiple:false只确认该delivery_tag的消息,true确认该delivery_tag的所有消息

channel.basicAck(msg.getMessageProperties().getDeliveryTag(),false);

} catch (Exception e) {

log.error("错误信息:{}", e.getMessage());

}

}

}

四、消息重回队列 自动确认模式下抛出异常,和手动确认模式下nack消息都可以重回队列。

可参考:RabbitMQ消息确定机制(自动ACK和手动ACK)

推荐链接

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: