消息中间件之RabbitMQ

139 阅读6分钟

1.RabbitMQ基于docker安装单机版本

1.1 搜索rabbitmq镜像

docker search rabbitmq:management

image.png

1.2 拉取镜像

docker pull rabbitmq:management

image.png

1.3 创建并启动容器

docker run -d --name rabbitmq -p 15672:15672 -p 5672:5672 rabbitmq:management

image.png

1.4 查看rabbitmq管理页面

访问地址 ip:15672

默认账号密码:guest/guest

image.png

2.RabbitMQ基本概念

image.png

2.1 virtual host

虚拟主机:rabbitmq中有多个虚拟主机,每个虚拟主机都有全套的AMQP组件,可以针对每个虚拟主机进行权限和数据分配,并且每个虚拟主机间是完全隔离的。

2.2 connection

客户端与rabbitmq进行交互,需要建立一个tcp连接,这个连接就是connection。

2.3 channel

用于客户端与rabbitmq进行数据交互,大多数的数据操作都是在信道channel这个层面进行操作。

2.4 exchange

交换机主要与生产者打交道,生产者发送的消息通过exchange交换机分配到各个不同的Queue队列中,消费者通常只关注自己感兴趣的队列即可。

2.5 Queue

队列就是存放数据的最小单位。队列结构天生就具有FIFO的顺序。

3.RabbitMQ编程模式

只介绍官方提供的常见编程模式

3.1 Hello World

image.png

不需要指定exchange交换机,生产者指定queue推送消息,消费者指定queue进行消费消息。

3.2 work queues

image.png

跟hello world模式基本一致,区别是有多个消费者,多个消费者竞争消费,轮询消费消息。

3.3 publish/subscribe

image.png

发布订阅模式,每个消费者监听自己的队列,完成各自的业务逻辑

3.4 routing

image.png

根据routing key精确指定发送数据到对应的队列中

3.5 topic

image.png

根据routing key进行模糊匹配

4.springboot整合RabbitMQ

4.1 创建项目

使用spring initializer创建springboot-rabbitmq项目,然后创建模块producer、consumer,项目目录如下:

image.png

4.2 生产者

  1. 引用jar包
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
  1. 配置application.yaml
spring:
  rabbitmq:
    host: ip
    port: 5672
    username: guest
    password: guest
    # 虚拟机可以不配置,若配置则需要在管理端创建,默认是/
    virtual-host: boot-test
    # 开启publisher-confirm 有以下可选值
    # simple:同步等待confirm结果,直到超时
    # correlated:异步回调,定义ConfirmCallback。mq返回结果时会回调这个ConfirmCallback
    # NONE:默认不开启
    publisher-confirm-type: correlated
    publisher-returns: true
  1. 配置类

说明:生产者将消息推送到exchange交换机,exchange将消息转发到对应的queue队列中,所以配置类需要定义交换机、队列、交换机与队列关系等配置。

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Exchange;
import org.springframework.amqp.core.ExchangeBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.QueueBuilder;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitConfig {
    public static final String EXCHANGE_NAME = "boot-exchange";
    public static final String QUEUE_NAME = "boot-queue_ttl";

    // 1. 交换机
    @Bean("bootExchange")
    public Exchange exchange() {
        return ExchangeBuilder.topicExchange(EXCHANGE_NAME).durable(true).build();
    }

    // 2. 队列
    @Bean("bootQueue")
    public Queue queue() {
        return QueueBuilder.durable(QUEUE_NAME).ttl(10000).build();
    }

    // 3. 交换机与队列绑定关系
    @Bean("binding")
    public Binding binding(@Qualifier("bootExchange") Exchange exchange, @Qualifier("bootQueue") Queue queue) {
        return BindingBuilder.bind(queue).to(exchange).with("#.boot").noargs();
    }
}
  1. 失败回调

生产者将消息推送到exchange交换机可能失败,对应失败回调配置是 publisher-confirm-type: correlated

配置完成后需要写回调处理逻辑,创建类 RabbitmqConfirmCallBack

import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;

@Component
public class RabbitmqConfirmCallBack implements RabbitTemplate.ConfirmCallback {
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (ack) {
            System.out.println("发送消息到exchange成功...");
        } else {
            System.out.println("发送消息到exchange失败...\n" + cause);
        }
    }
}

exchange交换机将消息根据路由规则转发到队列queue中可能失败,对应失败回调配置是publisher-returns: true

配置完成后需要写回调处理逻辑,创建类 RabbitmqReturnCallBack

@Component
public class RabbitmqReturnCallBack implements RabbitTemplate.ReturnsCallback {
    /**
     * 消息路由失败回调
     */
    @Override
    public void returnedMessage(ReturnedMessage returned) {
        System.out.println(returned.toString());
    }
}
  1. 测试代码
import com.sxd.producer.common.RabbitmqConfirmCallBack;
import com.sxd.producer.common.RabbitmqReturnCallBack;
import com.sxd.producer.config.RabbitConfig;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;

@SpringBootTest
class RabbitmqTests {
    @Resource
    private RabbitTemplate template;

    @Resource
    private RabbitmqConfirmCallBack confirmCallBack;

    @Resource
    private RabbitmqReturnCallBack returnCallBack;

    @Test
    void producerTest() {
        template.setMandatory(true);
        template.setConfirmCallback(confirmCallBack);
        template.setReturnsCallback(returnCallBack);
        String msg = "ABC";
        template.convertAndSend(RabbitConfig.EXCHANGE_NAME, "producer.boot", msg);
    }

}
  1. 运行结果

image.png

image.png

