背景
RabbitMQ 作为最常用的消息中间件,这里做一下相关学习记录
2.2 消息队列的使用场景
2.3 基础应用 SpringBoot + Spring AMQP(内部整合了RabbitMQ)
docker pull rabbitmq:3.7.7-management
docker run -d --name rabbitmq3.7.7 -p 5672:5672 -p 15672:15672 -v `pwd`/data:/var/lib/rabbitmq --hostname myRabbit -e RABBITMQ_DEFAULT_VHOST=my_vhost -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin df80af9ca0c9
参数说明:
-d 容器后台运行 --name 自定义容器名称 -p 指定服务端口映射 (5672:应用访问接口,15672: web管理页面访问接口) -v 映射目录或文件 –hostname (主机名) -e 指定环境变量
http://服务器地址:15672
org.springframework.boot
spring-boot-starter-amqp
spring:
rabbitmq:
host: "localhost"
port: 5672
username: "admin"
password: " df80af9ca0c9"
消费者
使用@RabbitListener 可以标注在方法上单独使用,也可以标注在类上,配合 @RabbitHandler 注解一起使用
2.4 基本原理
官网 https://www.rabbitmq.com/getstarted.html
首先了解一下 RabbitMq 常用关键字
以下是配置类实例:
@Configuration
public class RabbitMQConfig {
private static final String topicExchangeName = "topic-exchange";
private static final String fanoutExchange = "fanout-exchange";
private static final String headersExchange = "headers-exchange";
private static final String queueName = "cord";
//声明队列
@Bean
public Queue queue() {
//Queue(String name, boolean durable, boolean exclusive, boolean autoDelete)
return new Queue("cord", false, true, true);
//第二个参数为 true 配置队列的持久化
}
//声明Topic交换机
@Bean
TopicExchange topicExchange() {
return new TopicExchange(topicExchangeName);
}
//将队列与Topic交换机进行绑定,并指定路由键
@Bean
Binding topicBinding(Queue queue, TopicExchange topicExchange) {
return BindingBuilder.bind(queue).to(topicExchange).with("org.cord.#");
}
//声明fanout交换机
@Bean
FanoutExchange fanoutExchange() {
return new FanoutExchange(fanoutExchange);
}
//将队列与fanout交换机进行绑定
@Bean
Binding fanoutBinding(Queue queue, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(queue).to(fanoutExchange);
}
//声明Headers交换机
@Bean
HeadersExchange headersExchange() {
return new HeadersExchange(headersExchange);
}
//将队列与headers交换机进行绑定
@Bean
Binding headersBinding(Queue queue, HeadersExchange headersExchange) {
Map map = new HashMap<>();
map.put("First","A");
map.put("Fourth","D");
//whereAny表示部分匹配,whereAll表示全部匹配
// return BindingBuilder.bind(queue).to(headersExchange).whereAll(map).match();
return BindingBuilder.bind(queue).to(headersExchange).whereAny(map).match();
}
}
Producer.java
@Component
public class Producer {
@Autowired
private AmqpTemplate template;
@Autowired
private AmqpAdmin admin;
/**
* @param routingKey 路由关键字
* @param msg 消息体
*/
public void sendDirectMsg(String routingKey, String msg) {
template.convertAndSend(routingKey, msg);
}
/**
* @param routingKey 路由关键字
* @param msg 消息体
* @param exchange 交换机
*/
public void sendExchangeMsg(String exchange, String routingKey, String msg) {
template.convertAndSend(exchange, routingKey, msg);
}
/**
* @param map 消息headers属性
* @param exchange 交换机
* @param msg 消息体
*/
public void sendHeadersMsg(String exchange, String msg, Map map) {
template.convertAndSend(exchange, null, msg, message -> {
message.getMessageProperties().getHeaders().putAll(map);
return message;
});
}
}
测试用例
RabbitmqTest.java
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = CordApplication.class)
public class RabbitmqTest {
@Autowired
private Producer producer;
//Direct
@Test
public void sendDirectMsg() {
producer.sendDirectMsg("cord", String.valueOf(System.currentTimeMillis()));
}
//Topic
@Test
public void sendtopicMsg() {
producer.sendExchangeMsg("topic-exchange","org.cord.test", "hello world");
}
//Fanout
@Test
public void sendFanoutMsg() {
producer.sendExchangeMsg("fanout-exchange", "abcdefg", String.valueOf(System.currentTimeMillis()));
}
//Headers
@Test
public void sendHeadersMsg() {
Map map = new HashMap<>();
map.put("First","A");
producer.sendHeadersMsg("headers-exchange", "hello word", map);
}
}
2.5 ACK 消息确认
消息被消费者接受都需要做消息确认,消息通过 ACK 确认是否被正确接收,每个 Message 都要被确认(acknowledged),可以手动去 ACK 或自动 ACK。
消息确认模式有:
AcknowledgeMode.NONE:自动确认
AcknowledgeMode.AUTO:根据情况确认
AcknowledgeMode.MANUAL:手动确认。
默认模式为 自动确认,自动确认模式会在消息发送给消费者后立刻确认,也就是如果消费者后续代码报错,消息也会被消耗,不会保留。
手动确认
常用API
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);
nack表示拒绝消息。multiple表示拒绝指定了delivery_tag的所有未确认的消息,requeue表示不是重回队列
2.6 常见问题
观察者模式和发布/订阅模式的区别
观察者模式的定义:对象间的一种一对多的组合关系,以便一个对象的状态发生变化时,所有依赖于它的对象都得到通知。
发布/订阅模式
在观察者模式中的Subject就像一个发布者(Publisher),而观察者(Observer)完全可以看作一个订阅者(Subscriber)。subject通知观察者时,就像一个发布者通知他的订阅者。这也就是为什么很多书和文章使用“发布-订阅”概念来解释观察者设计模式。但是这里还有另外一个流行的模式叫做发布-订阅设计模式。它的概念和观察者模式非常类似。最大的区别是:
在发布-订阅模式,消息的发送方,叫做发布者(publishers),消息不会直接发送给特定的接收者(订阅者)。
如何保证RabbitMQ不被重复消费?
正常情况下,消费者在消费消息的时候,消费完毕后,会发送一个确认消息给消息队列,消息队列就知道该消息被消费了,就会将该消息从消息队列中删除;
但是因为网络传输等等故障,确认信息没有传送到消息队列,导致消息队列不知道自己已经消费过该消息了,再次将消息分发给其他的消费者。
比如:可以在写入消息队列的数据做唯一标示,消费消息时,根据唯一标识判断是否消费过;