一、RabbitMQ高级特性预热工作
1、自定义消费端监听(需要继承DefaultConsumer类)
public class CyanConsumer extends DefaultConsumer {
public CyanConsumer(Channel channel) {
super(channel);
}
public void handleDelivery(String consumerTag,Envelope envelope,AMQP.BasicProperties properties,byte[] body) throws IOException {
System.out.println("consumerTag:"+consumerTag);
System.out.println("envelope:"+envelope);
System.out.println("properties:"+properties);
System.out.println("body:"+new String(body));
}
}
2、代码示例
1)消息生产者
public static void main(String[] args) throws IOException, TimeoutException {
//todo 1、创建连接工厂并设置连接工厂属性
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("47.93.60.129");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("cyan");
connectionFactory.setUsername("cyan");
connectionFactory.setPassword("cyan");
//todo 2、创建一个连接
Connection connection = connectionFactory.newConnection();
//todo 3、创建一个channel
Channel channel = connection.createChannel();
//todo 定义交换器名称、路由键
String exchangeName = "cyan.base.direct";
String routingKey = "cyan.base.key";
//todo 4、发送消息
String msgBody = "你好cyan";
for(int i=0;i<5;i++) {
channel.basicPublish(exchangeName,routingKey,null,(msgBody+i).getBytes());
}
//todo 5、关闭连接
channel.close();
connection.close();
}
2)消息消费者
public static void main(String[] args) throws IOException, TimeoutException {
//todo 1、创建连接工厂并设置连接工厂属性
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("47.93.60.129");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("cyan");
connectionFactory.setUsername("cyan");
connectionFactory.setPassword("cyan");
connectionFactory.setConnectionTimeout(100000);
//todo 2、创建一个连接
Connection connection = connectionFactory.newConnection();
//todo 3、创建一个channel
Channel channel = connection.createChannel();
//todo 声明交换机名称、类型、队列名称、绑定键(路由键)
String exchangeName = "cyan.base.direct";
String exchangeType = "direct";
String queueName = "cyan.base.queue";
String routingKey = "cyan.base.key";
//todo 4、声明交换器、队列并把队列绑定到交换器上
channel.exchangeDeclare(exchangeName,exchangeType,true,false,null);
channel.queueDeclare(queueName,true,false,false,null);
channel.queueBind(queueName,exchangeName,routingKey);
//todo 5、使用自定义消费者接收消息(默认关闭消息自动签收功能)
channel.basicConsume(queueName,new CyanConsumer(channel));
}
二、RabbitMQ高级特性之ack、nack机制
1、什么是消息确认机制(消费端)
如果在处理消息的过程中,消费者的服务器出现异常,那么这条消息可能还没有完成消费而导致数据丢失,为了确保消息不会丢失,RabbitMQ支持消息确认机制(ACK机制)
2、消息确认机制流程(消费端)
ACK机制是消费者从RabbitMQ拿到消息并处理完成后反馈给RabbitMQ的,RabbitMQ收到消费端的ACK反馈后才将此消息从队列中移除。如果消费者在处理消息时出现了网络不稳定、服务器异常等现象,那么RabbitMQ就不会收到消费端的ACK反馈,RabbitMQ会认为这个消息没有正常消费,会将消息重新放回队列中(也可以通过设置nack属性让消息不重回队列)
3、代码示例
1)消息生产者
public static void main(String[] args) throws IOException, TimeoutException {
//... ...省略创建信道代码
//todo 定义交换器名称、路由键、消息体
String exchangeName = "cyan.ack_nack.direct";
String routingKey = "cyan.ack_nack.key";
String msgBody = "你好cyan";
//todo 发送消息
for(int i=0;i<10;i++) {
Map<String,Object> infoMap = new HashMap<>();
infoMap.put("mark",i);
AMQP.BasicProperties basicProperties = new AMQP.BasicProperties().builder()
.deliveryMode(2)//消息持久化
.contentEncoding("UTF-8")
.correlationId(UUID.randomUUID().toString())
.headers(infoMap)
.build();
channel.basicPublish(exchangeName,routingKey,basicProperties,(msgBody+i).getBytes());
}
}
2)消息消费者
public static void main(String[] args) throws IOException, TimeoutException {
//... ...省略创建信道代码
//todo 声明交换器名称、类型、队列名称、绑定键(路由键)
String exchangeName = "cyan.ack_nack.direct";
String exchangeType = "direct";
String queueName = "cyan.ack_nack.queue";
String routingKey = "cyan.ack_nack.key";
channel.exchangeDeclare(exchangeName,exchangeType,true,false,null);
channel.queueDeclare(queueName,true,false,false,null);
channel.queueBind(queueName,exchangeName,routingKey);
//todo 接收消息(关闭消息自动签收功能)
channel.basicConsume(queueName,false,new CyanAckConsumer(channel));
}
3)自定义消费端监听
public class CyanAckConsumer extends DefaultConsumer {
private Channel channel;
public CyanAckConsumer(Channel channel) {
super(channel);
this.channel = channel;
}
public void handleDelivery(String consumerTag,Envelope envelope,AMQP.BasicProperties properties,byte[] body) throws IOException {
try{
//todo 模拟业务
Integer mark = (Integer) properties.getHeaders().get("mark");
if(mark != 0 ) {
System.out.println("消费消息:"+new String(body));
//todo 手动提交
channel.basicAck(envelope.getDeliveryTag(),false);
}else{
throw new RuntimeException("模拟业务异常");
}
}catch (Exception e) {
System.out.println("异常消费消息:"+new String(body));
//todo 重回队列
//channel.basicNack(envelope.getDeliveryTag(),false,true);
//todo 不重回队列
channel.basicNack(envelope.getDeliveryTag(),false,false);
}
}
}
三、RabbitMQ高级特性之confirm机制
1、什么是消息确认机制(生产端)
生产者将消息投递后,如果mq-server接收到消息,就会给生产端一个应答,生产者接受到应答,来确保该条消息是否成功发送到了my-server,confirm机制是消息可靠性投递的核心保障
2、消息确认机制流程(生产端)
confirm机制是生产端向RabbitMQ发送消息并在消息被投递到所有匹配的队列之后反馈给生产端的,生产端收到RabbitMQ的confirm反馈后才能确认此消息是否已发送到所有匹配到的队列中。如果生产者发送消息后,在没有到达RabbitMQ之前出现意外(比如网络不稳定、服务器异常等现象),那么生产端就不会收到RabbitMQ的confirm反馈,生产端会认为这个消息没有正常发送到队列中。
3、代码示例
1)消息生产者
public static void main(String[] args) throws IOException, TimeoutException {
//... ...省略创建信道代码
//todo 1、设置消息投递模式(开启生产端确认模式)
channel.confirmSelect();
//todo 定义交换器名称、路由键、消息体
String exchangeName = "cyan.confirm.topicexchange";
String routingKey = "cyan.confirm.key";
String msgContext = "你好 青子....";
//todo 设置消息属性
Map<String,Object> info = new HashMap<>();
tulingInfo.put("company","cyan");
tulingInfo.put("location","天津");
AMQP.BasicProperties basicProperties = new AMQP.BasicProperties().builder()
.deliveryMode(2)
.correlationId(UUID.randomUUID().toString())
.timestamp(new Date())
.headers(info)
.build();
//todo 2、消息确认监听(监听成功和异常的confirm结果)
channel.addConfirmListener(new CyanConfirmListener());
//todo 发送消息
channel.basicPublish(exchangeName,routingKey,basicProperties,msgContext.getBytes());
}
2)消息消费者
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
//... ...省略创建信道代码
//todo 声明交换器名称、类型、队列名称、绑定键(路由键)
String exchangeName = "cyan.confirm.topicexchange";
String exchangeType = "topic";
String queueName = "cyan.confirm.queue";
String routingKey = "cyan.confirm.#";
channel.exchangeDeclare(exchangeName,exchangeType,true,false,null);
channel.queueDeclare(queueName,true,false,false,null);
channel.queueBind(queueName,exchangeName,routingKey);
//todo 创建消费者
QueueingConsumer queueingConsumer = new QueueingConsumer(channel);
//todo 消费消息
channel.basicConsume(queueName,false,queueingConsumer);
while (true) {
QueueingConsumer.Delivery delivery = queueingConsumer.nextDelivery();
System.out.println("消费端在:"+System.currentTimeMillis()+"消费:"+new String(delivery.getBody()));
System.out.println("company:"+delivery.getProperties().getHeaders().get("company"));
System.out.println("location:"+delivery.getProperties().getHeaders().get("location"));
System.out.println("correlationId:"+delivery.getProperties().getCorrelationId());
}
}
3)ConfirmListener消息监听
public class CyanConfirmListener implements ConfirmListener {
/**
* 处理正常
* @param deliveryTag 消息唯一id
* @param multiple 是否批量
* @throws IOException
*/
@Override
public void handleAck(long deliveryTag, boolean multiple) {
System.out.println("当前时间:"+System.currentTimeMillis()+"CyanConfirmListener handleAck:"+deliveryTag);
}
/**
* 处理异常
* @param deliveryTag 消息唯一id
* @param multiple 是否批量
* @throws IOException
*/
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
System.out.println("CyanConfirmListener handleNack:"+deliveryTag);
}
}
四、RabbitMQ高级特性之消费端限流
1、什么是消费端限流
场景: 例如订单高峰期,在mq的broker上堆积了成千上万条消息没有处理,如此多的消息瞬间推送给消费者,消费者不能处理这么多消息,会导致消费者出现巨大压力,甚至服务器崩溃。
解决方案: RabbitMQ提供了一种qos(服务质量保证),即在关闭了消费端自动ack的前提下,可以设置消息数的阀值,通过手动ack确认机制来避免大量消息同一时间抵达消费端
2、代码示例
1)消息生产者
public static void main(String[] args) throws IOException, TimeoutException {
//... ...省略创建信道代码
//todo 定义交换器名称、路由键、消息体
String exchangeName = "cyan.qos.direct";
String routingKey = "cyan.qos.key";
String msgBody = "你好cyan";
//todo 发送消息
for(int i=0;i<1000;i++) {
channel.basicPublish(exchangeName,routingKey,null,(msgBody+i).getBytes());
}
}
2)消息消费者
public static void main(String[] args) throws IOException, TimeoutException {
//... ...省略创建信道代码
//todo 声明交换器名称、类型、队列名称、绑定键(路由键)
String exchangeName = "cyan.qos.direct";
String exchangeType = "direct";
String queueName = "cyan.qos.queue";
String routingKey = "cyan.qos.key";
channel.exchangeDeclare(exchangeName,exchangeType,true,false,null);
channel.queueDeclare(queueName,true,false,false,null);
channel.queueBind(queueName,exchangeName,routingKey);
/**
* 限流设置
* prefetchSize:设置每条消息的大小,0表示不限制
* prefetchCount:标识每次推送多少条消息 一般是一条
* global:false标识消费级别的限流(true:标识channel的级别的,该级别尚未实现)
*/
channel.basicQos(0,1,false);
//todo 消费端限流 需要关闭消息自动签收
channel.basicConsume(queueName,false,new CyanQosConsumer(channel));
}
3)自定义消费端监听
public class CyanQosConsumer extends DefaultConsumer {
private Channel channel;
public CyanQosConsumer(Channel channel) {
super(channel);
this.channel = channel;
}
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("consumerTag:"+consumerTag);
System.out.println("envelope:"+envelope);
System.out.println("properties:"+properties);
System.out.println("body:"+new String(body));
//todo 手动签收(假如关闭手动签收,也关闭自动签收,那么消费端只会接收到一条消息)
channel.basicAck(envelope.getDeliveryTag(),false);
}
}
五、RabbitMQ高级特性之return listener机制
1、什么是ReturnListener机制
ReturnListener是用来处理一些不可路由的消息,消息能够投递到broker的交换器上,但是交换器根据routing key路由不到任何队列上,ReturnListener用来处理这种不可达的消息,若在消息生产端设置mandotory为true,那么就会调用生产端的ReturnListener来处理,若在消息生产端设置mandotory为false(默认值也是false),那么就会自动删除消息
2、代码示例
1)消息生产者
public static void main(String[] args) throws IOException, TimeoutException {
//... ...省略创建信道代码
//todo 定义交换器名称、路由键、消息体
String exchangeName = "cyan.retrun.direct";
String okRoutingKey = "cyan.retrun.key.ok";
String errorRoutingKey = "cyan.retrun.key.error";
String msgContext = "你好 青子...."+System.currentTimeMillis();
String errorMsg1 = "你好 青子 mandotory为false...."+System.currentTimeMillis();
String errorMsg2 = "你好 青子 mandotory为true...."+System.currentTimeMillis();
//todo 设置监听不可达消息
channel.addReturnListener(new CyanRetrunListener());
//todo 设置消息属性
Map<String,Object> tulingInfo = new HashMap<>();
tulingInfo.put("company","cyan");
tulingInfo.put("location","天津");
AMQP.BasicProperties basicProperties = new AMQP.BasicProperties().builder()
.deliveryMode(2)
.correlationId(UUID.randomUUID().toString())
.timestamp(new Date())
.headers(tulingInfo)
.build();
/**
* todo 发送消息
* mandatory:该属性设置为false,那么不可达消息就会被mq broker给删除掉,该属性设置为true,那么mq会调用我们得retrunListener来告诉我们业务系统说该消息不能成功发送.
*/
channel.basicPublish(exchangeName,okRoutingKey,true,basicProperties,msgContext.getBytes());
//todo 错误发送 mandotory为false
channel.basicPublish(exchangeName,errorRoutingKey,false,basicProperties,errorMsg1.getBytes());
//todo 错误发送 mandotory 为true
channel.basicPublish(exchangeName,errorRoutingKey,true,basicProperties,errorMsg2.getBytes());
}
2)消息消费者
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
//... ...省略创建信道代码
//todo 声明交换器名称、类型、队列名称、绑定键(路由键)
String exchangeName = "cyan.retrun.direct";
String exchangeType = "direct";
String queueName = "cyan.retrunlistener.queue";
String routingKey = "cyan.retrun.key.ok";
channel.exchangeDeclare(exchangeName,exchangeType,true,false,null);
channel.queueDeclare(queueName,true,false,false,null);
channel.queueBind(queueName,exchangeName,routingKey);
//todo 创建一个消费者
QueueingConsumer queueingConsumer = new QueueingConsumer(channel);
channel.basicConsume(queueName,true,queueingConsumer);
while (true) {
QueueingConsumer.Delivery delivery = queueingConsumer.nextDelivery();
System.out.println("接受的消息:"+new String(delivery.getBody()));
}
}
3)监听不可达消息
public class CyanRetrunListener implements ReturnListener {
@Override
public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("replyCode:"+replyCode);
System.out.println("replyText:"+replyText);
System.out.println("exchange:"+exchange);
System.out.println("routingKey:"+routingKey);
System.out.println("properties:"+properties);
System.out.println("msg body:"+new String(body));
}
}
六、RabbitMQ高级特性之消息过期
1、什么是消息过期
消息过期:消息本身设置了过期时间,或者队列设置了消息过期时间x-message-ttl,在过期时间内没有被消费即消息过期
2、如何设置消息过期时间
-
针对队列来说,可以使用x-message-ttl参数设置当前队列中所有消息的过期时间都是一样的,单位毫秒
-
针对单个消息来说,在发布消息时,可以使用Expiration参数来设置单个消息的过期时间,单位毫秒
3、代码示例
以队列设置过期时间为例,单个消息设置过期时间见RabbitMQ高级特性之死信队列中代码示例的消息生产者中的代码
public static void main(String[] args) throws IOException, TimeoutException {
//... ...省略创建信道代码
//todo 定义交换器名称、路由键、消息体
String exchangeName = "cyan.ttl.direct";
String routingKey = "cyan.ttl.key";
String queueName = "cyan.ttl.queue";
String msgBody = "你好cyan";
//todo 队列设置过期时间
Map<String,Object> queueArgs = new HashMap<>();
queueArgs.put("x-message-ttl",10000);
queueArgs.put("x-max-length",4);
channel.exchangeDeclare(exchangeName,"direct",true,false,null);
channel.queueDeclare(queueName,true,false,false,queueArgs);
channel.queueBind(queueName,exchangeName,routingKey);
//todo todo 发送消息
for(int i=0;i<10;i++) {
channel.basicPublish(exchangeName,routingKey,null,(msgBody+i).getBytes());
}
}
七、RabbitMQ高级特性之死信队列
1、什么是死信队列
在队列中的消息如果没有消费者消费,那么该消息就称为一个死信,那这个消息会被重新发送到另外一个exchange的队列中,后面这个队列就是死信队列
2、产生死信的原因
- 消息被拒绝(basic.reject/basic.nack)并且requeue=false
- 消息ttl过期
- 队列达到最大长度
死信队列也是一个正常的exchange,也会通过routing key绑定到具体的队列上
3、代码示例
1)消息生产者
public static void main(String[] args) throws IOException, TimeoutException {
//... ...省略创建信道代码
//todo 定义交换器名称、路由键、消息体
String nomalExchangeName = "cyan.nomaldlx.exchange";
String routingKey = "cyan.dlx.key";
String message = "我是测试的死信消息";
//todo 消息10秒没有被消费,那么就会转到死信队列上
AMQP.BasicProperties basicProperties = new AMQP.BasicProperties().builder()
.deliveryMode(2)
.expiration("10000")
.build();
//todo 发送消息
for(int i=0;i<100;i++) {
channel.basicPublish(nomalExchangeName,routingKey,basicProperties,message.getBytes());
}
}
2)消息消费者
public static void main(String[] args) throws IOException, TimeoutException {
//... ...省略创建信道代码
//todo 声明交换器名称、类型、队列名称、绑定键(路由键)
String nomalExchangeName = "cyan.nomaldlx.exchange";
String exchangeType = "topic";
String nomalqueueName = "cyan.nomaldex.queue";
String routingKey = "cyan.dlx.#";
//todo 声明死信队列交换器、队列名称
String dlxExhcangeName = "cyan.dlx.exchange";
String dlxQueueName = "cyan.dlx.queue";
//todo 声明队列
channel.exchangeDeclare(nomalExchangeName,exchangeType,true,false,null);
channel.queueDeclare(nomalqueueName,true,false,false,queueArgs);
channel.queueBind(nomalqueueName,nomalExchangeName,routingKey);
//todo 声明死信队列
channel.exchangeDeclare(dlxExhcangeName,exchangeType,true,false,null);
channel.queueDeclare(dlxQueueName,true,false,false,null);
channel.queueBind(dlxQueueName,dlxExhcangeName,"#");
//todo 正常队列上绑定死信队列
Map<String,Object> queueArgs = new HashMap<>();
queueArgs.put("x-dead-letter-exchange",dlxExhcangeName);
queueArgs.put("x-max-length",4);
channel.basicConsume(nomalqueueName,false,new DlxConsumer(channel));
}
3)自定义消费端监听
public class DlxConsumer extends DefaultConsumer {
private Channel channel;
public DlxConsumer(Channel channel) {
super(channel);
this.channel = channel;
}
public void handleDelivery(String consumerTag,Envelope envelope,AMQP.BasicProperties properties,byte[] body)throws IOException{
System.out.println("接受到消息:"+new String(body));
//todo 消费端拒绝签收,并且不支持重回队列,那么该条消息就是一条死信消息
channel.basicNack(envelope.getDeliveryTag(),false,false);
//channel.basicAck(envelope.getDeliveryTag(),false);
}
}
1、消息生产者 2、消息消费者 3、自定义消费者