RabbitMQ 高级特性

一,消息可靠性投递

在使用 RabbitMQ 的时候,作为消息发送方希望杜绝任何消息丢失或者投递失败场景。 RabbitMQ 为我们提供了两种方式用来控制消息的投递可靠性模式。
  • confifirm 确认模式
  • return 退回模式
rabbitmq 整个消息投递的路径为:
producer ---> rabbitmq broker ---> exchange ---> queue ---> consumer
rabbitmq broker:简单来说就是消息队列服务器实体。中文意思:中间件。接受客户端连接,实现AMQP消息队列和路由功能的进程。一个broker里可以开设多个vhost,用作不同用户的权限分离。

  • 消息从 producer exchange 则会返回一个 confirmCallback 。
  • 消息从 exchange queue 投递失败则会返回一个 returnCallback

通过 confirmCallback 判定 消息是否成功到达exchange

因为returnCallback 是 启动消息失败返回,比如路由不到队列时触发回调 

 我们将利用这两个 callback 控制消息的可靠性投递

1,confirm确认模式代码实现

代码实现:

        1,创建maven 工程,消息的生产者工程,项目模块名称:rabbitmq-producer-spring

        2,添加依赖

<dependencies> 
    <dependency> 
        <groupId>org.springframework</groupId> 
        <artifactId>spring‐context</artifactId> 
        <version>5.1.7.RELEASE</version> 
    </dependency> 
    <dependency> 
        <groupId>org.springframework.amqp</groupId> 
        <artifactId>spring‐rabbit</artifactId> 
        <version>2.1.8.RELEASE</version> 
    </dependency> 
    <dependency> 
        <groupId>junit</groupId> 
        <artifactId>junit</artifactId> 
        <version>4.12</version> 
    </dependency> 
    <dependency> 
        <groupId>org.springframework</groupId> 
        <artifactId>spring‐test</artifactId> 
        <version>5.1.7.RELEASE</version> 
    </dependency> 
</dependencies> 
<build> 
    <plugins> 
        <plugin> 
            <groupId>org.apache.maven.plugins</groupId> 
            <artifactId>maven‐compiler‐plugin</artifactId> 
            <version>3.8.0</version> 
        <configuration> 
            <source>1.8</source> 
            <target>1.8</target> 
        </configuration> 
        </plugin> 
    </plugins> 
</build>

3,在 resources 目录下创建 rabbitmq.properties 配置文件,添加链接RabbitMQ相关信息

rabbitmq.host=172.16.98.133 
rabbitmq.port=5672 
rabbitmq.username=guest 
rabbitmq.password=guest 
rabbitmq.virtual‐host=/
4. resources 目录下创建 spring-rabbitmq-producer.xml 配置文件,添加以下配置
<?xml version="1.0" encoding="UTF‐8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema‐instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:rabbit="http://www.springframework.org/schema/rabbit" xsi:schemaLocation="http://www.springframework.org/schema/beans     http://www.springframework.org/schema/beans/spring‐beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring‐context.xsd http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring‐rabbit.xsd"> 

<!‐‐加载配置文件‐‐> 
<context:property‐placeholder location="classpath:rabbitmq.properties"/> 

<!‐‐ 定义rabbitmq connectionFactory 1. 设置 publisher‐confirms="true" ‐‐> <rabbit:connection‐factory id="connectionFactory" 
                           host="${rabbitmq.host}"
                           port="${rabbitmq.port}" 
                           username="${rabbitmq.username}"                                 
                           password="${rabbitmq.password}" 
                           virtual‐host="${rabbitmq.virtual‐host}" 
                           publisher‐confirms="true" /> 

<!‐‐定义管理交换机、队列‐‐> 
<rabbit:admin connection‐factory="connectionFactory"/> 

<!‐‐定义rabbitTemplate对象操作可以在代码中方便发送消息‐‐> 
<rabbit:template id="rabbitTemplate" connection‐factory="connectionFactory"/> 

<!‐‐2. 消息可靠性投递(生产端)‐‐> 
<rabbit:queue id="test_queue_confirm" name="test_queue_confirm"></rabbit:queue>     
   <rabbit:direct‐exchange name="test_exchange_confirm"> 
        <rabbit:bindings> 
            <rabbit:binding queue="test_queue_confirm" key="confirm"> 
            </rabbit:binding> 
        </rabbit:bindings> 
    </rabbit:direct‐exchange> 
</beans>

或者配置yml 文件

5, 编写测试代码

@RunWith(SpringJUnit4ClassRunner.class) 
@ContextConfiguration(locations = "classpath:spring‐rabbitmq‐producer.xml") 
public class ProducerTest { 

@Autowired 
private RabbitTemplate rabbitTemplate; 
/**
* 确认模式: 
* 步骤: 
* 1. 确认模式开启:ConnectionFactory中开启publisher‐confirms="true" 
* 2. 在rabbitTemplate定义ConfirmCallBack回调函数 
*/ 

@Test 
public void testConfirm() { 

    //2. 定义回调 ** 
    rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() { 

        /***
         * @param correlationData 相关配置信息 
         * @param ack exchange交换机 是否成功收到了消息。true 成功,false代表失败 
         * @param cause 失败原因 
        */ 
    @Override public void confirm(CorrelationData correlationData, boolean ack, String cause) { 

        System.out.println("confirm方法被执行了...."); 

        if (ack) { 
            //接收成功 
            System.out.println("接收成功消息" + cause); 
        } else { 
            //接收失败 
            System.out.println("接收失败消息" + cause); 
            //做一些处理,让消息再次发送。 
        } 
    } 
}); 

//3. 发送消息 
rabbitTemplate.convertAndSend("test_exchange_confirm111", "confirm", "message confirm...."); 
    } 
}


