RabbitMq(三)高级特性

一、如何保证消息的幂等性

幂等: 幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同

生产者发送消息和消费者消费消息在和broker确认的过程中都有可能出现网络问题,导致消息重复发送或者消费。
一般由消息消费端来做幂等性控制,在消费消息的时候,获取业务消息的唯一字段,组装成key,通过redis的分布式锁来保证只消费一次

二、消息的confirm机制(签收监听)

消费者获取到消息的时候可以选择
1.签收信息(可以自动或手动签收)
2.拒收信息(可以选择是否重新入队)

生产者可以添加监听感知这个行为,从而决定是否需要重发

Confirm机制的实现步骤:
第一步:在channel 上开启确认模式 channel.confirmSelect();
第二步:在channel上添加监听,用来监听mq-server返回的应答

代码示例
生产者:

//设置消息投递模式(确认模式)
channel.confirmSelect();
/**
* 消息确认监听
*/
channel.addConfirmListener(new MyConfirmListener());

确认监听

public class MyConfirmListener implements ConfirmListener {

/**
* @param deliveryTag 唯一消息Id
* @param multiple:是否批量
*/
@Override
public void handleAck(long deliveryTag, boolean multiple) {
System.out.println("当前时间:"+ LocalDateTime.now() +",ConfirmListener handleAck:"+deliveryTag);
System.out.println("已收到producer发送消息,更新msg状态为发送成功.");
}

/**
* 处理异常
* @param deliveryTag
* @param multiple
*/
@Override
public void handleNack(long deliveryTag, boolean multiple) {
System.out.println("ConfirmListener handleNack:"+deliveryTag);
System.out.println("消息内容异常,更新msg状态为发送失败,稍后重新发送");
}
}

三、消息的路由监听

我们的消息生产者,通过把消息投递到exchange上,然后通过routingkey 把消息路由到某一个队列上,然后我们消费者通过队列消息侦听,然后进行消息消费处理.
以上会出现的情况
情况一: broker中根本没有对应的exchange交换机来接受该消息
情况二:消息能够投递到broker的交换机上,但是交换机根据routingKey 路由不到某一个队列上.

针对上述二种情况 我们就需要return listener来处理这种不可达的消息.
处理一;若在消息生产端 的mandatory设置为true 那么就会调用生产端ReturnListener 来处理,
处理二;若消息生产端的mandatory设置为false(默认值也是false) 那么mq-broker就会自动删除消息

/**
* 设置监听不可达消息
*/
channel.addReturnListener(new MyReturnListener());

//准备发送消息
String exchangeName = "yunji.retrun.direct";
String okRoutingKey = "yunji.retrun.key.ok";
String errorRoutingKey = "yunji.retrun.key.error";

//设置消息属性
Map<String,Object> header = new HashMap<>();
header.put("company","yunji");
header.put("location","深圳");

AMQP.BasicProperties basicProperties = new AMQP.BasicProperties().builder()
.deliveryMode(2)
.correlationId(UUID.randomUUID().toString())
.timestamp(new Date())
.headers(header)
.build();

String msgContext = "Hello, YJ...."+ LocalDateTime.now();

/**
* 发送消息
* mandatory:该属性设置为false,那么不可达消息就会被mq broker给删除掉
* :true,那么mq会调用我们得retrunListener 来告诉我们业务系统 说该消息
* 不能成功发送.
*/
channel.basicPublish(exchangeName,okRoutingKey,true,basicProperties,msgContext.getBytes());


String errorMsg1 = "Hello,YJ mandotory为false...."+System.currentTimeMillis();
//错误发送 mandotory为false
channel.basicPublish(exchangeName,errorRoutingKey,false,basicProperties,errorMsg1.getBytes());

String errorMsg2 = "Hello,YJ mandotory为true...."+System.currentTimeMillis();
//错误发送 mandotory 为true
channel.basicPublish(exchangeName,errorRoutingKey,true,basicProperties,errorMsg2.getBytes());
public class MyReturnListener implements ReturnListener {
@Override
public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) {
System.out.println("消息路由失败,找不到对应的交换机和队列,请检查后重新发送>>>>>>>>>>>>>>>");
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));
}
}

四、自定义消费行为

com.rabbitmq.client.Consumer接口定义了消费消息的所有业务场景,包括消费成功、取消、中断、中断恢复等。
com.rabbitmq.client.DefaultConsumer是Consumer接口的默认实现,所有实现方法默认为空,自定义消费监听可以extends DefaultConsumer 来针对性的重写需要的方法

public class MyConsumer extends DefaultConsumer {

public MyConsumer(Channel channel) {
super(channel);
}

@Override
public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body) {
System.out.println("consumerTag:"+consumerTag);
System.out.println("envelope:"+envelope);
System.out.println("properties:"+properties);
System.out.println("body:"+new String(body));
}
}

生产者

//声明交换机
String exchangeName = "yunji.customconsumer.direct";
String exchangeType = "direct";
channel.exchangeDeclare(exchangeName,exchangeType,true,false,null);

//声明队列
String queueName = "yunji.customconsumer.queue";
channel.queueDeclare(queueName,true,false,false,null);

//交换机绑定队列
String routingKey = "yunji.customconsumer.key";
channel.queueBind(queueName,exchangeName,routingKey);

channel.basicConsume(queueName,new MyConsumer(channel));

五、消费端限流

