RabbitMQ基础到进阶

RabbitMQ基础

一、SpringAMQP部署

1.引入SpringAMQP依赖


<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-amqpartifactId>
dependency>

2.配置MQ地址

spring:
  rabbitmq:
    host: 192.168.150.128 # 主机名
    port: 5672 # 端口
    virtual-host: / # 虚拟主机
    username: root # 用户名
    password: 123456 # 密码

二、简单消息队列

1.定义消息监听器

@Component
@Slf4j
public class SpringRabbitListener {
    /**
     * 简单消息队列监听
     */
    @RabbitListener(queuesToDeclare = @Queue("normal.queue"))
    public void normalRabbitListener(String msg){
        log.info("普通消息队列收到消息:{}",msg);
    }
}

2.向队列发送消息

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAMQPTest {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void publish() {
        String queueName = "normal.queue";
        String msg = "HelloWorld";
        rabbitTemplate.convertAndSend(queueName, msg);
    }
}

三、工作消息队列

​ 模型:多个消费者同时绑定同一个消息队列,但每个消费者消费能力不同,默认条件下消息按照轮询的规则公平分配到各消费者,这样就会因为消费者的性能差异导致消息处理速度变慢。

/**
 * 工作队列 1,处理 1 条消息需要 50 ms
 */
@RabbitListener(queuesToDeclare = @Queue("work.queue"))
public void workRabbitListener1(String msg) throws InterruptedException {
    log.info("工作队列监听 1 收到消息:{}",msg);
    Thread.sleep(50);
}
/**
 * 工作队列 2,处理 1 条消息需要 1000 ms
 */
@RabbitListener(queuesToDeclare = @Queue("work.queue"))
public void workRabbitListener2(String msg) throws InterruptedException {
    log.info("工作队列监听 2 收到消息:{}",msg);
    Thread.sleep(1000);
}

所以要解决此现象,就要改变消息分配机制(预取机制),做到每次只取一条消息,消息处理完成后才可继续获取消息。以实现能者多劳

能者多劳实现

spring:
  rabbitmq:
    listener:
      simple:
        prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息

四、fanout / direct / topic 交换机

1.fanout广播交换机

​ 消息到达广播交换机后,会路由给所有与之绑定的消息队列。

1.1 监听器

/**
 * 声明消息队列,与广播交换机绑定,两个消息队列都能收到消息
 * @param msg
 */
@RabbitListener(bindings = @QueueBinding(
        value = @Queue("fanout.queue1"),
        exchange = @Exchange(name = "fanout.exchange", type = ExchangeTypes.FANOUT)
))
public void fanoutExchangeListener1(String msg){
    log.info("广播消息队列1收到消息:{}",msg);
}

@RabbitListener(bindings = @QueueBinding(
        value = @Queue("fanout.queue2"),
        exchange = @Exchange(name = "fanout.exchange", type = ExchangeTypes.FANOUT)
))
public void fanoutExchangeListener2(String msg){
    log.info("广播消息队列2收到消息:{}",msg);
}

1.2 消息发送

/**
 * fanout交换机消息发送
 */
@Test
public void fanoutMsg(){
    String exchange = "fanout.exchange";
    String msg = "helloworld";
    rabbitTemplate.convertAndSend(exchange,null,msg);	//第二个参数为routingKey,在广播交换机中没有作用,可以随便写
}

2.direct交换机

​ 相比fanout交换机,增加了对routingKey的处理,在绑定交换机和队列的时候声明其routingKey,交换机会根据消息发送时指定的routingKey将消息路由到指定的消息队列。

2.1 消息监听

/**
 * 消息队列1:key为 "red","yellow"
 */
@RabbitListener(bindings = @QueueBinding(
        value = @Queue("direct.queue1"),
        exchange = @Exchange(name = "direct.exchange", type = ExchangeTypes.DIRECT),
        key = {"red","yellow"}
))
public void directExchangeListener1(String msg){
    log.info("广播消息队列1收到消息:{}",msg);
}