@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class confirmTest4 implements RabbitTemplate.ConfirmCallback {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 确认模式:
     * 步骤:
     * 1,确认模式开启:在yml 配置文件里配置 publisher-confirms: true
     * 2, 在rabbitTemplate 定义ConfirmCallBack 回调函数
     */

    @Test
    public void testConfirms() {
        rabbitTemplate.setConfirmCallback(new confirmTest4());

        Map<String, String> msg = new HashMap<>();
        msg.put("pageId", "5abefd525b05aa293098fca6");
        //转成json串
        String jsonString = JSON.toJSONString(msg);
        rabbitTemplate.convertAndSend("ex_routing_cms_postpage", "5abefd525b05aa293098fca6", jsonString);
    }
    /**
     *
     * @param correlationData 相关配置信息
     * @param ack exchange交换机 是否成功收到了消息,true成功,false代表失败
     * @param cause 失败原因
     */

    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        System.out.println("confirm 方法开始执行..............");

        if (ack) {
            //接收成功
            System.out.println("接收成功的消息:" + cause);
        } else {
            //接收失败
            System.out.println("接收失败消息:" + cause);
            //接收失败后,我们做一些处理,让消息再次发送,达到消息可靠性传递
        }
    }

其中重写confirm 方法里的参数,

        correlationData:消息唯一标识

        ack:确认结果

        cause:失败原因

 这是发送消息到queue 成功后,展示的内容,

但是如果发送失败是这样的

 当发送失败是可以配置return 退回模式代码。

2, return退回模式代码实现

回退模式:  当消息发送给EXchange 后,Exchange 路由到Queue 失败是才会执行ReturnCallBack,具体实现如下:

        1,在Spring-rabbitmq-producer.xml 配置文件,在rabbit:connection-factory 节点添加配置

publisher‐returns="true"

或者 在application.yml 中设置

设置交换机处理失败消息的模式,两种方法,一种写在代码里,一种配置文件

1, 配置文件

 2,代码方式:

/**
* 步骤:
* 1. 开启回退模式:publisher‐returns="true"
* 2. 设置ReturnCallBack
* 3. 设置Exchange处理消息的模式:
* 1. 如果消息没有路由到Queue,则丢弃消息(默认)
* 2. 如果消息没有路由到Queue,返回给消息发送方ReturnCallBack
*/
@Test
public void testReturn() {

    //设置交换机处理失败消息的模式
    rabbitTemplate.setMandatory(true);

    //2.设置ReturnCallBack
    rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {

    /**
    *
    * @param message 消息对象
    * @param replyCode 错误码
    * @param replyText 错误信息
    * @param exchange 交换机
    * @param routingKey 路由键
    */
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText,
    String exchange, String routingKey) {
        
        System.out.println("return 执行了....");
        System.out.println(message);
        System.out.println(replyCode);
        System.out.println(replyText);
        System.out.println(exchange);
        System.out.println(routingKey);
        //处理
    }
});

    //3. 发送消息
    rabbitTemplate.convertAndSend("test_exchange_confirm", "confirm", "message
    confirm....");
}

package com.xuecheng.manage_cms.returns;

import com.alibaba.fastjson.JSON;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.util.HashMap;
import java.util.Map;


@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class ReturnTest {

    @Autowired
    private RabbitTemplate rabbitTemplate;



    @Test
    public void testReturn() {

        rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
            /**
             *
             * @param message 消息对象
             * @param replyCode 错误码
             * @param replyText 错误信息
             * @param exchange 交换机
             * @param routingKey 路由键
             */
            @Override
            public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
                System.out.println("return 执行了..........");
                System.out.println("message = " + message);
                System.out.println("replyCode = " + replyCode);
                System.out.println("replyText = " + replyText);
                System.out.println("exchange = " + exchange);
                System.out.println("routingKey = " + routingKey);

            }
        });

        Map<String, String> msg = new HashMap<>();
        msg.put("pageId", "5abefd525b05aa293098fca6");
        //转成json串
        String jsonString = JSON.toJSONString(msg);
        rabbitTemplate.convertAndSend("ex_routing_cms_postpage", "5abefd525b05aa293098fca61", jsonString);
    }
}

设置routingKey为一个不符合规则的key,观察控制台打印结果。

returnedMessage方法中参数

        消息主体message : message
        消息主体 message : replyCode
        描述:replyText
        消息使用的交换器 exchange : exchange
        消息使用的路由键 routing : routingKey

总结

对于确认模式

设置 ConnectionFactory publisher-confifirms="true" 开启 确认模式。
使用 rabbitTemplate.setConfifirmCallback 设置回调函数。当消息发送到 exchange 后回调 confiirm方法。在方法中判断 ack ,如果为 true ,则发送成功,如果为 false ,则发送失败,需要处理。
对于退回模式
设置 ConnectionFactory publisher-returns="true" 开启 退回模式。
使用 rabbitTemplate.setReturnCallback 设置退回函数,当消息从 exchange 路由到 queue 失败后,如果设置
rabbitTemplate.setMandatory(true) 参数,则会将消息退回给 producer 。并执行回调函数
returnedMessage

