Bootstrap

Springboot整合RabbitMQ(Fanout、Direct、Topic模式)、设置队列信息TTL、死信队列、RabbitMQ磁盘监控,内存控制

RabbitMQ–了解中间件、常用的中间件、分布式系统使用中间件、Docker安装rabbitmq及遇到的问题、RabbitMQ核心组成、消息模式
Springboot整合RabbitMQ(Fanout、Direct、Topic模式)、设置队列信息TTL、死信队列、RabbitMQ磁盘监控,内存控制
Springboot+Rabbitmq消费者注解详解、改序列化方式
Docker简易部署RabbitMQ集群、分布式事务解决方案+案例(可靠生产、可靠消费)
Springboot+RabbitMQ+ACK机制(生产方确认(全局、局部)、消费方确认)、知识盲区

Springboot整合RabbitMQ

记得要先引入依赖

<!--springboot-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!--amqp 包含了rabbitmq的依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

在生产者和消费者中设置rabbitmq的连接信息(application.yml文件)

# 配置rabbitmq
spring:
  rabbitmq:
    username: admin
    password: admin
    virtual-host: /
    host: xxx.xxx.xxx.xxx
    port: 5672

简单模式

再给个图,好理解。

在这里插入图片描述

配置一个队列,不需要创建交换机(走默认的)

@SpringBootConfiguration
public class RabbitmqConfig {
    // 配置一个工作模型队列
    @Bean
    public Queue queueWork1() {
        return new Queue("queue_work");
    }
}

发送信息

public void sendWork() {
    for (int i = 0; i < 10; i++) {
        rabbitTemplate.convertAndSend("queue_work", "普通模式队列:" + i);
    }
}

设置一个接收者

@Component
@RabbitListener(queues = "queue_work")
public class SmsConsumer {
    @RabbitHandler
    public void receiveEmail(String message) {
        System.out.println("--接收到了普通模式队列信息-->" + message);
    }
}

发送消息

在这里插入图片描述

Work模式

在这里插入图片描述

如图:消息被投放到一个队列中,然后有两个消费者一起消费。

Work模式又分为公平和轮询方式:

  • 公平模式:能者多劳,消费能力高,消费的消息数量就多些
  • 轮询:不管消费能力如何,所有的消费者消费到的消息数量是基本一致的。

轮询模式

spring:
  rabbitmq:
    username: admin
    password: admin
    virtual-host: /
    host: xxx.xxx.xxx.xxx
    port: 5672
@Component
public class SmsConsumer {
    @RabbitHandler
    @RabbitListener(queues = "queue_work")
    public void receiveEmail(String va, Channel channel, Message message) throws InterruptedException, IOException {
        Thread.sleep(5000);
        System.out.println("--消费者1接收到了普通模式队列信息-->" + va);
    }

    @RabbitHandler
    @RabbitListener(queues = "queue_work")
    public void receiveEmail2(String va, Channel channel, Message message) throws IOException {
        System.out.println("--消费者2接收到了普通模式队列信息-->" + va);
    }
}

消费者1消息能力弱于消费者2时
在这里插入图片描述

两个队列收到的数量是一致的。

公平模式

需要配置spring.rabbitmq.listener.simple.prefetch=1

spring:
  rabbitmq:
    username: admin
    password: admin
    virtual-host: /
    host: xxx.xxx.xxx.xxx
    port: 5672
    listener:
      simple:
        prefetch: 1

两个消费者

@Component
public class SmsConsumer {
    @RabbitHandler
    @RabbitListener(queues = "queue_work")
    public void receiveEmail(String va) throws InterruptedException {
        Thread.sleep(3000);
        System.out.println("--消费者1接收到了普通模式队列信息-->" + va);
    }

    @RabbitHandler
    @RabbitListener(queues = "queue_work")
    public void receiveEmail2(String va) {
        System.out.println("--消费者2接收到了普通模式队列信息-->" + va);
    }
}

再次发消息尝试

在这里插入图片描述

由于消费者1消费性能过差,只接收到一条消息。

Fanout模式(配置方式绑定)

在Fanout模式、Direct模式还是其他模式下,交换机和队列的创建无论是绑定在生产者一方还是消费者一方都是可以的,只是要注意启动顺序,或者单独搞一个服务专门用来创建和绑定交换机和队列的关系。推荐放到消费者这一方,因为只有消费者监听不存在的队列时会程序异常,生产者不会。