/**
 * 消息队列2:key为 "green"
 */
@RabbitListener(bindings = @QueueBinding(
        value = @Queue("direct.queue2"),
        exchange = @Exchange(name = "direct.exchange", type = ExchangeTypes.DIRECT),
        key = {"green"}
))
public void directExchangeListener2(String msg){
    log.info("广播消息队列2收到消息:{}",msg);
}

/**
 * 消息队列3:key为 "yellow","green
 */
@RabbitListener(bindings = @QueueBinding(
        value = @Queue("direct.queue3"),
        exchange = @Exchange(name = "direct.exchange", type = ExchangeTypes.DIRECT),
        key = {"yellow","green"}
))
public void directExchangeListener3(String msg){
    log.info("广播消息队列3收到消息:{}",msg);
}

2.2 消息发送

/**
 * direct交换机消息发送
 */
@Test
public void directMsg(){
    String exchange = "direct.exchange";
    String msg = "helloworld";
    String routingKey = "red";
    rabbitTemplate.convertAndSend(exchange,routingKey,msg);
}

3.topic交换机

​ 相当于对direct交换机的加强,routingKey支持通配符:

Routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert

通配符规则:

#:匹配0个或多个词

*:匹配不多不少恰好1个词

举例:

item.#:能够匹配item.spu.insert 或者 item.spu或者item

item.*:只能匹配item.spu

3.1 消息监听

@RabbitListener(bindings = @QueueBinding(
    value = @Queue(name = "topic.queue1"),
    exchange = @Exchange(name = "itcast.topic", type = ExchangeTypes.TOPIC),
    key = "china.#"
))
public void listenTopicQueue1(String msg){
    System.out.println("消费者接收到topic.queue1的消息:【" + msg + "】");
}

@RabbitListener(bindings = @QueueBinding(
    value = @Queue(name = "topic.queue2"),
    exchange = @Exchange(name = "itcast.topic", type = ExchangeTypes.TOPIC),
    key = "#.news"
))
public void listenTopicQueue2(String msg){
    System.out.println("消费者接收到topic.queue2的消息:【" + msg + "】");
}

3.2 消息发送

/**
 * topicExchange
 */
@Test
public void testSendTopicExchange() {
    // 交换机名称
    String exchangeName = "itcast.topic";
    // 消息
    String message = "喜报!孙悟空大战哥斯拉,胜!";
    // 发送消息
    rabbitTemplate.convertAndSend(exchangeName, "china.news", message);
}

五、配置JSON消息转换器

​ RabbitTemplate默认消息转换器是将消息通过 JDK 进行序列化,但是这种方式消息体积大、可读性差且有安全漏洞。

通过配置更合适的消息转换器,可以实现向消息队列发送序列化对象

1.引入jackson依赖

<dependency>
    <groupId>com.fasterxml.jackson.dataformatgroupId>
    <artifactId>jackson-dataformat-xmlartifactId>
    <version>2.9.10version>
dependency>

2.配置消息转换器

@Bean
public MessageConverter jsonMessageConverter(){
    return new Jackson2JsonMessageConverter();
}

Rabbit MQ进阶

一、保证消息可靠性

1.生产者确认机制

  • publisher-confirm,发送者确认
    • 消息成功投递到交换机,返回ack
    • 消息未投递到交换机,返回nack
  • publisher-return,发送者回执
    • 消息投递到交换机了,但是没有路由到队列。返回ACK,及路由失败原因。
1.1 追加publisher的配置文件
spring:
  rabbitmq:
    publisher-confirm-type: correlated  # 开启publisher-confirm 并设置确认模式为异步回调
    publisher-returns: true # 开启publish-return功能
    template:
      mandatory: true # 消息路由失败时的策略。true,则调用ReturnCallback;false:则直接丢弃消息
1.2 定义Return回调

每个RabbitTemplate只能配置一个ReturnCallback,因此需要在项目加载时配置