RabbitMQ 中也提供了事务机制,但是性能较差,此处不做讲解。
使用 channel 列方法,完成事务控制:
txSelect(), 用于将当前 channel 设置成 transaction 模式
txCommit() ,用于提交事务
txRollback(), 用于回滚事务

 二,Consumer ACK(消息接收确认

消息消费者如何通知 Rabbit 消息消费成功?

ackAcknowledge,确认。 表示消费端收到消息后的确认方式。

有三种确认方式:
自动确认: acknowledge=" none "
手动确认: acknowledge=" manual "
根据异常情况确认: acknowledge=" auto " ,(这种方式使用麻烦,不作讲解)
其中自动确认是指,当消息一旦被 Consumer 接收到,则自动确认收到,并将相应 message RabbitMQ 的消息 缓存中移除。但是在实际业务处理中,很可能消息接收到,业务处理出现异常,那么该消息就会丢失。
如果设置了手动确认方式,则需要在业务处理成功后,调用 channel.basicAck(),手动签收,如果出现异常,则调 用 channel.basicNack() 方法,让其自动重新发送消息

代码实现

1. 创建 maven 工程,消息的消费者工程,项目模块名称: rabbitmq-consumer-spring
2. 添加依赖
<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring‐context</artifactId>
        <version>5.1.7.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.amqp</groupId>
        <artifactId>spring‐rabbit</artifactId>
        <version>2.1.8.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring‐test</artifactId>
        <version>5.1.7.RELEASE</version>
    </dependency>
</dependencies>
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven‐compiler‐plugin</artifactId>
            <version>3.8.0</version>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
            </configuration>
        </plugin>        
    </plugins>
</build>
3. resources 目录下创建 rabbitmq.properties 配置文件,添加链接 RabbitMQ 相关信息
rabbitmq.host = 172.16.98.133
rabbitmq.port = 5672
rabbitmq.username = guest
rabbitmq.password = guest
rabbitmq.virtual‐host = /
4. resources 目录下创建 spring-rabbitmq-consumer.xml 配置文件,添加以下配置

<?xml version="1.0" encoding="UTF‐8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema‐instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:rabbit="http://www.springframework.org/schema/rabbit"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring‐beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring‐context.xsd
http://www.springframework.org/schema/rabbit
http://www.springframework.org/schema/rabbit/spring‐rabbit.xsd">

<!‐‐加载配置文件‐‐>
<context:property‐placeholder location="classpath:rabbitmq.properties"/>

<!‐‐ 定义rabbitmq connectionFactory ‐‐>
<rabbit:connection‐factory id="connectionFactory" 
                           host="${rabbitmq.host}"
                           port="${rabbitmq.port}"
                           username="${rabbitmq.username}"
                           password="${rabbitmq.password}"

                           virtual‐host="${rabbitmq.virtual‐host}"/>

<context:component‐scan base‐package="com.itheima.listener" />

<!‐‐定义监听器容器 添加 acknowledge="manual" 手动‐‐>
<rabbit:listener‐container connection‐factory="connectionFactory" acknowledge="manual"
>
<rabbit:listener ref="ackListener" queue‐names="test_queue_confirm">
</rabbit:listener>
</rabbit:listener‐container>
</beans>

5. 编写 ackListener 监听类实现 ChannelAwareMessageListener 接口

/**
 * Consumer ACK机制:
 * 1. 设置手动签收。acknowledge="manual"
 * 2. 让监听器类实现ChannelAwareMessageListener接口
 * 3. 如果消息成功处理,则调用channel的 basicAck()签收
 * 4. 如果消息处理失败,则调用channel的basicNack()拒绝签收,broker重新发送给consumer
 */
@Component
public class ackListener implements ChannelAwareMessageListener {

    @RabbitListener(bindings = @QueueBinding(
            exchange = @Exchange(value = "ex_routing_cms_postpage",type = "direct"),
            value = @Queue(value = "queue_cms_postpage_01",durable = "true"),
            key = "#.#"
    ))

    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();

        try {
            //1, 接收转换消息
            System.out.println("接收消息内容:"+new String(message.getBody()));
            //2, 处理业务逻辑
            System.out.println("处理业务逻辑");
            //int i = 2/0 ; // 代码运行错误
            //3,手动签收
            channel.basicAck(deliveryTag,true);
        }catch (Exception e){
            e.printStackTrace();
            System.out.println("代码执行错误了。。。。。。");
            //4,代码出现错误后,设置拒绝签收,重新发送
            //第三个参数:requeue: 消息重回队列,如何设置为true,则消息重新回到queue,broker会重新发送该消息给消费端
            channel.basicNack(deliveryTag,true,true);
            //了解
            //channel.basicReject(deliveryTag,true);
        }
    }
}
上面的
    @RabbitListener(bindings = @QueueBinding(
            exchange = @Exchange(value = "ex_routing_cms_postpage",type = "direct"),
            value = @Queue(value = "queue_cms_postpage_01",durable = "true"),
            key = "#.#"
    ))
是监听注解
消息发送,通过ack 可以知道 Rabbit 消息消费成功。
如果消费者接受消息后,执行代码失败,可让消息重回队列,重新发送消息给消费端。

小结:

  • rabbit:listener-container标签中设置acknowledge属性,设置ack方式 none:自动确认,manual:手动确认
  • 如果在消费端没有出现异常,则调用channel.basicAck(deliveryTag,false);方法确认签收消息
  • 如果出现异常,则在catch中调用 basicNackbasicReject,拒绝消息,让MQ重新发送消息。
如何保证消息的高可靠性传输?
1. 持久化
• exchange 要持久化
• queue 要持久化
• message 要持久化
2. 生产方确认 Confifirm
3. 消费方确认 Ack
4.Broker 高可用

