RabbitMq

1,RabbitMq 简介

是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件)。RabbitMQ服务器是用Erlang语言编写的,而集群和故障转移是构建在开放电信平台框架上的。所有主要的编程语言均有与代理接口通讯的客户端库。

官网
安装

三个作用:异步处理,流量削峰,应用解耦

2,RabbitMq 几个术语

1. Exchange - 交换机

生产者将消息发送给交换机,交换机按照一定规则分发消息给指定队列。消息根据交换机类型和 binding 可以投递到多个队列中。

常用的交换机有四种。

  1. 直连交换机

directExchange: 根据 routeKey 匹配队列

@Bean
public DirectExchange directExchangeDemo(){
    /*
    * 直连交换机
    * 一共四个参数:String name, boolean durable, boolean autoDelete, Map<String, Object> arguments
    *       name: 名称
    *       durable: 持久化
    *       autoDelete:自动删除
    *       arguments:参数
    *
    * */
    return new DirectExchange("directExchangeTest",true,false);
}
  1. 扇形交换机

FanoutExchange:不用匹配 routekey,所有队列都能获取扇形交换机分发的消息

@Bean
public FanoutExchange fanoutExchangeDemo(){
    /* 扇形交换机 */
    return new FanoutExchange("fanoutExchangeTest",true,false);
}
  1. 主题交换机

TopicExchange: 增强版的直连交换机,路由键 routekey 中,* 代表匹配任意一个单词,# 代表匹配任意一个或多个单侧, . 代表一个部分(www.# 可以匹配 www.aaa)

@Bean
public TopicExchange topicExchangeDemo(){
    return new TopicExchange("topicExchangeTest1",true,false);
}
  1. 头部交换机

HeadersExchange : 通过头部键值对匹配队列的交换机

@Bean
public HeadersExchange headersExchangeDemo(){
    /* 头部交换机 */
    return new HeadersExchange("headersExchangeTest",true,false);
}

2. Broker

接收和分发消息的应用,就是 mq 的服务端。

3. Virtual host

虚拟分组,类似于 nameSpace。

4. Connection

publisher/customer 和 broker 直接的连接。

5. Channel

信道,复用 Connection。

6. Exchange

交换机,message 到达 Broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发消息到 queue 中。

7. Queue

最终消息被送到这里等待被 customer 取走。

8. Binding

exchange 和 queue 之间的虚拟连接, binding 中可以包含 routing key, Binding 信息被保存到 exchange 中的查询表中,用于 message 的分发依据

3. 消息队列大致使用过程

  1. 启动一个消息队列服务器
  2. 客户端连接到消息队列服务器,打开一个 channel
  3. 客户端声明一个 exchange,并设置相关属性
  4. 客户端声明一个 queue,并设置相关属性
  5. 客户端使用 routing key,在 exchage 和 queue 中建立绑定关系
  6. 生产者投递消息到 exchange,exchange 接收到消息后,就根据消息的 key 和已经设置的 binding,进行消息路由,将消息投递到对应的队列中。
  7. 消费者消费队列中的消息。

4,消息应答

创建消费者:

/**
 *  消费者消费消息
 *    1,消费哪个队列
 *    2,消费成功之后是否要自动应答 "true" 代表自动应答 "false" 手动应答
 *    3,消费者未成功消费的回调
 * */
channel.basicConsume(String queue, boolean autoAck, DeliverCallback deliverCallback, CancelCallback cancelCallback);

确认消费:

/**
 * 参数 1,消息标记
 *      2,false 表示只应答接收到那个传递的消息
 * 用于肯定确认:RabbitMQ 已知道该消息并且成功的处理消息,可以将其丢弃了
 * 
 * multiple 的 true 和 false 代表不同意思:
 *        true 表示批量应答 channel 上未应答的消息,false 表示只应答当前 channel 上的消息。
 * */
Channel.basicAck(long deliveryTag, boolean multiple)

拒绝消费

/**
 *    参数 1,消息标记
 *         2,是否应答 channel 上所有未应答的消息
 *         3,是否重新入列
 *    用于否定消息
 * */ 
