RabbitMQ简单入门

服务端安装及配置

docker安装

使用docker安装RabbitMQ,注意,要选择tag包含management的镜像(包含web端管理插件)

docker pull rabbitmq:3.7.7-management
docker run -d --name rabbitmq3.7.7 -p 5672:5672 -p 15672:15672 -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=xxx rabbitmq:3.7.7-management

设置初始账号和密码,web页面地址如下

http://ip:15672

客户端访问

添加依赖

<dependency>
  <groupId>com.rabbitmq</groupId>
  <artifactId>amqp-client</artifactId>
  <version>5.4.3</version>
</dependency>

发布消息

public class TestRabbitClient {

  public static void main(String[] args) throws IOException, TimeoutException {
    String host = "";
    String queueName = "hello_queue";
    ConnectionFactory factory = new ConnectionFactory();
    factory.setHost(host);
    factory.setUsername("");
    factory.setPassword("");
    try (Connection connection = factory.newConnection();
        Channel channel = connection.createChannel()) {
        //队列名称 是否持久化到硬盘 是否消息共享(多个消费者消费) 是否自动删除
      channel.queueDeclare(queueName, false, false, false, null);
      String message = "Hello World!";
      //发布消息 使用的默认交换机
      channel.basicPublish("", queueName, null, message.getBytes());
      System.out.println(" [x] Sent '" + message + "'");
    }
  }
}

消费消息

channel.basicConsume(queueName, true, new DeliverCallback() {
      @Override
      public void handle(String consumerTag, Delivery message) throws IOException {
        System.out.println("receive message: " + new String(message.getBody()));
      }
    }, (x) -> {
      System.out.println("消息被中断");
    });

注意:channel被关闭之后 就不能接收到消息了

交换机

交换机类型

  • Direct (直连交换机): 最常使用,会根据routingkey进行精准匹配。直连交换机可以分发任务给多个工作者(worker)
  • Fanout (扇形交换机): 将消费分发给所有绑定的队列,而不会理会routingkey。优点是转发消息最快,性能最好。一般会用来处理广播消息(broadcast routing)。
  • Topic(主题交换机): 根据routingkey进行模糊匹配,将消息分发给一个或多个队列(delimited by dots)。 routingkey可以有通配符'','#'。 表示匹配一个单词,# 匹配0个或多个单词。当绑定建为'#'时,表示接收所有消息,和Fanout交换机类似了,当绑定键不包含*和#时,和Direct交换机功能类似了。
  • Headers (头交换机): 类似于直连交换机。不同点在于头交换机的路由规则建立在头属性之上而不是路由键,一般开发使用较少。

使用Fanout交换机实现广播

@Configuration
public class RabbitConfig {

    @Bean
    public FanoutExchange testExchange2() {
        return new FanoutExchange("test_exchange2");
    }
}
@Component
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(), //注意这里不要定义队列名称,系统会随机生成 spring.gen-4klrfbJ0TfmrGQnCpyPAQg这种格式
        exchange = @Exchange(value = "test_exchange2", type = ExchangeTypes.FANOUT))
)
public class RabbitMqReceiver {
    /**
     * 测试消息接收
     */
    @RabbitHandler
    public void process(String context, Message message, Channel channel) {
    }
}
@Component
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(), //注意这里不要定义队列名称,系统会随机生成 spring.gen-4klrfbJ0TfmrGQnCpyPAQg这种格式
        exchange = @Exchange(value = "test_exchange2", type = ExchangeTypes.FANOUT))
)
public class RabbitMqReceiver {
    /**
     * 测试消息接收
     */
    @RabbitHandler
    public void process(String context, Message message, Channel channel) {
    }
}

通过Spring来创建动态队列,动态绑定关系。如果我们不指定队列名称,Spring 会创建非持久化、排他、自动删除的队列,具体逻辑可以看 RabbitListenerAnnotationBeanPostProcessor 的 declareQueue 方法。

死信队列

当一条消息在队列中出现以下三种情况的时候,该消息就会变成一条死信。

  • 消息被拒绝(basic.reject / basic.nack),并且requeue = false
  • 消息TTL过期(通过x-message-ttl属性设置)
  • 队列达到最大长度(通过x-max-length属性设置)

当消息在一个队列中变成一个死信之后,如果配置了死信队列,它将被重新publish到死信交换机,死信交换机将死信投递到一个队列上,这个队列就是死信队列。

//声明订单取消正常队列 当消息过期时,将消息发布到死信队列
Map<String, Object> params = Map
        .of("x-dead-letter-exchange", "order_cancel_delay_queue_exchange",
            "x-dead-letter-routing-key", "order_cancel_delay_queue_key",
            "x-message-ttl", 20 * 1000);//消息过期时间 毫秒 在这里设置不灵活(不能改),可以在消息发布时设置
    channel.queueDeclare("order_cancel_queue", true, false, false, params);
    channel.exchangeDeclare("order_cancel_queue_exchange", BuiltinExchangeType.DIRECT, true);
    channel
        .queueBind("order_cancel_queue", "order_cancel_queue_exchange", "order_cancel_queue_key");

在发布消息时指定过期时间

//声明订单取消正常队列 当消息过期时,将消息发布到死信队列
Map<String, Object> params = Map
        .of("x-dead-letter-exchange", "order_cancel_delay_queue_exchange",
            "x-dead-letter-routing-key", "order_cancel_delay_queue_key");//消息过期时间 毫秒
    channel.queueDeclare("order_cancel_queue", true, false, false, params);
    channel.exchangeDeclare("order_cancel_queue_exchange", BuiltinExchangeType.DIRECT, true);
    channel
        .queueBind("order_cancel_queue", "order_cancel_queue_exchange", "order_cancel_queue_key");

    BasicProperties basicProperties = new Builder()
        .expiration("15000")//消息过期时间 15s
        .build();
    //向正常队列发布消息
    channel.basicPublish("order_cancel_queue_exchange", "order_cancel_queue_key", basicProperties,
        "hello".getBytes());