配置类完成 交换机和队列的绑定关系。

创建两个队列和一个交换机,Java配置方式完成绑定关系。

@Configuration
public class RabbitMQConfiguration {

    // 创建 fanout 交换机
    @Bean
    public FanoutExchange fanoutExchange() {
        // 创建一个名称是 fanout_exchange 的交换机,允许持久化, 自动删除关闭
        return new FanoutExchange("fanout_exchange", true, false);
    }

    // 创建队列 sms.fanout.queue    email.fanout.queue
    @Bean
    public Queue smsQueue() {
        return new Queue("sms.fanout.queue");
    }

    @Bean
    public Queue emailQueue() {
        return new Queue("email.fanout.queue");
    }

    // 交换机和队列绑定 关系
    @Bean
    public Binding smsBinding() {
        return BindingBuilder.bind(smsQueue()).to(fanoutExchange());
    }
    @Bean
    public Binding emailBinding() {
        return BindingBuilder.bind(emailQueue()).to(fanoutExchange());
    }
    
}

生产者生产消息,并将消息投递到上面配置的fanout_exchange交换机中

@Autowired
private RabbitTemplate rabbitTemplate;

// 模拟用户下单
public void makeOrder(String userId, String productId, int num) {
    String exchange = "fanout_exchange";
    // fanout模式不需要 routingKey,直接空值即可
    String routingKey = "";
    // 模拟生成订单
    String order = UUID.randomUUID().toString();
    System.out.println("订单产生:" + order);
    rabbitTemplate.convertAndSend(exchange, routingKey, order);
}

消费者监听消息队列

@RabbitListener(queues = "email.fanout.queue")
@Service
public class EmailConsumer {
    @RabbitHandler
    public void receiveEmail(String message) {
        System.out.println("email fanout --接收到了订单信息-->" + message);
    }

}
@RabbitListener(queues = "sms.fanout.queue")
@Service
public class SmsConsumer {
    
    @RabbitHandler
    public void receiveEmail(String message) {
        System.out.println("sms fanout --接收到了订单信息-->" + message);
    }

}

发送消息,测试接收

在这里插入图片描述

Direct模式(配置方式绑定)

创建一个Direct模式的交换机,并且创建两个队列,将交换机和两个队列绑定起来并且设置好 routingKey

@Configuration
public class RabbitMQConfiguration {
    // 创建 direct 交换机
    @Bean
    public DirectExchange directExchange() {
        return new DirectExchange("direct_exchange", true, false);
    }
    // 创建队列 sms.direct.queue    email.direct.queue
    @Bean
    public Queue smsQueue() {
        return new Queue("sms.direct.queue");
    }
    @Bean
    public Queue emailQueue() {
        return new Queue("email.direct.queue");
    }
    // 交换机和队列绑定 关系
    @Bean
    public Binding smsBinding() {
        return BindingBuilder.bind(smsQueue()).to(directExchange()).with("sms");
    }
    @Bean
    public Binding emailBinding() {
        return BindingBuilder.bind(emailQueue()).to(directExchange()).with("email");
    }
}

生产者发送消息

// 模拟用户下单
public void makeOrderDirect(String userId, String productId, int num) {
    // 指定好交换机的名字(就是刚刚创建的交换机的名称)
    String exchange = "direct_exchange";
    // direct模式需要 设置 相应的 routingKey(消息携带routingKey后,会被交换机发送到有同样routingKey绑定的队列中)
    // 比如只想让 sms 这个routingKey的队列收到消息
    String routingKey = "sms";
    // 模拟生成订单
    String order = UUID.randomUUID().toString();
    System.out.println("订单产生:" + order);
    rabbitTemplate.convertAndSend(exchange, routingKey, order);
}

消费者监听队列

确认好要监听的队列,必须和前面创建的队列名称对应上

@RabbitListener(queues = "email.direct.queue")
@Service
public class EmailConsumer {
    @RabbitHandler
    public void receiveEmail(String message) {
        System.out.println("email direct --接收到了订单信息-->" + message);
    }
}
@RabbitListener(queues = "sms.direct.queue")
@Service
public class SmsConsumer {
    @RabbitHandler
    public void receiveEmail(String message) {
        System.out.println("sms direct --接收到了订单信息-->" + message);
    }
}

