前面总结了一些rabbitmq相关的知识点,现在就自己动手试一试,主要从交换机(Exchange)的四种模式入手。
1、首先在pom中加入rabbitmq的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2、application.yml 加入rabbitmq的相关配置
#rabbitmq 配置
spring:
rabbitmq:
host: 192.168.183.128
port: 5672
username: admin
password: 123456
#虚拟主机
virtual-host: /
listener:
simple:
#手动ACK
acknowledge-mode: manual
3、队列配置
/**
* 创建消息队列
* @Configuration 启动容器+@Bean注册Bean,@Bean下管理bean的生命周期
* @Bean 标注在方法上(返回某个实例的方法),等价于spring的xml配置文件中的<bean>,作用为:注册bean对象
*/
@Configuration
public class RabbitConfig {
public static final String DEFAULT_BOOK_QUEUE = "dev.book.register.default.queue";
public static final String MANUAL_BOOK_QUEUE = "dev.book.register.manual.queue";
@Bean
public Queue defaultBookQueue() {
// 第一个是 QUEUE 的名字,第二个是消息是否需要持久化处理
return new Queue(DEFAULT_BOOK_QUEUE, true);
}
@Bean
public Queue manualBookQueue() {
// 第一个是 QUEUE 的名字,第二个是消息是否需要持久化处理
return new Queue(MANUAL_BOOK_QUEUE, true);
}
}
4、发送者
import org.springframework.amqp.rabbit.core.RabbitTemplate;
@Autowired
private RabbitTemplate rabbitTemplate; //rabbitTemplate是springboot 提供的默认实现
@RequestMapping(value="/rabbit")
@ResponseBody
public void defaultMessage() {
for (int i = 0; i < 20; i++) {
rabbitTemplate.convertAndSend(RabbitConfig.DEFAULT_BOOK_QUEUE, "生产者一 " + i);
}
}
5、接收者
import com.rabbitmq.client.Channel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* BOOK_QUEUE 消费者
*/
@Component
public class BookHandler {
private static final Logger logger = LoggerFactory.getLogger(BookHandler.class);
/**
* <p>TODO 该方案是 spring-boot-data-amqp 默认的方式,不太推荐。具体推荐使用 listenerManualAck()</p>
* 默认情况下,如果没有配置手动ACK, 那么Spring Data AMQP 会在消息消费完毕后自动帮我们去ACK
* 存在问题:如果报错了,消息不会丢失,但是会无限循环消费,一直报错,如果开启了错误日志很容易就吧磁盘空间耗完
* 解决方案:手动ACK,或者try-catch 然后在 catch 里面讲错误的消息转移到其它的系列中去
* spring.rabbitmq.listener.simple.acknowledge-mode=manual
* <p>
*
* @param text 监听的内容
* deliveryTag:该消息的index
* multiple:是否批量.true:将一次性ack所有小于deliveryTag的消息。
*/
@RabbitListener(queues = {RabbitConfig.DEFAULT_BOOK_QUEUE})
public void listenerAutoAck(String text, Message message, Channel channel) {
// TODO 如果手动ACK,消息会被监听消费,但是消息在队列中依旧存在,如果 未配置 acknowledge-mode 默认是会在消费完毕后自动ACK掉
final long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
logger.info("[消费者一监听的消息] - [{}]", text);
// TODO 通知 MQ 消息已被成功消费,可以ACK了
channel.basicAck(deliveryTag, false);
} catch (IOException e) {
try {
// TODO 处理失败,重新压入MQ
channel.basicRecover();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
@RabbitListener(queues = {RabbitConfig.DEFAULT_BOOK_QUEUE})
public void listenerManualAck(String text, Message message, Channel channel) {
logger.info("[消费者二监听的消息] - [{}]",text);
try {
// TODO 通知 MQ 消息已被成功消费,可以ACK了
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (IOException e) {
// TODO 如果报错了,那么我们可以进行容错处理,比如转移当前消息进入其它队列
}
}
}
6、启动项目,发送消息
2018-09-07 11:01:19.490 INFO com.test.rabbitmq.BookHandler 39 listenerAutoAck - [消费者一监听的消息] - [生产者一 0]
2018-09-07 11:01:19.491 INFO com.test.rabbitmq.BookHandler 39 listenerAutoAck - [消费者一监听的消息] - [生产者一 2]
2018-09-07 11:01:19.491 INFO com.test.rabbitmq.BookHandler 39 listenerAutoAck - [消费者一监听的消息] - [生产者一 4]
2018-09-07 11:01:19.492 INFO com.test.rabbitmq.BookHandler 39 listenerAutoAck - [消费者一监听的消息] - [生产者一 6]
2018-09-07 11:01:19.492 INFO com.test.rabbitmq.BookHandler 39 listenerAutoAck - [消费者一监听的消息] - [生产者一 8]
2018-09-07 11:01:19.492 INFO com.test.rabbitmq.BookHandler 39 listenerAutoAck - [消费者一监听的消息] - [生产者一 10]
2018-09-07 11:01:19.493 INFO com.test.rabbitmq.BookHandler 39 listenerAutoAck - [消费者一监听的消息] - [生产者一 12]
2018-09-07 11:01:19.493 INFO com.test.rabbitmq.BookHandler 39 listenerAutoAck - [消费者一监听的消息] - [生产者一 14]
2018-09-07 11:01:19.495 INFO com.test.rabbitmq.BookHandler 54 listenerManualAck - [消费者二监听的消息] - [生产者一 1]
2018-09-07 11:01:19.496 INFO com.test.rabbitmq.BookHandler 54 listenerManualAck - [消费者二监听的消息] - [生产者一 3]
2018-09-07 11:01:19.496 INFO com.test.rabbitmq.BookHandler 54 listenerManualAck - [消费者二监听的消息] - [生产者一 5]
2018-09-07 11:01:19.496 INFO com.test.rabbitmq.BookHandler 39 listenerAutoAck - [消费者一监听的消息] - [生产者一 16]
2018-09-07 11:01:19.497 INFO com.test.rabbitmq.BookHandler 54 listenerManualAck - [消费者二监听的消息] - [生产者一 7]
2018-09-07 11:01:19.497 INFO com.test.rabbitmq.BookHandler 54 listenerManualAck - [消费者二监听的消息] - [生产者一 9]
2018-09-07 11:01:19.497 INFO com.test.rabbitmq.BookHandler 54 listenerManualAck - [消费者二监听的消息] - [生产者一 11]
2018-09-07 11:01:19.498 INFO com.test.rabbitmq.BookHandler 54 listenerManualAck - [消费者二监听的消息] - [生产者一 13]
2018-09-07 11:01:19.498 INFO com.test.rabbitmq.BookHandler 39 listenerAutoAck - [消费者一监听的消息] - [生产者一 18]
2018-09-07 11:01:19.498 INFO com.test.rabbitmq.BookHandler 54 listenerManualAck - [消费者二监听的消息] - [生产者一 15]
2018-09-07 11:01:19.498 INFO com.test.rabbitmq.BookHandler 54 listenerManualAck - [消费者二监听的消息] - [生产者一 17]
2018-09-07 11:01:19.499 INFO com.test.rabbitmq.BookHandler 54 listenerManualAck - [消费者二监听的消息] - [生产者一 19]
我们看到我们一共往队列里放了20 条消息,两个消费者是平均消费的也就是每个消费者10 条,也就是一对多的情况,在多对多的情况下应该也是平均消费的
7、交换机(Exchange)
Topic Exchange:
创建队列,交换机,将队列绑定到交换机上并配置路由模式
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 创建消息队列
* @Configuration 启动容器+@Bean注册Bean,@Bean下管理bean的生命周期
* @Bean 标注在方法上(返回某个实例的方法),等价于spring的xml配置文件中的<bean>,作用为:注册bean对象
* *** bean name为方法名 ****
*/
@Configuration
public class TopicRabbitConfig {
public static final String DEFAULT_BOOK_QUEUE = "dev.book.topic.default.queue";
public static final String MANUAL_BOOK_QUEUE = "dev.book.topic.manual.queue";
@Bean
public Queue queueMessage() {
// 第一个是 QUEUE 的名字,第二个是消息是否需要持久化处理
return new Queue(DEFAULT_BOOK_QUEUE, true);
}
@Bean
public Queue queueMessages() {
// 第一个是 QUEUE 的名字,第二个是消息是否需要持久化处理
return new Queue(MANUAL_BOOK_QUEUE, true);
}
@Bean
TopicExchange exchange() {
return new TopicExchange("exchange");
}
@Bean
Binding bindingExchangeMessage(Queue queueMessage, TopicExchange exchange) {
return BindingBuilder.bind(queueMessage).to(exchange).with("dev.book.topic.default.queue");
}
@Bean
Binding bindingExchangeMessages(Queue queueMessages, TopicExchange exchange) {
return BindingBuilder.bind(queueMessages).to(exchange).with("dev.book.topic.#");
}
}
创建两个队列消息的消费者,每个队列一个消费者方便测试
import com.rabbitmq.client.Channel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* BOOK_QUEUE 消费者
*/
@Component
public class BookHandler {
private static final Logger logger = LoggerFactory.getLogger(BookHandler.class);
/**
* <p>TODO 该方案是 spring-boot-data-amqp 默认的方式,不太推荐。具体推荐使用 listenerManualAck()</p>
* 默认情况下,如果没有配置手动ACK, 那么Spring Data AMQP 会在消息消费完毕后自动帮我们去ACK
* 存在问题:如果报错了,消息不会丢失,但是会无限循环消费,一直报错,如果开启了错误日志很容易就吧磁盘空间耗完
* 解决方案:手动ACK,或者try-catch 然后在 catch 里面讲错误的消息转移到其它的系列中去
* spring.rabbitmq.listener.simple.acknowledge-mode=manual
* <p>
*
* @param text 监听的内容
* deliveryTag:该消息的index
* multiple:是否批量.true:将一次性ack所有小于deliveryTag的消息。
*/
@RabbitListener(queues = {TopicRabbitConfig.DEFAULT_BOOK_QUEUE})
public void listenerAutoAck(String text, Message message, Channel channel) {
// TODO 如果手动ACK,消息会被监听消费,但是消息在队列中依旧存在,如果 未配置 acknowledge-mode 默认是会在消费完毕后自动ACK掉
final long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
logger.info("[消费者一监听的消息] - [{}]", text);
// TODO 通知 MQ 消息已被成功消费,可以ACK了
channel.basicAck(deliveryTag, false);
} catch (IOException e) {
try {
// TODO 处理失败,重新压入MQ
channel.basicRecover();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
@RabbitListener(queues = {TopicRabbitConfig.MANUAL_BOOK_QUEUE})
public void listenerManualAck(String text, Message message, Channel channel) {
logger.info("[消费者二监听的消息] - [{}]",text);
try {
// TODO 通知 MQ 消息已被成功消费,可以ACK了
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (IOException e) {
// TODO 如果报错了,那么我们可以进行容错处理,比如转移当前消息进入其它队列
}
}
}
接下来进行测试
//测试1
@RequestMapping(value="/rabbit")
@ResponseBody
public void defaultMessage() {
for (int i = 0; i < 10; i++) {
rabbitTemplate.convertAndSend("exchange","dev.book.topic.default.queue", "生产者一 " + i);
}
}
<!--进行访问我们看到如下的结果,两个消费者收到了同样的消息,说明消息发送到了两个队列中,
因为我们发送消息的“routingKey” 可以匹配到两个队列绑定的“routingKey”中,具体的配置规则可以参考上一篇的介绍-->
2018-09-08 14:27:03.596 INFO com.test.rabbitmq.BookHandler 39 listenerAutoAck - [消费者一监听的消息] - [生产者一 0]
2018-09-08 14:27:03.597 INFO com.test.rabbitmq.BookHandler 54 listenerManualAck - [消费者二监听的消息] - [生产者一 0]
2018-09-08 14:27:03.597 INFO com.test.rabbitmq.BookHandler 54 listenerManualAck - [消费者二监听的消息] - [生产者一 1]
2018-09-08 14:27:03.598 INFO com.test.rabbitmq.BookHandler 54 listenerManualAck - [消费者二监听的消息] - [生产者一 2]
2018-09-08 14:27:03.598 INFO com.test.rabbitmq.BookHandler 39 listenerAutoAck - [消费者一监听的消息] - [生产者一 1]
2018-09-08 14:27:03.598 INFO com.test.rabbitmq.BookHandler 54 listenerManualAck - [消费者二监听的消息] - [生产者一 3]
2018-09-08 14:27:03.598 INFO com.test.rabbitmq.BookHandler 39 listenerAutoAck - [消费者一监听的消息] - [生产者一 2]
2018-09-08 14:27:03.598 INFO com.test.rabbitmq.BookHandler 39 listenerAutoAck - [消费者一监听的消息] - [生产者一 3]
2018-09-08 14:27:03.598 INFO com.test.rabbitmq.BookHandler 54 listenerManualAck - [消费者二监听的消息] - [生产者一 4]
2018-09-08 14:27:03.599 INFO com.test.rabbitmq.BookHandler 39 listenerAutoAck - [消费者一监听的消息] - [生产者一 4]
2018-09-08 14:27:03.599 INFO com.test.rabbitmq.BookHandler 54 listenerManualAck - [消费者二监听的消息] - [生产者一 5]
2018-09-08 14:27:03.599 INFO com.test.rabbitmq.BookHandler 39 listenerAutoAck - [消费者一监听的消息] - [生产者一 5]
2018-09-08 14:27:03.601 INFO com.test.rabbitmq.BookHandler 39 listenerAutoAck - [消费者一监听的消息] - [生产者一 6]
2018-09-08 14:27:03.602 INFO com.test.rabbitmq.BookHandler 39 listenerAutoAck - [消费者一监听的消息] - [生产者一 7]
2018-09-08 14:27:03.603 INFO com.test.rabbitmq.BookHandler 54 listenerManualAck - [消费者二监听的消息] - [生产者一 6]
2018-09-08 14:27:03.604 INFO com.test.rabbitmq.BookHandler 54 listenerManualAck - [消费者二监听的消息] - [生产者一 7]
2018-09-08 14:27:03.605 INFO com.test.rabbitmq.BookHandler 39 listenerAutoAck - [消费者一监听的消息] - [生产者一 8]
2018-09-08 14:27:03.605 INFO com.test.rabbitmq.BookHandler 54 listenerManualAck - [消费者二监听的消息] - [生产者一 8]
2018-09-08 14:27:03.606 INFO com.test.rabbitmq.BookHandler 39 listenerAutoAck - [消费者一监听的消息] - [生产者一 9]
2018-09-08 14:27:03.606 INFO com.test.rabbitmq.BookHandler 54 listenerManualAck - [消费者二监听的消息] - [生产者一 9]
//测试2
@RequestMapping(value="/rabbit2")
@ResponseBody
public void defaultMessage2() {
for (int i = 0; i < 10; i++) {
rabbitTemplate.convertAndSend("exchange","dev.book.topic.manual.queue", "生产者一 " + i);
}
}
<!--我们看到只有消费者二接收到了消息,也就是消息只发送到了队列“dev.book.topic.manual.queue” 中,
因为它只匹配到了队列绑定的“dev.book.topic.#”这个“routing”中-->
2018-09-08 14:34:28.997 INFO com.test.rabbitmq.BookHandler 54 listenerManualAck - [消费者二监听的消息] - [生产者一 0]
2018-09-08 14:34:28.999 INFO com.test.rabbitmq.BookHandler 54 listenerManualAck - [消费者二监听的消息] - [生产者一 1]
2018-09-08 14:34:28.999 INFO com.test.rabbitmq.BookHandler 54 listenerManualAck - [消费者二监听的消息] - [生产者一 2]
2018-09-08 14:34:29.000 INFO com.test.rabbitmq.BookHandler 54 listenerManualAck - [消费者二监听的消息] - [生产者一 3]
2018-09-08 14:34:29.000 INFO com.test.rabbitmq.BookHandler 54 listenerManualAck - [消费者二监听的消息] - [生产者一 4]
2018-09-08 14:34:29.003 INFO com.test.rabbitmq.BookHandler 54 listenerManualAck - [消费者二监听的消息] - [生产者一 5]
2018-09-08 14:34:29.004 INFO com.test.rabbitmq.BookHandler 54 listenerManualAck - [消费者二监听的消息] - [生产者一 6]
2018-09-08 14:34:29.026 INFO com.test.rabbitmq.BookHandler 54 listenerManualAck - [消费者二监听的消息] - [生产者一 7]
2018-09-08 14:34:29.031 INFO com.test.rabbitmq.BookHandler 54 listenerManualAck - [消费者二监听的消息] - [生产者一 8]
2018-09-08 14:34:29.032 INFO com.test.rabbitmq.BookHandler 54 listenerManualAck - [消费者二监听的消息] - [生产者一 9]
Fanout Exchange(订阅模式):
直接贴一下主要代码和运行结果
创建消息队列
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 创建消息队列
* @Configuration 启动容器+@Bean注册Bean,@Bean下管理bean的生命周期
* @Bean 标注在方法上(返回某个实例的方法),等价于spring的xml配置文件中的<bean>,作用为:注册bean对象
* *** bean name为方法名 ****
*/
@Configuration
public class FanoutRabbitConfig {
public static final String DEFAULT_BOOK_QUEUE = "dev.book.fanout.a.queue";
public static final String MANUAL_BOOK_QUEUE = "dev.book.fanout.b.queue";
@Bean
public Queue queueMessageA() {
// 第一个是 QUEUE 的名字,第二个是消息是否需要持久化处理
return new Queue(DEFAULT_BOOK_QUEUE, true);
}
@Bean
public Queue queueMessageB() {
// 第一个是 QUEUE 的名字,第二个是消息是否需要持久化处理
return new Queue(MANUAL_BOOK_QUEUE, true);
}
@Bean
FanoutExchange fanoutExchange() {
return new FanoutExchange("fanoutExchange");
}
@Bean
Binding bindingExchangeMessage(Queue queueMessageA, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(queueMessageA).to(fanoutExchange);
}
@Bean
Binding bindingExchangeMessages(Queue queueMessageB, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(queueMessageB).to(fanoutExchange);
}
}
消费者
import com.rabbitmq.client.Channel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* BOOK_QUEUE 消费者
*/
@Component
public class BookHandler {
private static final Logger logger = LoggerFactory.getLogger(BookHandler.class);
/**
* <p>TODO 该方案是 spring-boot-data-amqp 默认的方式,不太推荐。具体推荐使用 listenerManualAck()</p>
* 默认情况下,如果没有配置手动ACK, 那么Spring Data AMQP 会在消息消费完毕后自动帮我们去ACK
* 存在问题:如果报错了,消息不会丢失,但是会无限循环消费,一直报错,如果开启了错误日志很容易就吧磁盘空间耗完
* 解决方案:手动ACK,或者try-catch 然后在 catch 里面讲错误的消息转移到其它的系列中去
* spring.rabbitmq.listener.simple.acknowledge-mode=manual
* <p>
*
* @param text 监听的内容
* deliveryTag:该消息的index
* multiple:是否批量.true:将一次性ack所有小于deliveryTag的消息。
*/
@RabbitListener(queues = {FanoutRabbitConfig.DEFAULT_BOOK_QUEUE})
public void listenerAutoAck(String text, Message message, Channel channel) {
// TODO 如果手动ACK,消息会被监听消费,但是消息在队列中依旧存在,如果 未配置 acknowledge-mode 默认是会在消费完毕后自动ACK掉
final long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
logger.info("[消费者一监听的消息] - [{}]", text);
// TODO 通知 MQ 消息已被成功消费,可以ACK了
channel.basicAck(deliveryTag, false);
} catch (IOException e) {
try {
// TODO 处理失败,重新压入MQ
channel.basicRecover();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
@RabbitListener(queues = {FanoutRabbitConfig.MANUAL_BOOK_QUEUE})
public void listenerManualAck(String text, Message message, Channel channel) {
logger.info("[消费者二监听的消息] - [{}]",text);
try {
// TODO 通知 MQ 消息已被成功消费,可以ACK了
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (IOException e) {
// TODO 如果报错了,那么我们可以进行容错处理,比如转移当前消息进入其它队列
}
}
}
测试
@RequestMapping(value="/rabbit3")
@ResponseBody
public void fanoutMessage3() {
for (int i = 0; i < 5; i++) {
rabbitTemplate.convertAndSend("fanoutExchange","", "生产者一 " + i);
}
}
<!-- 运行结果,两个队列都发送了消息-->
2018-09-08 15:06:37.446 INFO com.test.rabbitmq.BookHandler 39 listenerAutoAck - [消费者一监听的消息] - [生产者一 0]
2018-09-08 15:06:37.448 INFO com.test.rabbitmq.BookHandler 39 listenerAutoAck - [消费者一监听的消息] - [生产者一 1]
2018-09-08 15:06:37.449 INFO com.test.rabbitmq.BookHandler 39 listenerAutoAck - [消费者一监听的消息] - [生产者一 2]
2018-09-08 15:06:37.451 INFO com.test.rabbitmq.BookHandler 54 listenerManualAck - [消费者二监听的消息] - [生产者一 0]
2018-09-08 15:06:37.460 INFO com.test.rabbitmq.BookHandler 39 listenerAutoAck - [消费者一监听的消息] - [生产者一 3]
2018-09-08 15:06:37.461 INFO com.test.rabbitmq.BookHandler 39 listenerAutoAck - [消费者一监听的消息] - [生产者一 4]
2018-09-08 15:06:37.461 INFO com.test.rabbitmq.BookHandler 54 listenerManualAck - [消费者二监听的消息] - [生产者一 1]
2018-09-08 15:06:37.462 INFO com.test.rabbitmq.BookHandler 54 listenerManualAck - [消费者二监听的消息] - [生产者一 2]
2018-09-08 15:06:37.462 INFO com.test.rabbitmq.BookHandler 54 listenerManualAck - [消费者二监听的消息] - [生产者一 3]
2018-09-08 15:06:37.462 INFO com.test.rabbitmq.BookHandler 54 listenerManualAck - [消费者二监听的消息] - [生产者一 4]