@Slf4j
@Configuration
public class CommonConfig implements ApplicationContextAware {
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        // 获取RabbitTemplate
        RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
        // 设置ReturnCallback
        rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
            // 投递失败,记录日志
            log.info("消息发送失败,应答码{},原因{},交换机{},路由键{},消息{}",
                     replyCode, replyText, exchange, routingKey, message.toString());
            // 如果有业务需要,可以重发消息
        });
    }
}
1.3 定义Confirm回调
@Test
public void testSendMessage2SimpleQueue() throws InterruptedException {
    // 1.消息体
    String message = "hello, spring amqp!";
    // 2.全局唯一的消息ID,需要封装到CorrelationData中
    CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
    // 3.添加callback
    correlationData.getFuture().addCallback(
            result -> {
                if(result.isAck()){
                    // 3.1.ack,消息成功
                    log.debug("消息发送成功, ID:{}", correlationData.getId());
                }else{
                    // 3.2.nack,消息失败
                    log.error("消息发送失败, ID:{}, 原因{}",correlationData.getId(), result.getReason());
                }
            },
            ex -> log.error("消息发送异常, ID:{}, 原因{}",correlationData.getId(),ex.getMessage())
    );
    // 4.发送消息
    rabbitTemplate.convertAndSend("camq.topic", "simple", message, correlationData);
    // 休眠一会儿,等待ack回执
    Thread.sleep(2000);
}

2.消费者确认机制

2.1 失败重试机制
  • none模式下,消息投递是不可靠的,可能丢失

  • auto模式类似事务机制,出现异常时返回nack,消息回滚到mq;没有异常,返回ack

  • manual:自己根据业务情况,判断什么时候该ack

      开启消费者消息确认且设置模式为 **auto**,只有当消息被成功处理完成后,返回ack,消息队列的消息才会被删除,当消息处理过程出现异常,则会返回unack,队列中的消息状态**重新回到ready状态**(也叫requeue),重新发给消费者。
    
spring:
  rabbitmq:
    listener:
      simple:
        acknowledge-mode: auto # 开启自动消息确认模式

缺点:当消费者出现异常后,消息会不断requeue(重入队)到队列,再重新发送给消费者,然后再次异常,再次requeue,无限循环,导致mq的消息处理飙升,带来不必要的压力。因此为了避免徒增服务器压力,可以设置本地失败重试。

2.2 本地失败重试
  • 开启本地重试时,消息处理过程中抛出异常,不会requeue到队列,而是在消费者本地重试
  • 重试达到最大次数后,Spring会返回ack,消息会被丢弃
spring:
  rabbitmq:
    listener:
      simple:
        retry:
          enabled: true # 开启消费者失败重试
          initial-interval: 1000 # 初识的失败等待时长为1秒
          multiplier: 1 # 失败的等待时长倍数,下次等待时长 = multiplier * last-interval
          max-attempts: 3 # 最大重试次数
          stateless: true # true无状态;false有状态。如果业务中包含事务,这里改为false
2.3 重试失败策略

在开启重试模式后,重试次数耗尽,如果消息依然失败,则需要有MessageRecovery接口来处理,它包含三种不同的实现:

  • RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。(默认)
  • ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队
  • RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机(最优解)

将重试失败策略设置为RepublishMessageRecoverer:

1)在consumer服务中定义处理失败消息的交换机和队列

@Bean
public DirectExchange errorMessageExchange(){
    return new DirectExchange("error.direct");
}
@Bean
public Queue errorQueue(){
    return new Queue("error.queue", true);
}
@Bean
public Binding errorBinding(Queue errorQueue, DirectExchange errorMessageExchange){
    return BindingBuilder.bind(errorQueue()).to(errorMessageExchange()).with("error");
}

2)定义一个RepublishMessageRecoverer,关联队列和交换机

@Bean
public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){
    return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");
}

注意:失败消息是由消费者发送到交换机的!!!