4.3 消费者

  1. 引用jar包
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
  1. 配置application.yaml
spring:
  rabbitmq:
    host: ip
    port: 5672
    username: guest
    password: guest
    virtual-host: boot-test
    listener:
      # 容器类型simple或direct 简单理解为一对一;direct理解为一对多个消费者
      simple:
        acknowledge-mode: manual
        prefetch: 1
server:
  port: 8081
  1. 创建监听
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Service;

@Service
public class ListenerService {
    @RabbitListener(queues = "boot-queue_ttl")
    public void consumerTest(Message msg, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws Exception {
        System.out.println(new String(msg.getBody()));
        channel.basicAck(deliveryTag, true);
    }
}
  1. 测试结果

image.png

5.死信队列

5.1 什么是死信队列?

简单理解就是处理死信消息的队列,那什么是死信消息呢?

消息成为死信消息有三种情况:

  • 消息被否定,针对消费者端,首先配置参数default-requeue-rejected(默认值为true)为false, 然后使用channel.basicNack(批量拒绝)或者channel.basicReject(拒绝单条)方法拒绝消息。
  • 消息超过设定时长未被消费。
  • 超过队列长时,绑定了死信队列的消息会进入死信队列。

5.2 死信队列简单使用

使用步骤:

  • 声明死信交换机
  • 声明死信队列
  • 绑定交换机与队列关系
  • 业务队列设置死信交换机(消费者将消息推送到交换机后,由交换机路由到队列中,死信消息是由业务队列推送到死信交换机后,路由到死信队列中)

代码实现:

  1. 参数配置 default-requeue-rejected: false
spring:
  rabbitmq:
    host: 192.168.230.144
    port: 5672
    username: guest
    password: guest
    virtual-host: boot-test
    listener:
      # 容器类型simple或direct 简单理解为一对一;direct理解为一对多个消费者
      simple:
        acknowledge-mode: manual
        prefetch: 1
        default-requeue-rejected: false
  1. 配置类RabbitConfig
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Exchange;
import org.springframework.amqp.core.ExchangeBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.QueueBuilder;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

@Configuration
public class RabbitConfig {
    public static final String EXCHANGE_NAME = "boot-exchange";

    public static final String QUEUE_NAME = "boot-queue_ttl";

    public static final String DEAD_EXCHANGE_NAME = "dead-letter-exchange";

    public static final String DEAD_QUEUE_NAME = "dead-letter-queue";

    // 1. 交换机
    @Bean("bootExchange")
    public Exchange exchange() {
        return ExchangeBuilder.topicExchange(EXCHANGE_NAME).durable(true).build();
    }

    // 2. 队列
    @Bean("bootQueue")
    public Queue queue() {
        Map<String, Object> args = new HashMap<>(2);
        args.put("x-dead-letter-exchange", DEAD_EXCHANGE_NAME);
        args.put("x-dead-letter-routing-key", "#.boot");
        // 绑定死信交换机及路由信息
        return QueueBuilder.durable(QUEUE_NAME).withArguments(args).ttl(60000).build();
    }

    // 3. 交换机与队列绑定关系
    @Bean("binding")
    public Binding binding(@Qualifier("bootExchange") Exchange exchange, @Qualifier("bootQueue") Queue queue) {
        return BindingBuilder.bind(queue).to(exchange).with("#.boot").noargs();
    }

    // 死信交换机
    @Bean("deadLetterExchange")
    public Exchange deadLetterExchange() {
        return ExchangeBuilder.topicExchange(DEAD_EXCHANGE_NAME).build();
    }

    // 死信队列
    @Bean("deadLetterQueue")
    public Queue deadLetterQueue() {
        return QueueBuilder.durable(DEAD_QUEUE_NAME).build();
    }

    // 死信交换机与死信队列绑定关系
    @Bean("deadLetterBinding")
    public Binding deadLetterBinding(@Qualifier("deadLetterExchange") Exchange exchange,
                                     @Qualifier("deadLetterQueue") Queue queue) {
        return BindingBuilder.bind(queue).to(exchange).with("#.boot").noargs();
    }
}
  1. 测试结果

启动生产者服务后推送消息到业务队列中。 image.png

60秒消息超时后,消息推送到死信队列中。

image.png

5.3 利用死信队列实现延迟队列思路

延迟队列:等待一段时间后消费消息。

实现思路:设置队列超时时长,当消息超时则会进入死信队列,消费者指定消费的队列为死信队列。

消息 -> 业务队列(此时不消费) -> 超时进入私信队列 -> 消费者消费

6.RabbitMQ使用中的常见问题

6.1 消息可靠性投递

消息丢失的情况有以下几种:

  1. 生产者推送消息到exchange交换机时,有两种方法可以感知,一是手动开启(channel.txSelect)/提交(channel.txCommit)事务,二是本文案例中的confirm回调机制。
  2. exchange交换机路由到队列时,通过本文案例中return回调机制判断消息是否路由失败,然后处理路由失败消息。
  3. 消费者拉取消息时,通过ack应答机制,ack参数设置:NONE(自动ack)、MANUAL(手动ack)、AUTO(如果方法未抛异常,则发送ack;如果方法抛出异常,并且不是AmqpRejectAndDontRequeueException则发送nack,并且重新入队列;如果抛出异常是AmqpRejectAndDontRequeueException则发送nack且不会重新入队列)

6.2 幂等性问题

在代码进行处理,通过业务唯一id进行判断,防止由于网络原因或者rabbitmq重新入队机制导致消息重复消费。

6.3 消息顺序消费

单个队列+单消息推送(一个消费者)才能保证消息顺序消费。