Channel.basicNack(long deliveryTag, boolean multiple, boolean requeue)

拒绝消费

/**    
 *    参数 1,消息标记
 *         3,是否重新入列
 *    用于否定消息,相比 basicNack 缺少 multiple 参数,不能批量确认
 * */
Channel.basicReject(long deliveryTag, boolean requeue)
手动确认 demo
public class Customer1 {
    public static void main(String[] args) throws IOException, TimeoutException {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("127.0.0.1");
        factory.setUsername("yanqi");
        factory.setPassword("5211314");
        factory.setVirtualHost("love");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        System.out.println("Custom1 等待接收消息....");

        //消费消息
        DeliverCallback deliverCallback = new DeliverCallback() {
            @Override
            public void handle(String s, Delivery delivery) throws IOException {
                String message = new String(delivery.getBody());
                System.out.println(message);
                channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
            }
        };

        //取消消息
        //取消消费的一个回调接口 如在消费的时候队列被删除掉了
        CancelCallback cancelCallback = (consumerTag) -> {
            System.out.println(consumerTag);
            System.out.println("消息消费被中断");
        };

        /**
         * 消费者消费消息
         * 1.消费哪个队列
         * 2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
         * 3,消费成功
         * 4.消费者未成功消费的回调
         */
        channel.basicConsume("中华艺术宫", false, deliverCallback, cancelCallback);
    }
}

5,队列持久化

//durable:true 表示队列持久化,false 表示不持久化,重启 rabbitmq 队列就没了

Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments);
/**
 * 生成一个队列
 * 1.队列名称
 * 2.队列里面的消息是否持久化 默认消息存储在内存中
 * 3.该队列是否只供一个消费者进行消费 是否进行共享 true 可以多个消费者消费
 * 4.是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true 自动删除
 * 5.其他参数
 */
channel.queueDeclare("中华艺术宫", false, false, false, null);

6,消息持久化

//props 中添加 MessageProperties.PERSISTENT_TEXT_PLAIN
basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body);
/**
 * 发送一个消息
 * 1.发送到那个交换机
 * 2.路由的 key 是哪个
 * 3.其他的参数信息,比如 MessageProperties.PERSISTENT_TEXT_PLAIN 消息持久化
 * 4.发送消息的消息体
 */
channel.basicPublish("", "中华艺术宫", MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());

7,预取值

  Channel 上未确认的缓冲区,通过 basicQos(int prefetchCount) 设置值,避免缓冲区无限制未确认大小。通过设置预取值,还可以根据不同消费者性能问题实现不公平分发。

8,发布确认

生成者将 Channel 设置成 confirm 模式,一旦消息被投递到所有匹配的队列之后, broker就会发送一个确认给生产者(包含消息的唯一 ID),这就使得生产者知道消息已经正确到达目的队列了。