17:35:35:490 WARN 38180 — [ntContainer#0-1] o.s.a.r.retry.RepublishMessageRecoverer : Republishing failed message to exchange ‘error.direct’ with routing key error

二、死信交换机

1.死信的三种来源:

  • 消息消费失败,被消费者拒绝(RejectAndDontRequeueRecoverer)或nack
  • 消息超时
  • 消息队列已满

2.为一个队列指定死信交换机

在consumer服务中,定义一组死信交换机、死信队列:

@Configuration
public class CommonConfig {

    //声明普通消息队列,指定死信交换机
    @Bean
    public Queue simpleQueue(){
        return QueueBuilder.durable("simple.queue")
                .deadLetterExchange("dl.exchange")
                .deadLetterRoutingKey("deadLetter")
                .build();
    }

    //声明死信交换机
    @Bean
    public DirectExchange dlExchange(){
        return new DirectExchange("dl.exchange", true, false);
    }

    //声明死信队列
    @Bean
    public Queue dlQueue(){
        return new Queue("dl.queue", true);
    }

    //绑定死信交换机和死信队列
    @Bean
    public Binding dlBinding(){
        return BindingBuilder.bind(dlQueue()).to(dlExchange()).with("deadLetter");
    }
}

手动制造普通消息队列消息消费异常

@Component
public class SpringRabbitListener {

    @RabbitListener(queues = "simple.queue")
    public void listenSimpleQueue(String msg) {
        System.out.println("消费者接收到simple.queue的消息:【" + msg + "】");
        System.out.println(1/0);
    }

    @RabbitListener(queues = "dl.queue")
    public void listenErrorQueue(String msg) {
        System.out.println("死信队列dl.queue的消息:【" + msg + "】");
    }
}

测试:直接向消息队列发送消息

@Test
public void testSendMessage2SimpleQueue() throws InterruptedException {
    // 1.消息体
    String message = "hello, spring amqp!";
    // 2.全局唯一的消息ID,需要封装到CorrelationData中
    CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
    // 3.添加callback
    correlationData.getFuture().addCallback(
            result -> {
                if(result.isAck()){
                    // 3.1.ack,消息成功
                    log.debug("消息发送成功, ID:{}", correlationData.getId());
                }else{
                    // 3.2.nack,消息失败
                    log.error("消息发送失败, ID:{}, 原因{}",correlationData.getId(), result.getReason());
                }
            },
            ex -> log.error("消息发送异常, ID:{}, 原因{}",correlationData.getId(),ex.getMessage())
    );
    // 4.发送消息
    rabbitTemplate.convertAndSend("simple.queue", message);
    // 休眠一会儿,等待ack回执
    Thread.sleep(2000);
}

三、延迟消息

方式一:设置 ttl

@Configuration
public class CommonConfig {
    //ttl消息队列,队列有效期 5秒
    @Bean
    public Queue ttlQueue(){
        return QueueBuilder.durable("ttl.queue")
                .ttl(5000)
                .deadLetterExchange("dl.exchange")
                .deadLetterRoutingKey("deadLetter")
                .build();
    }
    //声明死信交换机
    //声明死信队列
    //绑定死信交换机和死信队列
}

发送消息时,也可以指定消息的有效时间,最终变成死信以消息和队列的最短时间为准,注意要看到效果,ttl队列中的消息不要被消费

Message message = MessageBuilder
        .withBody("hello, ttl message".getBytes(StandardCharsets.UTF_8))
        .setExpiration("2000")
        .build();

方式二:延迟队列

DelayExchange需要将一个交换机声明为delayed类型。当我们发送消息到delayExchange时,流程如下:

  • 接收消息
  • 判断消息是否具备x-delay属性
  • 如果有x-delay属性,说明是延迟消息,持久化到硬盘,读取x-delay值,作为延迟时间
  • 返回routing not found结果给消息发送者
  • x-delay时间到期后,重新投递消息到指定队列

1.安装Delay消息插件

2.声明Delay交换机和队列

//Delay交换机
@Bean
public DirectExchange delayExchange(){
    return ExchangeBuilder
            .directExchange("delay.exchange")
            .delayed()  //声明为 Delay交换机
            .durable(true)
            .build();
}

//Delay消息队列
@Bean
public Queue delayQueue(){
    return new Queue("delay.queue");
}
//绑定 delay消息队列和 delay交换机
@Bean
public Binding delayBinding(){
    return BindingBuilder.bind(delayQueue()).to(delayExchange()).with("delay");
}

3.消息发送时携带 X-Delay 消息头才能被delay交换机处理

Message message = MessageBuilder
        .withBody("hello, ttl message".getBytes(StandardCharsets.UTF_8))
        .setHeader("x-delay",2000)
        .build();

四、惰性队列

解决大量数据堆积问题的两个方向:1.增加消费者个数 2.扩大队列容量

从RabbitMQ的3.6.0版本开始,增加了惰性队列的概念,队列接收到的消息直接存入硬盘,消费者读取消息前先从硬盘加载到内存;硬盘存储成本低空间大,可以存储百万级数据。

方式一:基于命令行设置

rabbitmqctl set_policy Lazy "^lazy-queue$" '{"queue-mode":"lazy"}' --apply-to queues  

命令解读:

  • rabbitmqctl :RabbitMQ的命令行工具
  • set_policy :添加一个策略
  • Lazy :策略名称,可以自定义
  • "^lazy-queue$" :用正则表达式匹配队列的名字
  • '{"queue-mode":"lazy"}' :设置队列模式为lazy模式
  • --apply-to queues :策略的作用对象,是所有的队列

方式二:基于@Bean声明

//Delay消息队列
@Bean
public Queue lazyQueue(){
    return QueueBuilder
            .durable("lazy.queue")
            .lazy()     //开启 x-queue-mode 为 lazy模式
            .build();
}

方式三:基于@RabbitListener声明

@RabbitListener(queuesToDeclare = @Queue(
        name = "lazy.queue",
        durable = "true",
        arguments = @Argument(name = "x-queue-mode", value = "lazy")
))
public void listenLazyQueue(String msg) {
    System.out.println("消费者接收到lazy.queue的消息:【" + msg + "】");
}

五、MQ集群

1.普通集群

普通集群,或者叫标准集群(classic cluster),具备下列特征:

  • 会在集群的各个节点间共享部分数据,包括:交换机、队列元信息。不包含队列中的消息。
  • 当访问集群某节点时,如果队列不在该节点,会从数据所在节点传递到当前节点并返回
  • 队列所在节点宕机,队列中的消息就会丢失

2.镜像集群

镜像集群:本质是主从模式,具备下面的特征:

  • 交换机、队列、队列中的消息会在各个mq的镜像节点之间同步备份。
  • 创建队列的节点被称为该队列的主节点,备份到的其它节点叫做该队列的镜像节点。
  • 一个队列的主节点可能是另一个队列的镜像节点
  • 所有操作都是主节点完成,然后同步给镜像节点
  • 主宕机后,镜像节点会替代成新的主

3.仲裁队列

仲裁队列:仲裁队列是3.8版本以后才有的新功能,用来替代镜像队列,具备下列特征:

  • 与镜像队列一样,都是主从模式,支持主从数据同步
  • 使用非常简单,没有复杂的配置
  • 主从同步基于Raft协议,强一致
仲裁队列部署

1.创建仲裁队列

@Bean
public Queue quorumQueue() {
    return QueueBuilder
        .durable("quorum.queue") // 持久化
        .quorum() // 仲裁队列
        .build();
}

2.SpringAMQP连接MQ集群

注意,这里用address来代替host、port方式

spring:
  rabbitmq:
    addresses: 192.168.150.105:8071, 192.168.150.105:8072, 192.168.150.105:8073
    username: itcast
    password: 123321
    virtual-host: /

你可能感兴趣的:(java-rabbitmq,rabbitmq,java)