二、RabbitMQ高级特性

297 阅读11分钟

一、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、自定义消费者