三,消费端限流

 

        代码实现:

        1, 编写一个监听类,保证当前的监听类的消息处理机制是ACK(手动方式)

@Component 
public class QosListener implements ChannelAwareMessageListener { 

    @Override 
    public void onMessage(Message message, Channel channel) throws Exception { 

        Thread.sleep(1000); 
        
        //1.获取消息 
        System.out.println(new String(message.getBody())); 

        //2. 处理业务逻辑 
        //3. 签收 
        channel.basicAck(message.getMessageProperties().getDeliveryTag(),true); 
    } 
}

2,在配置文件的listener-container 配置属性中添加配置(或者用@Confirguration)

<rabbit:listener‐container connection‐factory = "connectionFactory" acknowledge = "manual"
prefetch = "1" >
配置说明:
perfetch = 1, 表示消费端每次从 mq拉去一条消息来消费,直到手动确认消费完毕后,才会继续拉去下 一条消息。


/**
 * Consumer ACK机制:
 * 1. 设置手动签收。acknowledge="manual"
 * 2. 让监听器类实现ChannelAwareMessageListener接口
 * 3. 如果消息成功处理,则调用channel的 basicAck()签收
 * 4. 如果消息处理失败,则调用channel的basicNack()拒绝签收,broker重新发送给consumer
 */
@Component
public class ackListener implements ChannelAwareMessageListener {

    @RabbitListener(bindings = @QueueBinding(
            exchange = @Exchange(value = "ex_routing_cms_postpage",type = "direct"),
            value = @Queue(value = "queue_cms_postpage_01",durable = "true"),
            key = "#.#"
    ))
    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();