再次强调:当消费者监听了一个不存在的队列时,程序直接抛出异常!!!

执行发送一条携带sms的消息到direct_exchange的交换机中。只有和 sms队列绑定的消费者收到了消息

在这里插入图片描述

Topic模式(注解方式绑定)

注解方式不需要硬编码方式进行交换机和队列的绑定,直接使用注解声明式方式即可。其他模式也可使用注解方式来进行声明式开发。需要根据 exchange中的 type 来区分是哪种模式。

消费者中监听

@Component
@RabbitListener(bindings = @QueueBinding(   // 创建绑定关系
        value = @Queue(value = "email.topic.queue", durable = "true", autoDelete = "false"),    // 创建队列
        exchange = @Exchange(value = "topic_exchange", type = ExchangeTypes.TOPIC), // 创建交换机,及模式
        key = "*.email.#"   // 路由匹配规则
))
public class EmailConsumer {
    @RabbitHandler
    public void receiveEmail(String message) {
        System.out.println("email topic --接收到了订单信息-->" + message);
    }
}
@Component
@RabbitListener(bindings = @QueueBinding(   // 创建绑定关系
        value = @Queue(value = "sms.topic.queue", durable = "true", autoDelete = "false"),    // 创建队列
        exchange = @Exchange(value = "topic_exchange", type = ExchangeTypes.TOPIC), // 创建交换机,及模式
        key = "*.sms.#"   // 路由匹配规则
))
public class SmsConsumer {
    @RabbitHandler
    public void receiveEmail(String message) {
        System.out.println("sms topic --接收到了订单信息-->" + message);
    }
}

直接启动消费者服务后,就会发现交换机和队列都创建好了,而且交换机和队列的关系也已经绑定了。当然在开发中最好是使用配置类的方式进行创建和绑定交换机和队列。因为使用注解的方式不便于管理。

在这里插入图片描述

假如只想把消息放入到 email.topic.queue的队列中,那么如何做呢?

可以看到 Routing Key 匹配规则是 *.email.#,那么我们发送消息的时候设置为xxx.email[.xxx]即可,中括号中可有可无。如以下的 com.email就完全满足这点。

// 模拟用户下单
public void makeOrderTopic(String userId, String productId, int num) {
    String exchange = "topic_exchange";
    // topic模式需要 设置 相应的 匹配规则
    // 比如只想让 email.topic.queue 这个队列收到消息
    String routingKey = "com.email";	// 
    // 模拟生成订单
    String order = UUID.randomUUID().toString();
    System.out.println("订单产生:" + order);
    rabbitTemplate.convertAndSend(exchange, routingKey, order);
}

发送一条消息,尝试

在这里插入图片描述

只有email消费者接收到了消息。

扩展

TTL队列过期时间

TTL – Time To Live,过期时间,表示可以对消息设置预期的时间,在这个时间内都可以被消费者接收获取;但是过了时间之后消息自动被删除。RabbitMQ可以对消息队列设置TTL。

设置队列TTL(批量)

设置队列的TTL后,每个被放入此队列的消息都有一个过期时间,时间一过,这条消息就会被移除了。

如何创建一个带有过期时间的队列呢?

@Configuration
public class RabbitMQConfiguration {

    // 创建 direct 交换机
    @Bean
    public DirectExchange directExchange() {
        return new DirectExchange("ttl_direct_exchange", true, false);
    }
    
    // 创建队列并且设置过期时间
    @Bean
    public Queue smsQueue() {
        // 通过map设置 一些参数
        Map<String, Object> args = new HashMap<>();
        args.put("x-message-ttl", 5000);    // 注意是 int 类型 (这些参数可在 rabbitmq管理页查看)
        return new Queue("ttl.direct.queue", true, false, false, args);
    }

    // 绑定关系
    @Bean
    public Binding smsBinding() {
        // 绑定关系。并且设置 'ttl' 的routingKey
        return BindingBuilder.bind(smsQueue()).to(directExchange()).with("ttl");
    }

}

创建队列的一些参数,可以从RabbitMQ的管理页面上查阅。

在这里插入图片描述

生产者发送消息。

// 发送一条消息
public void makeOrderTTl(String userId, String productId, int num) {
    String exchange = "ttl_direct_exchange";
    String routingKey = "ttl";
    // 模拟生成订单
    String order = UUID.randomUUID().toString();
    System.out.println("订单产生:" + order);
    rabbitTemplate.convertAndSend(exchange, routingKey, order);
}

