RabbitMq
每周一句:百步之内,必有芳草
1.使用
1.1.docker部署rabbitMq
- docker下载镜像
docker pull rabbitmq:3.12-management
- 启动
docker run \
-e RABBITMQ_DEFAULT_USER=itheima \
-e RABBITMQ_DEFAULT_PASS=123321 \
-v mq-plugins:/plugins \
--name mq \
--hostname mq \
-p 15672:15672 \
-p 5672:5672 \
--network hmall \
-d \
rabbitmq:3.8-management
1.2.java中使用
- 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
- 配置yaml
spring:
rabbitmq:
host: 127.0.0.1
port: 5672
virtual-host: /hmall
username: zhao
password: 123
- 使用
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void TestPublisher(){
String queueName = "simple.queue";
String msg = "hello world";
rabbitTemplate.convertAndSend(queueName,msg);
}
- 接收消息
@Slf4j
@Component
public class consumer {
@RabbitListener(queues = "simple.queue")
public void receive(String msg) {
System.out.println(msg);
}
}
2.work模型
2.1.消费者消息推送限制
-
默认情况下,RabbitMq会将消息依次轮询投递给绑定在队列上的每一个消费者,但这并没有考虑到消费者是否处理完消息,可能出现消息的堆积。
-
解决方式:
spring: rabbitmq: listener: simple: prefethch: 1 #每次只能获取一条消息,处理完才能获取下一个消息 -
解决问题(面试) :
- 通过work模型的使用可以解决消息的堆积问题(或者通过缓存,java线程池等异步处理加快消息处理的速度),在多个消费者绑定同一个消息队列,有的处理的快,有的处理的慢,通过work模型,可以使得消息的处理速度加快;
- 同一个消息只能被一个消费者进行处理;
- 通过设置prefetch来控制消费者预取消息的数量,处理完一条再去处理下一条,实现能者多劳。
3.Fanout交换机
3.1.概念
- Fanout交换机会将接收到的消息广播给每一个与其绑定的对列
3.2.使用场景
- 支付订单:当用户支付订单的时候,将支付成功的消息发送给每一个绑定的对列,只要有一个消费者接收到成功的消息就可以去数据库中更改用户的余额。
3.3.实现
- 在rabbitMq中新建交换机类型选择fanout
- 在队列中绑定交换机
- 在java中发送代码,并接收
@Test
public void TestPublisherFanout(){
String queueName = "simple.fanout";
String msg = "hello world1";
rabbitTemplate.convertAndSend(queueName,"",msg);
}
@RabbitListener(queues = "fanout.queue")
public void receiveFanout(String msg) {
System.out.println("Fanout"+msg);
}
@RabbitListener(queues = "fanout.queue1")
public void receiveFanout1(String msg) {
System.out.println("Fanout1"+msg);
}
// 输出
Fanout1hello world1
Fanouthello world1
4.Direct交换机
4.1.定义
-
Direct 交换机会将接收到的消息根据规则路由到指定的Queue,由此称为定向路由
- 每个Queue到Exchange设置一个BindingKey
- 发布者发送消息的时候指定接收消息的RoutingKey
- Exchange将消息路由到Binding与消息RoutingKey一致的对列
4.2.使用
-
在rabbitmq中创建两个对列
-
创建一个交换机类型选择direct
-
在创建的交换机中绑定两个队列,并且绑定BindingKey
-
在Java中发送和接收代码
@Test public void TestPublisherDirect(){ String exchangeName = "simple.direct"; String msg = "hello 粉色"; // 选择绑定的bindingkey rabbitTemplate.convertAndSend(exchangeName,"pink",msg); } @RabbitListener(queues = "direct.queue1") public void receiveDirect1(String msg) { System.out.println("direct1==="+msg); } @RabbitListener(queues = "direct.queue2") public void receiveDirect2(String msg) { System.out.println("direct2==="+msg); }
5.Topic交换机
5.1.定义
-
TopicExchange和DirecExchange类似,区别在于routingKey可以是多个单词的列表,并且使用"."分割。
-
Queue和Exchange制定的BindingKey时可以使用通配符:
-
#:代指0个或者多个单词
-
*:代指一个单词
-
例子
Ceshi.# (全部以ceshi开头的BingdingKey)
Ceshi.*(以ceshi开头的并且只有一个单词的BingdingKey)
-
-
优点:比direct更加灵活的绑定BindingKey
5.2.使用
- 在rabbitmq中创建两个对列
- 创建一个交换机类型选择topic
- 在创建的交换机中绑定两个队列,并且绑定BindingKey
- 在Java中发送和接收代码
@Test
public void TestPublisherTopic(){
String exchangeName = "simple.topic";
String msg = "hello ceshi.news.ces";
rabbitTemplate.convertAndSend(exchangeName,"ceshi.news.ces",msg);
}
@RabbitListener(queues = "topic.queue1")
public void receiveTopic1(String msg) {
System.out.println("Topic1==="+msg);
}
@RabbitListener(queues = "topic.queue2")
public void receiveTopic2(String msg) {
System.out.println("Topic2==="+msg);
}
6.java代码创建交换机和对列
6.1.两种方式
- 基于配置
@Configuration
public class FanoutConfiguration {
/**
* 创建交换机
*
* @return
*/
@Bean
public FanoutExchange fanoutExchange() {
// new FanoutExchange("hmall.fanout")
return ExchangeBuilder.fanoutExchange("hmall.fanout").build();
}
/**
* 创建队列
*
* @return
*/
@Bean
public Queue fanoutQueue() {
// 默认自动持久化
// new Queue("hmall.fanout");
// durable 是持久化的意思
return QueueBuilder.durable("hmall.fanout").build();
}
/**
* 绑定 方式一
*
* @param fanoutQueue 队列
* @param fanoutExchange 交换机
* @return
*/
@Bean
public Binding fanoutBinding(Queue fanoutQueue, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(fanoutQueue).to(fanoutExchange);
}
/**
* 绑定 方式二
*
* @return
*/
@Bean
public Binding fanoutBinding() {
// 在sping中 直接调用方法底层不一定直接调用方法,如果bean中没有这个交换机和对垒列那么就会创建,如果有就会直接调用
// 因为被动态代理了
return BindingBuilder.bind(fanoutQueue()).to(fanoutExchange());
}
}
- 基于注解
@RabbitListener(
bindings = @QueueBinding(
value = @Queue(name = "direct.queue1", durable = "true"),
exchange = @Exchange( name="hmall.direct", type = ExchangeTypes.DIRECT),
key = {"blue","red"}
)
)
public void receiveDirect1(String msg) {
System.out.println("direct1===" + msg);
}
7.消息转换器Spring的消息发送
7.1.解决问题
-
发送消息回自动将消息序列化,但是默认使用的是jdk的序列化方法,有以下问题
- 数据体积大
- 安全问题
- 可读性差
7.2.实现
- 首先在项目中加入依赖(生产者消费者都要加上)
<!-- jackson -->
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
- 在启动类里加入消息转换器
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
- 发送消息
@Test
public void TestPublisher(){
String queueName = "object.queue";
HashMap<String, Object> msg = new HashMap<>();
msg.put("name","ces");
msg.put("age",123);
rabbitTemplate.convertAndSend(queueName,msg);
}
// 输出结果
{"name":"ces","age":123}
RabbitMq(高级)
1.消息的可靠性
1.1发送者的可靠性
-
生产者重联:
-
问题有时候网络波动,可能出现客户端于mq连接失败的情况,通过在yaml里面配置重联机制
spring: rabbitmq: # 连接失败尝试 connection-timeout: 1s #设置MQ的连接超时时间 template: retry: enabled: true #开启超时重试机制 multiplier: 1 #失败后下次的等待时长的倍数 下次等待时长 = initial-interval * multiplier initial-interval: 1000ms #失败后的初始等待时间 max-attempts: 3 #最大重试次数 -
注意:此重连为阻塞式的,会影响业务的性能,如果对业务性能有要求,建议建议重试机制。如果要使用要配置合理的等待时间和重试次数等。
-
-
生产者确认(尽量不要用面试可能问):
概念:RabbitMq有Publisher Confirm和Publisher Return两种确认机制。开启确认机制后,在MQ成功接收到消息后会返回确认消息给生产者,有以下几种情况:
- 消息投递到mq,但是l路由失败了。此时会通过Publisher Return返回路由异常原因,然后返回ack,告知成功。
- 临时消息投递到了mq,并且成功入队,返回ack,告知成功。
- 持久消息投递到了mq,并且成功入对且持久化完成,返回ack,告知投递成功
- 其他情况都会返回NACK,告知失败。
实现:
-
代码实现:在生产者yaml里面添加配置
publisher-confirm-type: correlated publisher-returns: true -
同步:simple
-
异步:correlated
-
配置和发送消息
@Slf4j @Configuration public class MqConfirmConfig implements ApplicationContextAware { @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { RabbitTemplate rabbit = applicationContext.getBean(RabbitTemplate.class); rabbit.setReturnsCallback(new RabbitTemplate.ReturnsCallback() { @Override public void returnedMessage(ReturnedMessage returnedMessage) { log.debug("收到的消息:exchange:{}, key:{},msg:{},code:{},text:{}", returnedMessage.getExchange(), returnedMessage.getRoutingKey(), returnedMessage.getMessage(), returnedMessage.getReplyCode(), returnedMessage.getReplyText()); } }); } }@Test void testConfirm() { CorrelationData cd = new CorrelationData(UUID.randomUUID().toString()); cd.getFuture().addCallback(new ListenableFutureCallback<CorrelationData.Confirm>() { @Override public void onFailure(Throwable ex) { log.error("消息回掉失败"); } @Override public void onSuccess(CorrelationData.Confirm result) { if (result.isAck()) { log.info("消息发送成功"); } else { log.error("消息发送失败"); } } }); rabbitTemplate.convertAndSend("simple.direct", "red", "direct.queue2", cd); }
2.MQ的可靠性
2.1.问题
-
在默认的情况下,RabbitMq会将接收到的消息保存到内存中以降低消息收发的延迟会导致两个问题
- 一旦MQ宕机,内存中的消息会丢失
- 内存空间有限,当消费者故障或处理过慢时,会导致消息积压,引发MQ阻塞
2.2.数据持久化(老版)
-
交换机持久化:mq配置Durable
-
队列持久化:mq配置Durable
-
消息持久化:delivery mode改为2
-
代码实现
Message msg = MessageBuilder .withBody("hello".getBytes(StandardCharsets.UTF_8)) // PERSISTENT持久化 NON_PERSISTENT非持久化 .setDeliveryMode(MessageDeliveryMode.PERSISTENT).build(); rabbitTemplate.convertAndSend("direct.queue2", msg);