        try {
            System.out.println("一个进来了");
            Thread.sleep(10000);
            System.out.println("一个结束了");
            channel.basicAck(deliveryTag,true);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

总结:

rabbit:listener-container 中配置 prefetch 属性设置消费端一次拉取多少消息
消费端的确认模式一定为手动确认。 acknowledge="manual"

四,TLL(TIME TO LIVE)

有两种设置过期方式:

        1,设置队列过期

        2,设置消息过期

存活时间/过期时间: 当消息到达存活时间后,还没有被消费,会被自动清除.

RabbitMQ 可以对消息设置过期时间,也可以对整个队列(Queue) 设置过期时间.

代码实现:

        1,设置队列的过期时间:

  •         在消息的生产方,在spring-rabbitmq-producer.xml配置文件中,添加如下配置
<!‐‐ttl‐‐>
<rabbit:queue name="test_queue_ttl" id="test_queue_ttl">
    <!‐‐设置queue的参数‐‐>
    <rabbit:queue‐arguments>
        <!‐‐x‐message‐ttl指队列的过期时间‐‐>
        <entry key="x‐message‐ttl" value="100000" value‐type="java.lang.Integer"/>
    </rabbit:queue‐arguments>
</rabbit:queue>


<rabbit:topic‐exchange name="test_exchange_ttl" >
    <rabbit:bindings>
        <rabbit:binding pattern="ttl.#" queue="test_queue_ttl"></rabbit:binding>
    </rabbit:bindings>
</rabbit:topic‐exchange>
  •  或设置 RabbitMqConfig.java
   //声明队列
    @Bean(QUEUE_CMS_POST)
    public Queue QUEUE_CMS_POST(){

        Map<String, Object> arguments = new HashMap<String, Object>();
        arguments.put("x-message-ttl", 5000);

        return QueueBuilder.durable(queue_cms_postpage_name).withArgument("x-message-ttl",5000).build();
    }

        交换机需要配置topic 模式

   /**
     * 交换机配置使用topic 类型
     */
    @Bean(EX_ROUTING_CMS_POST)
    public Exchange EX_ROUTING_CMS_POST(){
        return ExchangeBuilder.topicExchange(EX_ROUTING_CMS_POST).durable(true).build();
    }

设置生效后,rabbitMq管理页面会出现TTL 图标

  

这里经常出现 rabbitmq.client.ShutdownSignalException: channel error; protocol method: #method<channel.close>(repl 问题,好好排查,能解决掉

https://mp.csdn.net/mp_blog/creation/editor/119564708

2 编写发送消息测试方法

@Test 
public void testTtl() { 

    for (int i = 0; i < 10; i++) { 
    // 发送消息 
        rabbitTemplate.convertAndSend("test_exchange_ttl", "ttl.hehe", "message ttl....");         
    } 
}
测试结果:当消息发送成功后,过 10s 后在 RabbitMQ的管理控制台会看到消息会自动删除。

设置单个消息的过期时间

        编写代码测试,并且设置队列的过期时间为100s,单个消息的过期时间为5s,

@Test 
public void testTtl() { 

    // 消息后处理对象,设置一些消息的参数信息 
    MessagePostProcessor messagePostProcessor = new MessagePostProcessor() { 

        @Override 
        public Message postProcessMessage(Message message) throws AmqpException { 
    
        //1.设置message的信息 
        message.getMessageProperties().setExpiration("5000");

        //消息的过期时间 
        //2.返回该消息 
        return message; 
    } 
};

    //消息单独过期 
    rabbitTemplate.convertAndSend("test_exchange_ttl", "ttl.hehe", "message ttl....",messagePostProcessor); 

    for (int i = 0; i < 10; i++) { 
        if(i == 5){ 
            //消息单独过期 
            rabbitTemplate.convertAndSend("test_exchange_ttl", "ttl.hehe", "message ttl....",messagePostProcessor); 
        }else{
            //不过期的消息 
            rabbitTemplate.convertAndSend("test_exchange_ttl", "ttl.hehe", "message ttl...."); 
    } 
} 
}    

消息过期就是在发送消息后面加个参数,  MessagePostProcessor 类, 这个类里配置过期时间,那么这条消息过期时间就不一样

如果设置了消息的过期时间,也设置了队列的过期时间,它以时间短的为准。
        
        队列过期后,会将队列所有消息全部移除。
        
        消息过期后,只有消息在队列顶端,才会判断其是否过期(移除掉 )

总结:

设置队列过期时间使用参数: x-message-ttl ,单位: ms( 毫秒 ) ,会对整个队列消息统一过期。
设置消息过期时间使用参数: expiration 。单位: ms( 毫秒 ),当该消息在队列头部时(消费时),会单独判断这一消息是否过期。
如果两者都进行了设置,以时间短的为准。

五,死信队列

死信队列,英文缩写: DLX Dead Letter Exchange(死信交换机) 当消息成为 Dead message 后,可以被重新 发送到另一个交换机,这个交换机就是 DLX

死信队列:没有被及时消费的消息存放的队列

 消息成为死信的几种情况:

  • 消费者拒收消息(basic.reject/ basic.nack) ,并且没有重新入队 requeue=false
  • 消息在队列中未被消费,且超过队列或者消息本身的过期时间TTL(time-to-live)
  • 队列的消息长度达到极限
  • 结果:消息成为死信后,如果该队列绑定了死信交换机,则消息会被死信交换机重新路由到死信队列

队列绑定死信交换机步骤:

1,新建死信交换机(和普通交换机一样)

2,新建死信队列(和普通队列一样,名称:dead_queue)

3,死信交换机和死信队列绑定

 4,创建一个新的队列(名称:product_qeueu),设置过期时间,绑定死信交换机

测试:直接web控制台往product_qeueu发送消息即可,会看到消息先是在product_qeueu队列停留10秒(因为没有消费者消费),然后该消息从product_qeueu移入到dead_queue。

代码实现:

        1,在消息的生产方中,在application.yml 中 配置

 

                声明正常的队列(producer_queue)和交换机(producer_exchange)

package com.tdrc.common.core.rabbitmq;
 
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
import java.util.HashMap;
import java.util.Map;
 
/**
 * @author dpf
 * @version 1.0
 * @date 2020-6-22 9:52
 * @instruction ...
 */
@Configuration
public class RabbitExChangeConfig {
    /**
     * 业务交换机
     */
    public static final String DESTINATION_NAME = "rabbitMq_direct";
    /**
     * 业务队列名称
     */
    public static final String SMS_QUEUE = "Sms_msg";
    /**
     * 死信队列交换机名称
     */
    public static final String   DEAD_LETTER_EXCHANGE_NAME="deadLetter_direct";
    /**
     * 死信队列名称
     */
    public static final String   DEAD_LETTER_QUEUE = "deadLetter_queue";
    /**
     * RouteKey
     */
    public static final String SMS_ROUTING_KEY = "sms";
    /**
     * 配置死信交换机
     * @return
     */
    @Bean
    public DirectExchange  deadLetterDirectExchange(){
        return new DirectExchange(DEAD_LETTER_EXCHANGE_NAME);
    }
    /**
     * 配置死信队列
     * @return
     */
    @Bean
    public Queue  deadLetterQueue(){
        return new Queue(DEAD_LETTER_QUEUE);
    }
    /**
     * 绑定死信队列和死信交换机
     * @return
     */
    @Bean
    Binding deadLetterBindingDirect() {
        return BindingBuilder.bind(deadLetterQueue()).to(deadLetterDirectExchange()).with(SMS_ROUTING_KEY);
    }
    /**
     * 配置队列
     * @return
     */
    @Bean
    public Queue smsDirectQueue() {
        Map<String, Object> args = new HashMap<>(16);
         // 队列消息过期时间
         args.put("x-message-ttl", 10000);
         args.put("x-dead-letter-exchange", DEAD_LETTER_EXCHANGE_NAME);
         args.put("x-dead-letter-routing-key", SMS_ROUTING_KEY);
       //  args.put("x-expires", 5000);队列过期时间
       // args.put("x-max-length",5 );
        return new Queue(SMS_QUEUE, true,false,false,args);
    }
    /**
     * 配置交换机
     * @return
     */
    @Bean
    public DirectExchange directExchange() {
        return new DirectExchange(DESTINATION_NAME);
    }
 
    /**
     * 交换机与队列绑定
     * @return
     */
    @Bean
    Binding smsBindingDirect() {
        return BindingBuilder.bind(smsDirectQueue()).to(directExchange()).with(SMS_ROUTING_KEY);
    }
 
 
    @Bean
    public SimpleRabbitListenerContainerFactory simpleRabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
        SimpleRabbitListenerContainerFactory simpleRabbitListenerContainerFactory =
                new SimpleRabbitListenerContainerFactory();
        //这个connectionFactory就是我们自己配置的连接工厂直接注入进来
        simpleRabbitListenerContainerFactory.setConnectionFactory(connectionFactory);
        //这边设置消息确认方式由自动确认变为手动确认
        simpleRabbitListenerContainerFactory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
        //设置消息预取数量
        // simpleRabbitListenerContainerFactory.setPrefetchCount(1);
        return simpleRabbitListenerContainerFactory;
    }
    /**
     * 每个rabbitTemplate方法只可以有一个回调,不然会报错 only one ConfirmCallback is supported by each RabbitTemplate,解决办法是配成多利的
     *
     * @param connectionFactory
     * @return
     */
    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate template = new RabbitTemplate(connectionFactory);
        //成功回调
        template.setConfirmCallback(new Callback());
        // 开启mandatory模式(开启失败回调)
        template.setMandatory(true);
        //失败回调
        template.setReturnCallback(new Callback());
 
        return template;
    }
}