场景:首先,我们迎来了订单的高峰期,在mq的broker上堆积了成千上万条消息没有处理,这个时候,我们随便打开了消费者,就会出现如此多的消息瞬间推送给消费者,我们的消费者不能处理这么多消息 就会导致消费者出现巨大压力,甚至服务器崩溃
解决方案:
rabbitmq 提供一个qos(服务质量保证),也就是在关闭了消费端的自动ack的前提下,我们可以设置阈值(出队)的消息数没有被确认(手动确认),那么就不会推送消息过来.
限流的级别(consumer级别或者是channel级别)

代码演示:
生产者:

/**
* 限流设置: prefetchSize:每条消息大小的设置
* prefetchCount:标识每次推送多少条消息 一般是一条
* global:false标识channel级别的 true:标识消费的级别的
*/
channel.basicQos(0,1,false);

/**
* 消费端限流 需要关闭消息自动签收
*/
channel.basicConsume(queueName,false,new QosConsumer(channel));
public class QosConsumer extends DefaultConsumer {

public QosConsumer(Channel channel) {
super(channel);
}

@Override
public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body) {
System.out.println("consumerTag:"+consumerTag);
System.out.println("envelope:"+envelope);
System.out.println("properties:"+properties);
System.out.println("body:"+new String(body));

/**
* multiple:false标识不批量签收
*/
try {
Thread.sleep(new Random().nextInt(3000));
getChannel().basicAck(envelope.getDeliveryTag(),false);
}catch (Exception e){

}
}
}

六、消费端ACK确认

消费端的ack类型:自动ack 和手动ack
做消息限流的时候,我们需要关闭自动ack 然后进行手动ack的确认,若我们业务出现了问题,我们就可以进行nack
重回队列
当消费端进行了nack的操作的时候,我们可以通过设置来进行对消息的重回队列的操作(但是一般我们不会设置重回队列的操作)

@Override
public void handleDelivery(String consumerTag,Envelope envelope,AMQP.BasicProperties properties,byte[] body) throws IOException {
try{
//模拟业务
Integer mark = (Integer) properties.getHeaders().get("mark");
if(mark != 0 ) {
System.out.println("消费消息:"+new String(body));
getChannel().basicAck(envelope.getDeliveryTag(),false);
}else{
throw new RuntimeException("模拟业务异常");
}
}catch (Exception e) {
System.out.println("异常消费消息:"+new String(body));
//重回队列
// getChannel().basicNack(envelope.getDeliveryTag(),false,true);
//不重回队列
getChannel().basicNack(envelope.getDeliveryTag(),false,false);
}
}

七、死信队列

1.1)什么是死信?
就是在队列中的消息如果没有消费者消费,那么该消息就成为一个死信,那这个消息被重新发送到另外一个exchange上的话,那么后面这个exhcange绑定的队列就是死信队列

1.2)消息变成死信的几种情况
a)消息被拒绝
(basic.reject/basic.nack)并且requeue(重回队列)的属性设置为 false 表示不需要重回队列,那么该消息就是一个死信消息

b)消息TTL过期
消息本身设置了过期时间,或者队列设置了消息过期时间x-message-ttl

c)队列达到最大长度
比如队列最大长度是3000 ,那么3001消息就会被送到死信队列上

死信队列也是一个正常的queue,也会通过routingkey 绑定到具体的队列上。

//4:通过连接创建channel
Channel channel = connection.createChannel();
String exchangeType = "topic";

//声明死信队列
String dlxExchangeName = "yunji.dlx.exchange";
String dlxQueueName = "yunji.dlx.queue";

channel.exchangeDeclare(dlxExchangeName,exchangeType,true,false,null);
channel.queueDeclare(dlxQueueName,true,false,false,null);
channel.queueBind(dlxQueueName,dlxExchangeName,"#");

//声明正常的队列
String normalExchangeName = "yunji.nomaldlx.exchange";
String normalQueueName = "yunji.nomaldex.queue";

channel.exchangeDeclare(normalExchangeName,exchangeType,true,false,null);
Map<String,Object> queueArgs = new HashMap<>();
//正常队列上绑定死信队列
queueArgs.put("x-dead-letter-exchange",dlxExchangeName);
queueArgs.put("x-max-length",4);
channel.queueDeclare(normalQueueName,true,false,false,queueArgs);
channel.queueBind(normalQueueName,normalExchangeName,"yunji.dlx.#");

channel.basicConsume(normalQueueName,false,new DlxConsumer(channel));
public class DlxConsumer extends DefaultConsumer {


public DlxConsumer(Channel channel) {
super(channel);
}

@Override
public void handleDelivery(String consumerTag,Envelope envelope,AMQP.BasicProperties properties,byte[] body)throws IOException
{
System.out.println("接受到消息:"+new String(body));
//消费端拒绝签收,并且不支持重回队列,那么该条消息就是一条死信消息
// getChannel().basicNack(envelope.getDeliveryTag(),false,false);
getChannel().basicAck(envelope.getDeliveryTag(),false);
}
}

TTL过期进入死信队列

//定义交换机的名称
String exchangeName = "yunji.ttl.direct";

String routingKey = "yunji.ttl.key";

String queueName = "yunji.ttl.queue";

//申明交换机
channel.exchangeDeclare(exchangeName,"direct",true,false,null);

//申明队列
Map<String,Object> queueArgs = new HashMap<>();
//设置队列中的消息10s没有被消费就会过期
queueArgs.put("x-message-ttl",10000);
//队列的长度
queueArgs.put("x-max-length",4);
channel.queueDeclare(queueName,true,false,false,queueArgs);

//绑定
channel.queueBind(queueName,exchangeName,routingKey);

String msgBody = "hello, YJ";
for(int i=0;i<10;i++) {
channel.basicPublish(exchangeName,routingKey,null,(msgBody+i).getBytes());
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值