RabbitMQ浅度学习
这里介绍注解式开发RabbitMQ
RabbitMQ是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件)。RabbitMQ服务器是用Erlang语言编写的,而集群和故障转移是构建在开放电信平台框架上的。所有主要的编程语言均有与代理接口通讯的客户端库。 ---- 来自百度百科
Springboot集成RabbitMQ, 上代码
- Springboot 2.2.2.RELEASE
- JDK 1.8
*准备工作:
1: 在RabbitMQ中添加exchange, queue
2: 将exchange和queue进行绑定
3: 绑定规则, routingKey 设置为xxx.* 模糊后一位 Example: user.insert
不能匹配user.insert.demo, 如要模糊多位, 使用xxx.#
4. 路由规则也可精确匹配
也可不用手动创建exchange和queue, 以及他们的绑定关系 , 代码编写好后, 启动项目, 正常连接到RabbitMQ后相应的exchange和queue会自动创建并且绑定好
展示关键依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
RabbitMQ部分配置
spring.application.name=spring-boot-rabbitmq
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=admin
#默认的虚拟主机
spring.rabbitmq.virtual-host=my_vhost
#连接超时时间 15s
spring.rabbitmq.connection-timeout=15000
#============rabbitmq消费者配置===========#
#并发数 主页的channel会有五个
spring.rabbitmq.listener.simple.concurrency=5
#最大并发数
#spring.rabbitmq.listener.simple.max-concurrency=10
#auto自动签收 manual手动签收, 只要谈到消息可靠, 基本都是手动签收
spring.rabbitmq.listener.simple.acknowledge-mode=manual
#限流 同一时间只允许一条消息
spring.rabbitmq.listener.simple.prefetch=5
# 回调方式消息确认模式, 消息发出后 异步等待broker回送响应
# confirm机制, 回调消息到达了exchange的消息
spring.rabbitmq.publisher-confirm-type=correlated
# return机制 , 回调消息没有到达queue的消息
spring.rabbitmq.publisher-returns=true
#配置为true,到不了queue会回调方法
spring.rabbitmq.template.mandatory=true
配置文件完全可配置类替代, 看个人爱好, 书写习惯
发送端:
**
注意:
这里RabbitTemplate用构造器注入的而没有直接用@Autowired, 是因为我没有写RabbitMQConfig配置类,
准确说是我没有用配置类将RabbitTemplate变成一个非Spring实例化单例的Bean. B装完了, 说白了,
这里RabbitTemplate不能是单例的, Spring默认创建的Bean是单例的. 单例的RabbitTemplate只能回调一次,
一个RabbitTemplate只能回调一次. (这个说法可能不太准确, 官方一点我也不知道咋说…
如果用配置类来配置RabbitTemplate可以在返回RabbitTemplate的方法上加上@Scope(“prototype”), 这样用注解方式注入的RabbitTemplate就不是单例了
这个类里两个关键:
- 多例的RabbitTemplate
- 设置回调的类
package com.sunnyfe.rabbitmq.demo.producer;
import com.sunnyfe.rabbitmq.demo.callback.RabbitmqCallback;
import com.sunnyfe.rabbitmq.demo.entity.User;
import lombok.extern.log4j.Log4j2;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;
/**
* @author maple
*/
@Component
@Log4j2
public class InsertUserSender {
/**
* 非单例Bean, 构造器注入
*/
private final RabbitTemplate rabbitTemplate;
/**
* 构造方法注入rabbitTemplate成为一个多例的rabbitTemplate
* 如果是但里的rabbitmq, 只能回调一次 , 再次调用的时候会 报错: 一个rabbitmqTemplate只能回调一次
*/
public InsertUserSender(RabbitTemplate rabbitTemplate) {
this.rabbitTemplate = rabbitTemplate;
// 设置回调地址 消息到达exchange的回调
rabbitTemplate.setConfirmCallback(new RabbitmqCallback());
// 消息失败的回调 没有到queue的回调
rabbitTemplate.setReturnCallback(new RabbitmqCallback());
}
public void sendInsertUser(User user) {
CorrelationData correlationData = new CorrelationData();
correlationData.setId(user.getPrimaryKey());
try {
rabbitTemplate.convertAndSend(
//exchange 交换机 相应要在rabbitmq中添加 user-exchange
"liveExchange",
//routingKey 路由key user.insert
"info",
//消息体内容
user,
//消息唯一id
correlationData
);
log.info("消息发送成功");
} catch (AmqpException e) {
e.printStackTrace();
}
}
}
回调类
- 大可不必另外写一个回调类, 可直接写在发送者中, 然后在设置回调类的时候用this
- 要使这两个回调生效, 配置文件中的confirm机制和return机制一定得配
package com.sunnyfe.rabbitmq.demo.callback;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;
/**
* Detail 回调
*
* @author SunnyFon
* @date 2021年01月18日 21:48
*/
@Slf4j
@Component
public class RabbitmqCallback implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {
/**
* 如果没有exchange,也会收到回调
*
* ConfirmCallback 只确认消息是否正确到达 Exchange 中
* 1. 如果消息没有到exchange,则confirm回调,ack=false
* 2. 如果消息到达exchange,则confirm回调,ack=true
*
* 配置参数
* spring.rabbitmq.publisher-confirms=true
*
* @param correlationData 如果发送方没有传这个对象,则为null
* @param ack ack
* @param cause cause
* @see RabbitTemplate.ConfirmCallback#confirm
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
log.info(" 回调id:" + correlationData);
if (ack) {
log.info("消息成功到达exchange");
} else {
log.error("消息未成功到达exchange:" + cause);
}
}
/**
* Returned message callback. 到不了queue会回调到该方法
*
* @param message the returned message.
* @param replyCode the reply code.
* @param replyText the reply text.
* @param exchange the exchange.
* @param routingKey the routing key.
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("未到queue的回调么");
}
}
消息接收:
注解绑定队列和交换机(exchange), 还有路由.
死信队列信息就是arguments参数, 不需要死信队列可以去掉整个arguments={}
有几个关键点:
- 注解放的位置一定得方正确, 括号啥都不能乱, 否则脑袋找破了, 不知道为啥不创建死信队列.
- 配置文件签收方式一定设置为手动签收
- 我这里发送的是User对象, 但是接收用Message接收, 有点不讲武德可以改成public void process(@Payload User user, @Headers Map<String, Object> headers, Channel channel)
- 也可用message.getBody获取对象信息.
- 至于argument中的name定义都是来源于RabbitMQ
- 当消费消息出现异常时, 就会走catch代码, 就会执行 channel.basicNack(consumerTag, false, false); consumerTag是消息唯一id, 第一个false是否批量执行, 第二个false是否重新进入队列basicNack方法后会将消息放入死信队列, 就可在死信队列中对消费失败的消息进行业务逻辑执行, 消费死信队列中的消息.
- 死信队列 , 可以理解为一个普通的队列
package com.sunnyfe.rabbitmq.demo.rabbitmqtest;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.*;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* Detail RabbitMQ消息接收
*
* @author SunnyFon
* @date 2021年01月18日 17:39
*/
@Component
public class Receiver {
@RabbitListener(bindings = {
@QueueBinding(value = @Queue(value = "liveQueue", arguments =
{@Argument(name = "x-dead-letter-exchange", value = "deadExchange"),
@Argument(name = "x-dead-letter-routing-key", value = "deadKey")
, @Argument(name = "x-message-ttl", value = "10000", type = "java.lang.Integer")
// ,@Argument(name = "x-max-length",value = "5",type = "java.lang.Integer")队列最大长度
}),
exchange = @Exchange(value = "liveExchange"),
key = {"info", "error", "warning"}
)
})
// 设置成manual手动确认,一定要对消息做出应答,否则rabbit认为当前队列没有消费完成,将不再继续向该队列发送消息。
// 1.channel.basicAck(long,boolean); 确认收到消息,消息将被队列移除,false只确认当前consumer一个消息收到,true确认所有consumer获得的消息。
// 2.channel.basicNack(long,boolean,boolean); 确认否定消息,第一个boolean表示一个consumer还是所有,第二个boolean表示requeue是否重新回到队列,true重新入队。
// 3.channel.basicReject(long,boolean); 拒绝消息,requeue=false 表示不再重新入队,如果配置了死信队列则进入死信队列。
// 4.当消息回滚到消息队列时,这条消息不会回到队列尾部,而是仍是在队列头部,这时消费者会又接收到这条消息,如果想消息进入队尾,须确认消息后再次发送消息。
@RabbitHandler
public void onMessage(Message message, Channel channel) {
long consumerTag = message.getMessageProperties().getDeliveryTag();
try {
// int i = 1/0; 模拟消费消息, 回调接收ack是true 因为回调的ack根据exchange是否收到消息判断
// int i = 1/0;
System.out.println("收到消息");
System.out.println(new String(message.getBody()));
/*
* 配置文件中配置的是手动签收
* 如果注释ACK 也可以消费信息, 不过在控制台上消息仍然存在 消息会回到队列
*
* 业务逻辑完成后一定更要告诉rabbitmq服务器消息消费完了
* false 不使用批量ack false可能会出现死循环 , 有消息永远消费不了 可以考虑将消息放到死信队列, 执行业务逻辑的时候try catch, 在catch中添加到死信队列
* true 使用批量ack 可能会导致消息丢失, 加上死信队列可解决消息丢失问题
*/
channel.basicAck(consumerTag, true);
} catch (Exception e) {
try {
// 消费异常消息会添加到死信队列
channel.basicNack(consumerTag, false, false);
} catch (IOException ioException) {
ioException.printStackTrace();
}
e.printStackTrace();
}
}
}
死信队列
package com.sunnyfe.rabbitmq.demo.rabbitmqtest;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* Detail
*
* @author SunnyFon
* @date 2021年01月18日 17:51
*/
@Component
public class DeadReceiver {
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue(value = "deadQueue"),
exchange = @Exchange(value = "deadExchange"),
key = "deadKey"
)
})
public void receive2(Message message, Channel channel) throws IOException {
System.out.println("我是一条死信:" + message);
long consumerTag = message.getMessageProperties().getDeliveryTag();
System.out.println(consumerTag);
channel.basicAck(consumerTag, true);
}
}