编写给业务交换机发短信的controller 

@Resource
private RabbitTemplate rabbitTemplate;
 
@GetMapping("/sendSms")
    private void sendSms() throws InterruptedException {
        String msg = "HelloWorld rabbitmq";
        for(Integer i=0;i<10;i++){
            CorrelationData correlationData = new CorrelationData(i.toString());
            rabbitTemplate.convertAndSend(RabbitExChangeConfig.DESTINATION_NAME, RabbitExChangeConfig.SMS_ROUTING_KEY, msg+i ,correlationData);
        }
 
    }

启动程序,用postman调用发送信息接口

测试结果:

1,启动程序前消息对列中无程序内创建的业务队列和死信队列。(还没有创建交换机和队列)

2,程序启动后出现业务交换机和死信交换机,业务队列和死信队列。

3,业务交换机有发送过去的消息,私信交换机现在没有

4,等设置的ttl 过期时间过后,死信队列中有了发送的消息,业务队列中没有了(因为过了消费时间并且一直没有消费)

程序中添加监听死信队列监听代码,看的会更详细

 @RabbitListener(queues = RabbitExChangeConfig.DEAD_LETTER_QUEUE, containerFactory = "simpleRabbitListenerContainerFactory")
    public void reciveDeadLetter(Message message, Channel channel, @Headers Map<String, Object> headers) throws IOException {
        long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
        try {
 
            System.out.println("死信队列消费者收到消息 : " + new String(message.getBody(), "UTF-8"));
            /**
             * 手动ack
             * deliveryTag:该消息的index
             * multiple:是否批量.true:将一次性ack所有小于deliveryTag的消息。
             */
            channel.basicAck(deliveryTag, false);
        } catch (Exception e) {
            //消息退回 (可以在可视化界面看到)
            //批量退回 退回之后重回消息队列 true  false的话就是丢弃这条信息,如果配置了死信队列,那这条消息会进入死信队列
            channel.basicNack(deliveryTag, false, true);
            //单条退回 channel.basicReject();
        }
    }

实际环境我们还需要对死信队列进行一个监听和处理,当然具体的处理逻辑和业务相关,这里只是简单演示死信队列是否生效。

六,延迟队列

1,什么是延迟队列

        ⼀种带有延迟功能的消息队列, Producer 将消息发送到消息队列服务端,但并不期望这条消息立马投递,而是推迟到在当前时间点之后的某⼀个时间投递到Consumer进行消费,该消息即定时消息。

2,应用场景

  • 通过消息触发⼀些定时任务,比如在某⼀固定时间点向用户发送提醒消息
  • 用户登录之后5分钟给用户做分类推送、用户多少天未登录给用户做召回推送;
  • 消息生产和消费有时间窗⼝要求:比如在天猫电商交易中超时未支付关闭订单的场景,在订单创建时会发送⼀条延时消息。这条消息将会在30分钟以后投递给消费者,消费者收到此消息后需要判断对应的订单是否已完成支付。 如支付未完成,则关闭订单。如已完成支付则忽略

3,实战

        需求           

                JD、淘系、天猫、拼多多电商平台,规定新注册的商家,审核通过后需要在【规定时间】内上架商品,否则冻结账号。

        代码实现:

                1,定时器

                2,延迟队列

                        注意:在RabbitMQ中并未提供延迟队列功能。但可以使用:TLL+死信队列组合实现延迟队列功能

         

1,在消息的生产方,rabbitmqConfig 中,添加如下配置:

        定义正常交换机和队列并绑定,定义死信交换机和队列,并绑定

package com.tdrc.common.core.rabbitmq;
 
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
import java.util.HashMap;
import java.util.Map;
 
/**
 * @author dpf
 * @version 1.0
 * @date 2020-6-22 9:52
 * @instruction ...
 */
@Configuration
public class RabbitExChangeConfig {
    /**
     * 业务交换机
     */
    public static final String DESTINATION_NAME = "rabbitMq_direct";
    /**
     * 业务队列名称
     */
    public static final String SMS_QUEUE = "Sms_msg";
    /**
     * 死信队列交换机名称
     */
    public static final String   DEAD_LETTER_EXCHANGE_NAME="deadLetter_direct";
    /**
     * 死信队列名称
     */
    public static final String   DEAD_LETTER_QUEUE = "deadLetter_queue";
    /**
     * RouteKey
     */
    public static final String SMS_ROUTING_KEY = "sms";
    /**
     * 配置死信交换机
     * @return
     */
    @Bean
    public DirectExchange  deadLetterDirectExchange(){
        return new DirectExchange(DEAD_LETTER_EXCHANGE_NAME);
    }
    /**
     * 配置死信队列
     * @return
     */
    @Bean
    public Queue  deadLetterQueue(){
        return new Queue(DEAD_LETTER_QUEUE);
    }
    /**
     * 绑定死信队列和死信交换机
     * @return
     */
    @Bean
    Binding deadLetterBindingDirect() {
        return BindingBuilder.bind(deadLetterQueue()).to(deadLetterDirectExchange()).with(SMS_ROUTING_KEY);
    }
    /**
     * 配置队列
     * @return
     */
    @Bean
    public Queue smsDirectQueue() {
        Map<String, Object> args = new HashMap<>(16);
         // 队列消息过期时间
         args.put("x-message-ttl", 10000);
         args.put("x-dead-letter-exchange", DEAD_LETTER_EXCHANGE_NAME);
         args.put("x-dead-letter-routing-key", SMS_ROUTING_KEY);
       //  args.put("x-expires", 5000);队列过期时间
       // args.put("x-max-length",5 );
        return new Queue(SMS_QUEUE, true,false,false,args);
    }
    /**
     * 配置交换机
     * @return
     */
    @Bean
    public DirectExchange directExchange() {
        return new DirectExchange(DESTINATION_NAME);
    }
 