发布确认
public class PushlierConfirm {
    public static void main(String[] args) {
        //创建一个连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("127.0.0.1");
        factory.setPort(5672);
        factory.setUsername("yanqi");
        factory.setPassword("5211314");
        factory.setVirtualHost("love");

        try (
                Connection connection = factory.newConnection();
                Channel channel = connection.createChannel()) {

            //发布确认
            channel.confirmSelect();

            /**
             * 生成一个队列
             * 1.队列名称
             * 2.队列里面的消息是否持久化 默认消息存储在内存中
             * 3.该队列是否只供一个消费者进行消费 是否进行共享 true 可以多个消费者消费
             * 4.是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true 自动删除
             * 5.其他参数
             */
            channel.queueDeclare("中华艺术宫", false, false, false, null);

            /**
             * 发送一个消息
             * 1.发送到那个交换机
             * 2.路由的 key 是哪个
             * 3.其他的参数信息,比如 MessageProperties.PERSISTENT_TEXT_PLAIN 消息持久化
             * 4.发送消息的消息体
             */
            int i = 0;
            while(true){
                String message = "hello world--" + (++i);
                channel.basicPublish("", "中华艺术宫", MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
                boolean b = channel.waitForConfirms();
                if(b){
                    System.out.println("消息 " + i + " 发布成功!");
                }else{
                    System.out.println("消息 " + i + " 发布失败!");
                }

                Thread.sleep(3_000);
            }

            //System.out.println("消息发送完毕");
        } catch (TimeoutException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

9,死信队列

无法被消费的消息。

来源:
1)消息 TTL 过期
2)队列达到最大长度,无法再添加数据到 mq 中
3)被拒绝的消息,并且 requeue = false

声明死信队列 demo
public static void rejectCustom() throws IOException, TimeoutException {
    Channel channel = ChannelUtil.getChannel();

    /**
     * 声明死信队列
     * queueDeclare(String queue,
     *              boolean durable,
     *              boolean exclusive,
     *              boolean autoDelete,
     *              Map<String, Object> arguments)
     *   queue:队列名
     *   durable:是否持久化
     *   exclusive:该队列是否只供一个消费者进行消费 是否进行共享 true 可以多个消费者消费
     *   autoDelete:是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true 自动删除
     *   arguments:其他参数
     */
    String dead_queue = "dead_queue";
    channel.queueDeclare(dead_queue, false, false, false, null);

    /**
     * 私信队列绑定交换机
     * */
    String dead_exchange = "dead_exchange";
    channel.exchangeDeclare(dead_exchange, BuiltinExchangeType.DIRECT);
    channel.queueBind(dead_queue, dead_exchange, "dead_routing");

    //声明正常队列
    String normal_queue = "normal_queue";
    Map<String, Object> params = new HashMap<>();
    params.put("x-dead-letter-exchange", dead_exchange);
    params.put("x-dead-letter-routing-key", "dead_routing");
    channel.queueDeclare(normal_queue, false, false, false, params);


    //等待接收消息
    System.out.println("等待接收消息----");
    channel.basicConsume(normal_queue, false, (consumerTag, message) -> {
        System.out.println(new String(message.getBody(), "UTF-8"));
        channel.basicReject(message.getEnvelope().getDeliveryTag(), false);
    }, consumerTag -> {
        System.out.println("消费失败");
    });
}

10,延时队列

TTL 是 RabbitMQ 中一个消息或者队列的属性,表明一条消息或者该队列中的所有消息的最大存活时间

设置超时时间
1)队列的 TTl,队列中的消息一旦过了 TTL 时间未被消费,就会丢弃(有死信队列就放到死信队列中)
2)消息的 TTL,即使消息过期,也不一定被马上丢弃

Rabbitmq 插件实现延迟队列

延时队列
//消息队列设置延时,投送消息到普通队列,ttl 时间内未被消费,投送到死信队列
public static void delay_queue() throws IOException, TimeoutException {
    Channel channel = ChannelUtil.getChannel();

    //死信队列
    String dead_queue = "delay_dead_queue";
    channel.queueDeclare(dead_queue, false, false, false, null);

    /**
     * 死信队列绑定交换机
     * */
    String dead_exchange = "delay_dead_exchange";
    String delay_routing_key = "delay_routing";
    channel.exchangeDeclare(dead_exchange, BuiltinExchangeType.DIRECT);
    channel.queueBind(dead_queue, dead_exchange, delay_routing_key);

    //声明带有 ttl 的队列
    String queue = "delay_queue";
    Map<String, Object> params = new HashMap<>();

    //设置队列的 ttl 时间
    params.put("x-message-ttl", 5000);
    params.put("x-dead-letter-exchange", dead_exchange);
    params.put("x-dead-letter-routing-key", delay_routing_key);
    channel.queueDeclare(queue, false, false, false, params);

    channel.basicPublish("", queue, null, "延时队列数据:1".getBytes());
    channel.basicPublish("", queue, null, "延时队列数据:2".getBytes());
    channel.basicPublish("", queue, null, "延时队列数据:3".getBytes());
}

/**
 * 消息延时
 * 消息即使过期,也不一定会被马上丢弃,因为消息是否过期是在即将投递到消费者之前判定的,如果当前队列有严重的消息积压情况,则已过期的消息也许还能存活较长时间
 * 可以用 java DelayQueue
 */
