RabbitMq
基本概念
基本使用
五种工作模式
- 导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
<version>1.5.2.RELEASE</version>
</dependency>
- 配置文件
#对于rabbitMQ的支持
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
- 配置类
/**
* 针对消费者配置
* 1. 设置交换机类型
* 2. 将队列绑定到交换机
FanoutExchange: 将消息分发到所有的绑定队列,无routingkey的概念
HeadersExchange :通过添加属性key-value匹配
DirectExchange:按照routingkey分发到指定队列
TopicExchange:多关键字匹配
*/
@Bean
public DirectExchange defaultExchange() {
return new DirectExchange(EXCHANGE_A);
}
/**
* 获取队列A
* @return
*/
@Bean
public Queue queueA() {
return new Queue(QUEUE_A, true); //队列持久
}
@Bean
public Binding binding() {
return BindingBuilder.bind(queueA()).to(defaultExchange()).with(RabbitConfig.ROUTINGKEY_A);
}
- 简单队列
一个生产者对应一个消费者
/*简单队列(模式)*/
@Test
public void contextLoads(){
String msg = "这是一个简单队列模式";
amqpTemplate.convertAndSend("spring.simple.queue", msg );
}
@Component
public class SimpleListener {
// 通过注解自动创建 spring.simple.queue 队列
@RabbitListener(queuesToDeclare = @Queue("spring.simple.queue"))
public void listen(String msg) {
System.out.println("简单队列 接收到消息:" + msg);
}
}
- work 模式 (一个生产者对应多个消费者)
/*work 模式*/
@Test
public void work() throws InterruptedException {
String msg = "这是一个work模式";
for (int i = 0; i < 10; i++) {
amqpTemplate.convertAndSend("spring.work.queue", msg + i);
}
Thread.sleep(5000);
}
@Component
public class WorkListener {
// 通过注解自动创建 spring.work.queue 队列
@RabbitListener(queuesToDeclare = @Queue("spring.work.queue"))
public void listen(String msg) {
System.out.println("work模式 接收到消息:" + msg);
}
// 创建两个队列共同消费
@RabbitListener(queuesToDeclare = @Queue("spring.work.queue"))
public void listen2(String msg) {
System.out.println("work模式二 接收到消息:" + msg);
}
}
- 订阅模型-Fanout
订阅模型-Fanout也成为广播模式,流程如下:
1.可以有多个消费者
2.每个消费者有自己的队列
3. 每个队列都要绑定到Exchange(交换机)
4. 生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定。
5. 交换机把消息发送给绑定过的所有队列
6. 队列的消费者都能拿到消息。实现一条消息被多个消费者消费
/*订阅模型-Fanout*/
@Test
public void fanout() throws InterruptedException {
String msg = "订阅模式";
for (int i = 0; i < 10; i++) {
// 这里注意细节,第二个参数需要写,否则第一个参数就变成routingKey了
amqpTemplate.convertAndSend("spring.fanout.exchange", "", msg + i);
}
Thread.sleep(5000);
}
@Component
public class FanoutListener {
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "spring.fanout.queue", durable = "true"),
exchange = @Exchange(
value = "spring.fanout.exchange",
ignoreDeclarationExceptions = "true",
type = ExchangeTypes.FANOUT
)
))
public void listen(String msg) {
System.out.println("订阅模式1 接收到消息:" + msg);
}
// 队列2(第二个人),同样能接收到消息
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "spring.fanout2.queue", durable = "true"),
exchange = @Exchange(
value = "spring.fanout.exchange",
ignoreDeclarationExceptions = "true",
type = ExchangeTypes.FANOUT
)
))
public void listen2(String msg) {
System.out.println("订阅模式2 接收到消息:" + msg);
}
}
- 订阅模型-Direct (路由模式)
在Fanout模式中,一条消息,会被所有订阅的队列都消费。但是,在某些场景下,我们希望不同的消息被不同的队列消费。这时就要用到Direct类型的Exchange。给特定的消费者消费
在Direct模型下:
1.队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key)
2.消息的发送方在 向 Exchange发送消息时,也必须指定消息的 RoutingKey。
3.Exchange不再把消息交给每一个绑定的队列,而是根据消息的Routing Key进行判断,只有队列的 Routingkey
4. 与消息的 Routing key完全一致,才会接收到消息
/*订阅模型-Direct (路由模式)*/
@Test
public void direct() throws InterruptedException {
String msg = "路由模式";
for (int i = 0; i < 10; i++) {
amqpTemplate.convertAndSend("spring.direct.exchange", "direct", msg + i);
}
Thread.sleep(5000);
}
@Component
public class DirectListener {
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "spring.direct.queue", durable = "true"),
exchange = @Exchange(
value = "spring.direct.exchange",
ignoreDeclarationExceptions = "true"
),
key = {"direct"}
))
public void listen(String msg) {
System.out.println("路由模式1 接收到消息:" + msg);
}
// 队列2(第二个人),key值不同,接收不到消息
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "spring.direct2.queue", durable = "true"),
exchange = @Exchange(
value = "spring.direct.exchange",
ignoreDeclarationExceptions = "true"
),
key = {"direct-test"}
))
public void listen2(String msg) {
System.out.println("路由模式2 接收到消息:" + msg);
}
}
- 订阅模型-Topic (主题模式)
Topic类型的Exchange与Direct相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型Exchange可以让队列在绑定Routing key 的时候使用通配符!
Routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: user.insert
通配符规则 | 举例 |
---|---|
#:匹配一个或多个词 | person.#:能够匹配person.insert.save 或者 person.insert |
*:匹配不多不少恰好1个词 | person.*:只能匹配person.insert |
/* 订阅模型-Topic (主题模式)*/
@Test
public void topic() throws InterruptedException {
amqpTemplate.convertAndSend("spring.topic.exchange", "person.insert", "增加人员");
amqpTemplate.convertAndSend("spring.topic.exchange", "person.delete", "删除人员");
amqpTemplate.convertAndSend("spring.topic.exchange", "money.insert", "加钱");
amqpTemplate.convertAndSend("spring.topic.exchange", "money.delete", "减钱");
Thread.sleep(5000);
}
@Component
public class TopicListener {
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "spring.topic.queue", durable = "true"),
exchange = @Exchange(
value = "spring.topic.exchange",
ignoreDeclarationExceptions = "true",
type = ExchangeTypes.TOPIC
),
key = {"person.*"}
))
public void listen(String msg) {
System.out.println("person 接收到消息:" + msg);
}
// 通配规则不同,接收不到消息
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "spring.topic.queue", durable = "true"),
exchange = @Exchange(
value = "spring.topic.exchange",
ignoreDeclarationExceptions = "true",
type = ExchangeTypes.TOPIC
),
key = {"money.*"}
))
public void listen2(String msg) {
System.out.println("money Student 接收到消息:" + msg);
}
}
RabbitMq的消息发送确认与接收确认
- 默认情况下如果一个 Message 被消费者所正确接收则会被从 Queue 中移除
- 如果一个 Queue 没被任何消费者订阅,那么这个 Queue 中的消息会被 Cache(缓存),当有消费者订阅时则会立即发送,当 Message 被消费者正确接收时,就会被从 Queue 中移除
-
消息发送确认
当消息无法路由到队列时,确认消息路由失败。消息成功路由时,当需要发送的队列都发送成功后,进行确认消息,对于持久化队列意味着写入磁盘,对于镜像队列意味着所有镜像接收成功 -
ConfirmCallback
- 通过实现 ConfirmCallback 接口,消息发送到 Broker 后触发回调,确认消息是否到达 Broker 服务器,也就是只确认是否正确到达 Exchange 中
-
配置文件
spring.rabbitmq.publisher-confirms: true
@Component
public class RabbitTemplateConfirmCallbackConfig implements RabbitTemplate.ConfirmCallback{
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void init(){
rabbitTemplate.setConfirmCallback(this); //指定 ConfirmCallback
}
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
System.out.println("消息唯一标识:"+correlationData);
System.out.println("确认结果:"+ack);
System.out.println("失败原因:"+cause);
}
-
ReturnCallback
- 通过实现 ReturnCallback 接口,启动消息失败返回,比如路由不到队列时触发回调
还需要在配置文件添加配置
spring.rabbitmq.publisher-returns: true
- 通过实现 ReturnCallback 接口,启动消息失败返回,比如路由不到队列时触发回调
@Component
public class RabbitTemplateConfig implements RabbitTemplate.ReturnCallback{
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void init(){
rabbitTemplate.setReturnCallback(this); //指定 ReturnCallback
}
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("消息主体 message : "+message);
System.out.println("消息主体 message : "+replyCode);
System.out.println("描述:"+replyText);
System.out.println("消息使用的交换器 exchange : "+exchange);
System.out.println("消息使用的路由键 routing : "+routingKey);
}
}
消费者消息确认接收
- 消费端 Ack 和 Nack 机制
- 参考 api
void basicNack(long deliveryTag, boolean multiple, boolean requeue) throws IOException;
void basicAck(long deliveryTag, boolean multiple) throws IOException;
- 消息通过 ACK 确认是否被正确接收,每个 Message 都要被确认(acknowledged),可以手动去 ACK 或自动 ACK
- 自动确认会在消息发送给消费者后立即确认,但存在丢失消息的可能,如果消费端消费逻辑抛出异常,也就是消费端没有处理成功这条消息,那么就相当于丢失了消息
- 如果消息已经被处理,但后续代码抛出异常,使用 Spring 进行管理的话消费端业务逻辑会进行回滚,这也同样造成了实际意义的消息丢失
- 如果手动确认则当消费者调用 ack、nack、reject 几种方法进行确认,手动确认可以在业务失败后进行一些操作,如果消息未被 ACK 则会发送到下一个消费者
- 如果某个服务忘记 ACK 了,则 RabbitMQ 不会再发送数据给它,因为 RabbitMQ 认为该服务的处理能力有限
ACK 机制还可以起到限流作用,比如在接收到某条消息时休眠几秒钟消息确认模式有:
1 AcknowledgeMode.NONE:自动确认
2 AcknowledgeMode.AUTO:根据情况确认
3 AcknowledgeMode.MANUAL:手动确认
- 确认消息(局部方法处理消息)
- 默认情况下消息消费者是自动 ack (确认)消息的,如果要手动 ack(确认)则需要修改确认模式为 manual
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: manual
- 确认消息
@RabbitHandler
public void processMessage2(String message,Channel channel,@Header(AmqpHeaders.DELIVERY_TAG) long tag) {
System.out.println(message);
try {
channel.basicAck(tag,false); // 确认消息
} catch (IOException e) {
e.printStackTrace();
}
}
或者
@RabbitHandler
public void processMessage2(String message, Channel channel,@Headers Map<String,Object> map) {
System.out.println(message);
if (map.get("error")!= null){
System.out.println("错误的消息");
try {
channel.basicNack((Long)map.get(AmqpHeaders.DELIVERY_TAG),false,true); //否认消息
return;
} catch (IOException e) {
e.printStackTrace();
}
}
try {
channel.basicAck((Long)map.get(AmqpHeaders.DELIVERY_TAG),false); //确认消息
} catch (IOException e) {
e.printStackTrace();
}
}
或者
@RabbitHandler
public void consumerMsg2(Message message,Channel channel) throws IOException {
log.info("consumerMsg2===消费消息:{}",message.getPayload());
//手工签收
Long deliveryTag = (Long) message.getHeaders().get(AmqpHeaders.DELIVERY_TAG);
log.info("consumerMsg2===接受deliveryTag:{}",deliveryTag);
channel.basicAck(deliveryTag,false);
}
- channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);的参数
- deliveryTag(唯一标识 ID):当一个消费者向 RabbitMQ 注册后,会建立起一个 Channel ,RabbitMQ 会用 basic.deliver 方法向消费者推送消息,这个方法携带了一个 delivery tag, 它代表了 RabbitMQ 向该 Channel 投递的这条消息的唯一标识 ID,是一个单调递增的正整数,delivery tag 的范围仅限于 Channel
- multiple:为了减少网络流量,手动确认可以被批处理,当该参数为 true 时,则可以一次性确认 delivery_tag 小于等于传入值的所有消息
- mq 传递类的一种方式
Order order = new Order();
ObjectMapper objectMapper = new ObjectMapper();
String orderJson = objectMapper.writeValueAsString(order);
org.springframework.amqp.core.MessageProperties messageProperties = new MessageProperties();
org.springframework.amqp.core.Message message =
new org.springframework.amqp.core.Message(orderJson.getBytes(),messageProperties);
rabbitTemplate.convertAndSend("springboot.direct.exchange","springboot.key3",message,correlationData);
ObjectMapper objectMapper = new ObjectMapper();
Order order = objectMapper.readValue(message.getBody(),Order.class);