延时队列

  • 使用死信队列的消息过期方式来实现
    有一个问题,如果一个队列配置了死信队列,在发布消息时指定过期时间,第一条20S,第二条5S,那么第二条也会延迟到20S后执行,因为rabbitmq只会检查第一个消息是否过期。
  • 使用rabbitmq的延迟插件,创建的交换机类型为x-delayed-message,并设置x-delayed-type属性为direct,这种方式也没有第一种方式的问题。
public Exchange demo08Exchange() {
      Map<String, Object> args = new HashMap<>();
      args.put("x-delayed-type", "direct");
      return new CustomExchange(Demo01Message.NEW_DELAY_EXCHANGE, "x-delayed-message", true, false, args);
    }
//消息发布
rabbitTemplate.convertAndSend(Demo01Message.NEW_DELAY_EXCHANGE, Demo01Message.ROUTING_KEY, message, msg -> {
      msg.getMessageProperties().setDelay(20 * 1000);//延迟20S
      return msg;
    });

整合Spring

  • @EnableRabbit注解导入的RabbitListenerAnnotationBeanPostProcessor来处理@RabbitListener注解和@RabbitHandler注解
  • 将消息处理器包装成MultiMethodRabbitListenerEndpoint,注册到RabbitListenerEndpointRegistrar对象中
  • RabbitListenerEndpointRegistrar对象只是一个工具类,最终是注册到RabbitListenerEndpointRegistry对象中,它也是通过@EnableRabbit注解导入的
  • RabbitListenerEndpointRegistry将RabbitListenerEndpoint对象包装成MessageListenerContainer对象,它其中包含MessageListener对象(包装了消息处理器)
  • 在MessageListenerContainer的start()方法过程中,调用checkMismatchedQueues()方法
  • 通过调用CachingConnectionFactory创建connection,在这个过程中调用CompositeConnectionListener的onCreate()方法
  • ConnectionListener是通过RabbitAdmin对象添加到CachingConnectionFactory中的
  • 通过RabbitAdmin对象声明Queue,Exchange,Binding
  • 继续MessageListenerContainer的start()方法,将消息处理器包装成AsyncMessageProcessingConsumer对象,它是一个Runnable,交给线程池处理,可以通过 concurrentConsumers 属性来控制消费者数量从而并发消费
  • AsyncMessageProcessingConsumer中包含一个BlockingQueueConsumer对象,在它的start()方法中会注册消息处理的回调,具体类为InternalConsumer
  • 有消息到来时,InternalConsumer向BlockingQueueConsumer的queue中加入一条消息(推拉结合)
  • AsyncMessageProcessingConsumer一直在轮训获取BlockingQueueConsumer的queue,最大等待时间1秒。
  • 获取到消息,交给最终的消息处理器处理(就是我们标记@RabbitListener注解和@RabbitHandler注解的方法)

关于ConfirmCallback和ReturnCallback的使用

  • ConfirmCallback : 注意:只能确认消息是否能到达 Exchange
  • ReturnCallback : 注意 它是当交换机路由不到 队列的时候 它才会被触发

如何保证消息不被重复消费

每个消息添加一个Id,消费时判断是否已经消费过

//消息发送方
Message message = MessageBuilder.withBody("hello".getBytes(StandardCharsets.UTF_8))
        .setContentType(
            MessageProperties.CONTENT_TYPE_TEXT_PLAIN)
        .setContentEncoding(StandardCharsets.UTF_8.name())
        .setMessageId(UUID.randomUUID().toString()).build();
rabbitTemplate.convertAndSend("test_exchange1", "test_ronting_key1", message);
//消息消费方
@RabbitHandler
public void process(String context, Message message, Channel channel) {
    System.out.println("接收到消息: " + context);
    System.out.println(message.getMessageProperties().getMessageId());
    // 不管成功失败,向原队列确认消费
    try {
      channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    } catch (IOException e) {
      log.error("原队列确认消费失败:" + e.getLocalizedMessage());
    }
  }

参数中context的内容类似new String(message.getBody(), StandardCharsets.UTF_8);,源码位于 SimpleMessageConverter 的 fromMessage() 方法的100行

如何保证消息不丢失

  1. 生产端,使用confirm机制,发送失败重试
  2. RabbitMQ服务端,队列持久化,消息内容持久化
  3. 消费端,手动确认机制

集群部署

  1. 普通集群模式(无高可用性): 默认模式,以两个节点(rabbit01、rabbit02)为例来进行说明。对于Queue来说,消息实体只存在于其中一个节点rabbit01(或者rabbit02),rabbit01和rabbit02两个节点仅有相同的元数据,即队列的结构。当消息进入rabbit01节点的Queue后,consumer从rabbit02节点消费时,RabbitMQ会临时在rabbit01、rabbit02间进行消息传输,把A中的消息实体取出并经过B发送给consumer。

  2. 镜像集群模式(高可用性): 最常用的集群模式,把需要的队列做成镜像队列,存在于多个节点,属于RabbitMQ的HA方案。该模式解决了普通模式中的问题,其实质和普通模式不同之处在于,消息实体会主动在镜像节点间同步,而不是在客户端取数据时临时拉取。

当然,负载均衡还是需要我们自己做的。

参考

RabbitMQ 多实例 广播消息_松月的博客-程序员宅基地_rabbitmq 广播消息
RabbitMQ集群镜像模式部署
消息幂等

posted @ 2024-05-19 19:15  strongmore  阅读(38)  评论(0编辑  收藏  举报