    /**
     * 交换机与队列绑定
     * @return
     */
    @Bean
    Binding smsBindingDirect() {
        return BindingBuilder.bind(smsDirectQueue()).to(directExchange()).with(SMS_ROUTING_KEY);
    }
 
 
    @Bean
    public SimpleRabbitListenerContainerFactory simpleRabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
        SimpleRabbitListenerContainerFactory simpleRabbitListenerContainerFactory =
                new SimpleRabbitListenerContainerFactory();
        //这个connectionFactory就是我们自己配置的连接工厂直接注入进来
        simpleRabbitListenerContainerFactory.setConnectionFactory(connectionFactory);
        //这边设置消息确认方式由自动确认变为手动确认
        simpleRabbitListenerContainerFactory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
        //设置消息预取数量
        // simpleRabbitListenerContainerFactory.setPrefetchCount(1);
        return simpleRabbitListenerContainerFactory;
    }
    /**
     * 每个rabbitTemplate方法只可以有一个回调,不然会报错 only one ConfirmCallback is supported by each RabbitTemplate,解决办法是配成多利的
     *
     * @param connectionFactory
     * @return
     */
    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate template = new RabbitTemplate(connectionFactory);
        //成功回调
        template.setConfirmCallback(new Callback());
        // 开启mandatory模式(开启失败回调)
        template.setMandatory(true);
        //失败回调
        template.setReturnCallback(new Callback());
 
        return template;
    }
}
@RestController
@RequestMapping("/api/admin/merchant")
public class MerchantAccountController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("check")
    public Object check(){

        //修改数据库的商家账号状态  TODO

        rabbitTemplate.convertAndSend(RabbitMQConfig.NEW_MERCHANT_EXCHANGE,RabbitMQConfig.NEW_MERCHANT_ROUTIING_KEY,"商家账号通过审核");

        Map<String,Object> map = new HashMap<>();
        map.put("code",0);
        map.put("msg","账号审核通过,请10秒内上传1个商品");
        return map;
    }
}

Listener死信队列消费

package net.xdclass.xdclasssp.mq;

import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
@RabbitListener(queues = "lock_merchant_dead_queue")
public class MerchantMQListener {

    @RabbitHandler
    public void messageHandler(String body, Message message, Channel channel) throws IOException {

        long msgTag = message.getMessageProperties().getDeliveryTag();
        System.out.println("msgTag="+msgTag);
        System.out.println("body="+body);
        //做复杂业务逻辑  TODO

        //告诉broker,消息已经被确认
        channel.basicAck(msgTag,false);
    }
}

七,日志与监控

        RabbitMQ 日志

        RabbitMQ默认日志存放路径: /var/log/rabbitmq/rabbit@xxx.log

         

        RabbitMQ日志详细信息:

                日志包含了RabbitMQ的版本号、 Erlang 的版本号、 RabbitMQ 服务节点名称、 cookie hash 值、 RabbitMQ 配置
        文件地址、内存限制、磁盘限制、默认账户guest 的创建以及权限配置等等。

 web管控台监控:

                直接访问当前的IP:15672,输入用户名和密码(默认是 guest ),就可以查看 RabbitMQ 的管理控制台。当然也可
        通过命令的形式来查看。如下:
        
  •         查看队列:rabbitmqctl list_queues

  •  对应管理控制台的页面如下:

  • 查看用户: rabbitmqctl list_users 

 查看连接:rabbitmqctl list_connections

 八,消息追踪

             为什么要消息追踪

                       在使用任何消息中间件的过程中,难免会出现某条消息异常丢失的情况。对于RabbitMQ而言,可能是因为生产者
                或消费者与RabbitMQ断开了连接,而它们与 RabbitMQ 又采用了不同的确认机制;也有可能是因为交换器与队列
                之间不同的转发策略;甚至是交换器并没有与任何队列进行绑定,生产者又不感知或者没有采取相应的措施;另外
                RabbitMQ本身的集群策略也可能导致消息的丢失。这个时候就需要有一个较好的机制跟踪记录消息的投递过程,
                以此协助开发和运维人员进行问题的定位。

             怎么使用

               在RabbitMQ中可以使用Firehoserabbitmq_tracing插件功能来实现消息追踪。

           消息追踪-Firehose

                        fifirehose的机制是将生产者投递给rabbitmq的消息, rabbitmq 投递给消费者的消息按照指定的格式发送到默认的
                exchange上。这个默认的exchange 的名称为 amq.rabbitmq.trace ,它是一个 topic 类型的 exchange 。发送到
                这个exchange上的消息的 routing key publish.exchangename deliver.queuename 。其中 exchangename
                和queuename为实际 exchange queue 的名称,分别对应生产者投递到 exchange 的消息,和消费者从 queue
                获取的消息。

                

rabbitmqctl trace_on :开启 Firehose 命令
消息追踪验证:
        1,创建一个队列test_trace,并将当前的队列绑定到amq.rabbit.trace 交换机上,设置RoutingKey为:#

     2,未开启消息追踪之前,我们发送一个消息

                