public static void delay_message() throws IOException, TimeoutException {
    Channel channel = ChannelUtil.getChannel();

    //死信队列
    String dead_queue = "delay_dead_queue";
    channel.queueDeclare(dead_queue, false, false, false, null);

    /**
     * 死信队列绑定交换机
     * */
    String dead_exchange = "delay_dead_exchange";
    String delay_routing_key = "delay_routing";
    channel.exchangeDeclare(dead_exchange, BuiltinExchangeType.DIRECT);
    channel.queueBind(dead_queue, dead_exchange, delay_routing_key);

    //声明带有 ttl 的队列
    String queue = "delay_queue";
    Map<String, Object> params = new HashMap<>();

    //设置队列的 ttl 时间
    params.put("x-dead-letter-exchange", dead_exchange);
    params.put("x-dead-letter-routing-key", delay_routing_key);
    channel.queueDeclare(queue, false, false, false, params);

    channel.basicPublish("", queue, new AMQP.BasicProperties().builder().expiration("10000").build(), "延时队列数据:1".getBytes());
    channel.basicPublish("", queue, new AMQP.BasicProperties().builder().expiration("3000").build(), "延时队列数据:2".getBytes());
    channel.basicPublish("", queue, new AMQP.BasicProperties().builder().expiration("300").build(), "延时队列数据:3".getBytes());
}

11,如何保证消息不丢失

Rabbitmq 丢失消息的三种情况:

  1. 生产者弄丢了数据。生产者将数据发送到 RabbitMQ 的时候,可能数据就在半路给搞丢了,因为网络问题啥的,都有可能
  2. RabbitMQ 弄丢了数据。MQ还没有持久化自己挂了
  3. 消费端弄丢了数据。刚消费到,还没处理,结果进程挂了,比如重启了

解决方案

1,针对生产者

方案1 :开启RabbitMQ事务

同步的事务机制,不推荐

方案2: 使用confirm机制

事务机制和 confirm 机制最大的不同在于,事务机制是同步的,你提交一个事务之后会阻塞在那儿,但是 confirm 机制是异步的

在生产者开启了confirm模式之后,每次写的消息都会分配一个唯一的id,然后如果写入了rabbitmq之中,rabbitmq会给你回传一个ack消息,告诉你这个消息发送OK了;如果rabbitmq没能处理这个消息,会回调你一个nack接口,告诉你这个消息失败了,你可以进行重试。而且你可以结合这个机制知道自己在内存里维护每个消息的id,如果超过一定时间还没接收到这个消息的回调,那么你可以进行重发