先把配置了 交换机和队列的 项目跑一下。把交换机和队列以及关系创建好。

在这里插入图片描述

可以看到生成了一个队列,有一个TTL的标志。发送一条消息。可以看到页面上有一个消息。

在这里插入图片描述

过了5s后。消费已经被移除了。

在这里插入图片描述

设置消息的TTL(单条)

只设置消息的TTL,不设置队列的TTL。设置普通队列。

@Configuration
public class RabbitMQConfiguration {
    // 创建 direct 交换机
    @Bean
    public DirectExchange directExchange() {
        return new DirectExchange("direct_exchange", true, false);
    }
    // 创建队列并且设置过期时间
    @Bean
    public Queue smsQueue() {
        return new Queue("sms.direct.queue", true, false, false);
    }
    // 绑定关系
    @Bean
    public Binding smsBinding() {
        return BindingBuilder.bind(smsQueue()).to(directExchange()).with("sms");
    }
}

生产者发送消息时需要设置一下消息过期的时间。

public void makeOrderTTlmessage(String userId, String productId, int num) {
    String exchange = "direct_exchange";
    String routingKey = "sms";
    // 模拟生成订单
    String order = UUID.randomUUID().toString();
    System.out.println("订单产生:" + order);

    // 给消息设置过期时间
    MessagePostProcessor messagePostProcessor = message -> {
        // 设置 字符串 类型的
        message.getMessageProperties().setExpiration("6000");
        // 还可以设置编码格式
        message.getMessageProperties().setContentEncoding("UTF-8");
        return message;
    };
    // 将参数传入
    rabbitTemplate.convertAndSend(exchange, routingKey, order, messagePostProcessor);
}

发送一个消息

在这里插入图片描述

6s后

在这里插入图片描述

假如:队列消息都设置了过期时间TTL,那么会以 TTL 最小的那个时间为准!!!

如:队列设置5s过期,单条消息设为3s过期,那么以单条消息的3s为准。若队列2s过期,消息5s过期,那么以队列的2s过期为准。

设置队列的TTL和设置消息的TTL两种差异:

如果设置队列的TTL,那么当队列中消息过期后我们可以用死信队列进行接收。但是如果是消息的TTL,那么当消息一旦过期,这个消息只能被移除掉,无法扩展到死信队列中。

死信队列

DLX(Dead-Letter-Exchange),可以称之为死信交换机,或者死信邮箱。当消息在一个队列中变为死信之后,它能被重新发送到另外一个交换机中,这个交换机就是死信交换机DLX。绑定了DLX的队列被称之为死信队列

什么情况下消息会被放入到死信队列中呢?

  • 消息被拒绝
  • 消息过期
  • 队列达到最大长度了

DLX和普通的交换机毫无区别。只需要在定义队列的时候设置队列参数x-dead-letter-exchange指定交换机即可。

流程如下:

在这里插入图片描述

死信接收者,可以后续进行处理或者存盘。

创建死信交换机。其实就是普通的交换机。这里设置为Direct模式。

@Configuration
public class DeadExchangeConfiguration {
    // 创建 direct 交换机
    @Bean
    public DirectExchange deadDirectExchange() {
        return new DirectExchange("dead_direct_exchange", true, false);
    }

    // 创建队列
    @Bean
    public Queue deadSmsQueue() {
        return new Queue("dead.direct.queue", true, false, false);
    }

    // 绑定关系
    @Bean
    public Binding deadSmsBinding() {
        return BindingBuilder.bind(deadSmsQueue()).to(deadDirectExchange()).with("dead");
    }
}

创建TTL队列和时间过期后发送到哪个死信队列中

同样的,需要设置的队列参数可以到 RabbitMQ的管理页面进行查看。

在这里插入图片描述

其中x-dead-letter-exchange就是设置死信交换机的名称。也就是我们之前创建好的dead_direct_exchange.

x-dead-letter-routing-key是死信交换机的routingKey。我们设置的dead

那么TTL队列设置时需要设置好这些参数。如下:

@Configuration
public class RabbitMQConfiguration {

    // 创建 direct 交换机
    @Bean
    public DirectExchange directExchange() {
        return new DirectExchange("ttl_direct_exchange", true, false);
    }