2, RabbitMq 应用问题

        消息可靠性保障 

                提出需求:如何能够保证消息的 100% 发送成功?

                首先大家要明确任何一个系统都不能保证消息的百分百投递成功,我们是可以保证消息以最高最可靠的发送给目标的

                RabbitMQ中采用 消息补充机制 来保证消息的可靠性

                

步骤分析:
参与部分:消息生产者、消息消费者、数据库、三个队列( Q1 Q2 Q3 )、交换机、回调检查服务、定时检查服
1. 消息的生产者将业务数据存到数据库中
2. 发送消息给 队列 Q1
3. 消息的生产者等待一定的时间后,在发送一个延迟消息给队列 Q3
4. 消息的消费方监听 Q1 队列消息,成功接收后
5. 消息的消费方会 发送 一条确认消息给 队列 Q2
6. 回调检查服务监听 队列 Q2 发送的确认消息
7. 回调检查服务接收到确认消息后,将消息写入到 消息的数据库表中
8. 回调检查服务同时也会监听 队列 Q3 延迟消息, 如果接收到消息会和数据库比对消息的唯一标识
9. 如果发现没有接收到确认消息,那么回调检查服务就会远程调用 消息生产者,重新发送消息
10. 重新执行 2-7 步骤,保证消息的可靠性传输
11. 如果发送消息和延迟消息都出现异常,定时检查服务会监控 消息库中的消息数据,如果发现不一致的消息然
后远程调用消息的生产者重新发送消息。

消息可靠性保障,就是说需要一系列的措施来保障rabbitmq 消息不出错,

额外参考:https://www.cnblogs.com/linjiqin/p/12683076.html

2,消息幂等性处理  

        幂等性指一次和多次请求某一个资源,对于资源本身应该具有同样的结果。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。
        在MQ 中指,消费多条相同的消息,得到与消费该消息一次相同的结果。
在本教程中使用 乐观锁机制 保证消息的幂等操作

自动补偿机制 

如果消费端接收消息时,消费端消费不成功的话,

@Component
public class Consumer {

    public static final String QUEUE_NAME = "byte-zb";

    @RabbitListener(queues = QUEUE_NAME)
    public void receiveMessage(String message) throws Exception {

        System.out.println("接收到的消息为"+message);
        int i = 1 / 0;
    }
}

我们会看到消费者工程控制台一直在刷新报错,当消费者配出异常,也就是说当消息消费不成功的话,该消息会存放在rabbitmq的服务端,一直进行重试,直到不抛出异常为止。

如果一直抛异常,我们的服务很容易挂掉,那有没有办法控制重试几次不成功就不再重试了呢?答案是有的。我们在消费者application.yml中增加一段配置。

spring:
  rabbitmq:
    # 连接地址
    host: 127.0.0.1
    # 端口
    port: 5672
    # 登录账号
    username: guest
    # 登录密码
    password: guest
    # 虚拟主机
    virtual-host: /
    listener:
      simple:
        retry:
          enabled: true # 开启消费者进行重试
          max-attempts: 5 # 最大重试次数
          initial-interval: 3000 # 重试时间间隔

上面配置的意思是消费异常后,重试五次,每次隔3s。继续启动消费者看看效果,我们发现重试五次以后,就不再重试了。

结合实践案例来使用消息补偿机制

 解决消息幂等性问题

        一些刚接触java的同学可能对幂等性不太清楚。幂等性就是重复消费造成结果不一致。为了保证幂等性,因此消费者消费消息只能消费一次消息。我么可以是用全局的消息id来控制幂等性。当消息被消费了之后我们可以选择缓存保存这个消息id,然后当再次消费的时候,我们可以查询缓存,如果存在这个消息id,我们就不错处理直接return即可。先改造生产者代码,在消息中添加消息id:

@RequestMapping("/send")
    public void sendMessage(){

        JSONObject jsonObject = new JSONObject();
        jsonObject.put("email","11111111111");
        jsonObject.put("timestamp",System.currentTimeMillis());
        String json = jsonObject.toJSONString();
        System.out.println(json);

        	Message message = MessageBuilder.withBody(json.getBytes()).setContentType(MessageProperties.CONTENT_TYPE_JSON)
                .setContentEncoding("UTF-8").setMessageId(UUID.randomUUID()+"").build();
        amqpTemplate.convertAndSend(EXCHANGE_NAME,QUEUE_NAME,message);
    }

消费者代码改造:

@Component
public class Consumer {

    public static final String QUEUE_NAME = "byte-zb";

    @RabbitListener(queues = QUEUE_NAME)
    public void receiveMessage(Message message) throws Exception {

        Jedis jedis = new Jedis("localhost", 6379);

        String messageId = message.getMessageProperties().getMessageId();
        String msg = new String(message.getBody(),"UTF-8");
        System.out.println("接收导的消息为:"+msg+"==消息id为:"+messageId);

        String messageIdRedis = jedis.get("messageId");

        if(messageId == messageIdRedis){
            return;
        }
        JSONObject jsonObject = JSONObject.parseObject(msg);
        String email = jsonObject.getString("email");
        String content = jsonObject.getString("timestamp");

        String httpUrl = "http://127.0.0.1:8080/email?email"+email+"&content="+content;
        // 如果发生异常则返回null
        String body = HttpUtils.httpGet(httpUrl, "utf-8");
        //
        if(body == null){
            throw new Exception();
        }
        jedis.set("messageId",messageId);
    }
}

我们在消费者端使用redis存储消息id,只做演示,具体项目请根据实际情况选择相应的工具进行存储。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

vegetari

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值