定义消费者
@RequestMapping("/sendMessage/confirm")
public void sendMessage3(@RequestParam String msg){
    String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());

    //添加发布确认
    rabbitTemplate.setConfirmCallback(myCallBack);

    /**
     * true:交换机无法将消息进行路由时,会将该消息返回给生产者
     * false:如果发现消息无法进行路由,则直接丢弃
     */
    rabbitTemplate.setMandatory(true);

    //设置回退消息交给谁处理
    rabbitTemplate.setReturnCallback(myCallBack);

    //第一条消息
    System.out.println(String.format("当前时间:%s,发布一条消息:%s", time, msg));
    String routingKey1 = "confirm_routing_key";
    CorrelationData c1 = new CorrelationData("1");
    rabbitTemplate.convertAndSend("confirm_exchange", routingKey1, msg + routingKey1,
            c1);

    //第二条消息
    System.out.println(String.format("当前时间:%s,发布一条消息:%s", time, msg));
    String routingKey2 = "confirm_routing_key1";
    CorrelationData c2 = new CorrelationData("2");
    rabbitTemplate.convertAndSend("confirm_exchange", routingKey2, msg + routingKey2,
            c2);
}
发布成功或者失败回调
@Component
public class MyCallBack implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {
    /**
     * 交换机不管是否收到消息的一个回调方法
     * CorrelationData:消息相关数据
     * ack:交换机是否收到消息
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        String id = correlationData != null ? correlationData.getId() : "";
        if (ack) {
            System.out.println( String.format("交换机已经收到 id 为: %s 的消息", id) );
        } else {
            System.out.println( String.format("交换机还未收到 id 为: %s 消息,由于原因: %s", id, cause));
        }
    }

    @Override
    public void returnedMessage(Message message, int i, String replyText, String exchange, String routingKey) {
        System.out.println( String.format("消息:%s 被服务器退回,退回原因: %s, 交换机是: %s, 路由 key:%s",
                new String(message.getBody()), replyText, exchange, routingKey));
    }
}

2,针对 RabbitMQ

方案一:消息持久化

1)Exchange 设置持久化(设置 durable )

@Bean
public FanoutExchange backupExchange(){
    FanoutExchange backupExchange = new FanoutExchange("backupExchange");
    backupExchange.isDurable();
    return backupExchange;
}

2)Queue 设置持久化(设置 durable )

@Bean
public Queue confirmQueue(){
    return QueueBuilder.durable("confirm_queue").build();
}

3)Message持久化发送:发送消息设置发送模式deliveryMode=2,代表持久化消息

方案二:消息补偿机制

生产端将消息存库,根据消息状态提供补偿措施。

3,针对消费者

使用rabbitmq提供的ack机制,服务端首先关闭rabbitmq的自动ack,然后每次在确保处理完这个消息之后,在代码里手动调用ack。这样就可以避免消息还没有处理完就ack。才把消息从内存删除。

12,如何避免消息重复消费

为甚么会重复消费?
1 生产者由于网络等原因未收到确认,重复发送了这条消息
2 消费者的确认由于网络等原因未被收到,再次消费消息

保证消息的幂等性?
让每个消息携带一个全局的唯一ID,即可保证消息的幂等性,具体消费过程为:
消费者获取到消息后先根据id去查询redis/db是否存在该消息
如果不存在,则正常消费,消费完毕后写入redis/db
如果存在,则证明消息被消费过,直接丢弃

13,消息积压怎么处理?

消息积压的原因?
1,消息堆积即消息没及时被消费,是生产者生产消息速度快于消费者消费的速度导致的。
2,消费者消费慢可能是因为:本身逻辑耗费时间较长、阻塞了。

1. 针对生产端

  1. 给消息设置过期时间,超时就丢弃
  2. 考虑使用队列最大长度限制
  3. 限制生产:
//param1:prefetchSize,消息本身的大小 如果设置为0 那么表示对消息本身的大小不限制
//param2:prefetchCount,告诉rabbitmq不要一次性给消费者推送大于N个消息
//param3:global,是否将上面的设置应用于整个通道
void basicQos(int prefetchSize, int prefetchCount, boolean global) throws IOException;

2. 针对消费端

  1. 消费端扩容,多增加几个消费者
  2. 消费端性能优化,默认情况下,rabbitmq消费者为单线程串行消费。
    org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer类的concurrentConsumers与txSize(对应prefetchCount)都是1),设置并发消费两个关键属性concurrentConsumers和prefetchCount。concurrentConsumers:设置的是对每个listener在初始化的时候设置的并发消费者的个数;prefetchCount:每次从broker里面取的待消费的消息的个数。
//配置方法:修改application.properties:
spring.rabbitmq.listener.concurrency=m
spring.rabbitmq.listener.prefetch=n
  1. 临时处理,先使用新的消费者把数据缓存下来,之后慢慢消费。

14,如何保证消费顺序

RabbitMQ 本身队列是有序的,但是消费的效率不同可能导致最终处理后的结构顺序不一致。

可能导致顺序不一致的情况:
1,一个队列对应多个消费者,每个消费者的效率不一致导致消费顺序不一致。
2,一个消费者内部使用多线程操作,也会导致消费顺序不一致。

解决方案:
1、拆分多个 queue,每个 queue 一个 consumer。

2、一个 queue,但是对应一个 consumer,然后这个 consumer 内部用内存队列(其实就是List而已)做排队,然后分发给底层不同的thread来处理(此方案可以支持高并发)。

posted @ 2023-07-07 15:24  primaryC  阅读(7)  评论(0编辑  收藏  举报