    // 创建队列并且设置过期时间
    @Bean
    public Queue smsQueue() {
        // 通过map设置 一些参数
        Map<String, Object> args = new HashMap<>();
        args.put("x-message-ttl", 5000);    // 注意是 int 类型 (这些参数可在 rabbitmq管理页查看)
        args.put("x-dead-letter-exchange", "dead_direct_exchange"); // 设置死信交换机
        args.put("x-dead-letter-routing-key", "dead");  // 由于我们的死信交换机是Direct模式,需要设置相应的 routingKey
        return new Queue("ttl.direct.queue", true, false, false, args);
    }

    // 绑定关系
    @Bean
    public Binding smsBinding() {
        // 绑定关系。并且设置 'ttl' 的routingKey
        return BindingBuilder.bind(smsQueue()).to(directExchange()).with("ttl");
    }

}

这里有一个问题需要注意一下:如果我们之前创建过一个队列(普通队列),但是发现需要进行业务扩展需要把这个普通队列设置为 TTL队列,那么通过如上代码的方式是不可以的,程序会报错。也就是我们不能对已经创建好的队列进行修改参数操作。需要把它、删了重新执行程序。

切记:在生产环境中,数据高速运转传输时千万不要随便删除某个队列。我们可以再新建一个理想的队列来进行扩展。

发送一条消息到TTL队列中

在这里插入图片描述

经过5s后,消息由于没有被消费掉。被放入了死信队列中

在这里插入图片描述

除了过期时间外,当队列长度到达最大值时,剩余还没到队列中的消息会被直接放入到死信队列中。

@Bean
public Queue smsQueue() {
    // 通过map设置 一些参数
    Map<String, Object> args = new HashMap<>();
    args.put("x-message-ttl", 5000);    // 注意是 int 类型 (这些参数可在 rabbitmq管理页查看)
    args.put("x-max-length", 5);    // 设置队列长度为 5
    args.put("x-dead-letter-exchange", "dead_direct_exchange"); // 设置死信交换机
    args.put("x-dead-letter-routing-key", "dead");  // 由于我们的死信交换机是Direct模式,需要设置相应的 routingKey
    return new Queue("ttl.direct.queue", true, false, false, args);
}

模拟发送消息 12条。有5条是放入到TTL队列中,然后7条放入了死信队列
在这里插入图片描述

然后由于消息在5s内没被消费,最终都被放入到死信队列中

在这里插入图片描述

内存磁盘监控

RabbitMQ内存警告

RabbitMQ内存警告:

当内存使用超过配置的阈值或者磁盘空间剩余空间对于配置的阈值时,RabbitMQ会暂时阻塞客户端的连接,并且停止接收从客户端发送来的消息,依次避免服务器的崩溃。

在这里插入图片描述

从RabbitMQ的管理页面可看到内存的使用情况

在这里插入图片描述

上图可以看到,我现在使用了144MB,当使用到727MB时,这个Memory的状态框将会爆红。然后生产者们的连接状态就会变成Blocking或Blocked。RabbitMQ将会停止接收消息。

RabbitMQ内存控制

有两种方式可以调整RabbitMQ的内存:

  • 通过命令的方式(当服务器重启时,设置将会失效)

    # 设置方式一(内存阈值,默认0.4/2GB,当内存超过40%,产生警告并阻塞所有生产者连接)
    rabbitmqctl set_vm_memory_high_watermark 0.4
    
    # 设置方式二(设置一个具体的值,内存超过2G爆红)
    rabbitmqctl set_vm_memory_high_watermark absolute 2G
    

    我们尝试将内存的阈值设置为 70MB,超过70MB直接爆红

    rabbitmqctl set_vm_memory_high_watermark absolute 70MB
    

在这里插入图片描述

Connection连接直接阻塞状态

在这里插入图片描述

  • 通过配置文件的方式(服务器重启,设置不会失效)

    在配置文件中/etc/rabbitmq/rabbitmq.conf,无论是使用 relative 还是 absolute 选其一即可。

    # 默认阈值 0.4 (40%)
    vm_memory_high_watermark.relative=0.4
    
    # 使用 relative相对值进行设置时, 取值建议在 0.4-0.7之间
    vm_memory_high_watermark.relative=0.6
    
    # 使用absolute绝对值方式(尽量不要用绝对值配置)
    vm_memory_high_watermark.absolute=2GB
    
;