RabbitMQ

大多应用中,可以通过消息服务中间件来提升系统异步通信,拓展解耦能力

消息服务中两个重要概念:

  • 消息代理(message broker)
  • 目的地(destination)

当消息发送者发送消息以后,将由消息代理接管,消息代理保证消息传递到指定目的地.

消息队列主要有两种形式的目的地,对应两种通信方式:

  • 队列(queue):点对点消息通信(point-to-point)
  • 主题(topic):发布(publish) 订阅(subscribe) 消息通信

点对点式:

消息发送者发送消息,消息代理将其放入一个队列中,消息接收者从中获取消息内容,消息读取后被移出队列;消息只有唯一的发送者和接受者,但并不是说只能有一个接收者.(多个接收者来拿这个消息,只能有一个拿到)

发布订阅式:

发送者发布消息到主题,多个接收者订阅这个主题,那么就会在消息到达时同时收到消息.

JMS(Java Message Service) JAVA消息服务:

基于JVM消息代理的规范. ActiveMQ.HornetMQ是JMS实现

AMQP(Advanced Message Queuing Protocol):

高级消息队列协议,也是消息代理的规范,兼容JMS,RabbitMQ是AMQP的实现

spring-jms和spring-rabbit提供了以上两种规范的支持

  • 需要ConnectionFactory的实现来连接消息代理
  • 提供JmsTemplate,RabbitTemplate来发送消息
  • @JmsListener,@RabbitListener注解在方法上监听消息代理发布的消息
  • @EnableJms,@EnableRabbit开启支持
  • 自动配置类: JmsAutoConfiguration,RabbitAutoConfiguration

RabbitMQ是由erlang开发的AMQP的开源实现

核心概念

  • Message

    消息,消息是不具名的,它由消息头和消息体组成;消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括routing-key(路由键),priority(相对于其他消息的优先权),delivery-mode(指出该消息可能需要持久化存储)等.

  • Publisher

    消息的生产者,也是一个向交换器发布消息的客户端程序.

  • Exchange

    交换器,用来接收生产者发送的消息,并将这些消息路由给服务器中的队列;Exchange有4种类型,不同类型的Exchange转发消息的策略有所区别.

    • direct(默认):通过指定路由键点对点方式与队列绑定
    • fanout:广播模式,将消息发送到所有队列
    • topic:允许对路由键制定模糊匹配,单词之间用点隔开,#匹配0个或多个单词,*匹配一个单词
    • headers 和默认基本一致,且性能不佳
  • Queue

    消息队列,用来保存消息直到发送给消费者;它是消息的容器,也是消息的终点;一个消息可投入一个或多个队列;消息一直在队列里面,等待消费者连接到这个队列将其取走.

  • Binding

    绑定,用于消息队列和交换器之间的关联;一个绑定器就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表.Exchange和Queue的绑定可以是多对多的关系.

  • Connection

    网络连接,比如一个TCP连接.

  • Channel

    信道,多路复用连接中的一条独立的双向数据流通道;信道是建立在真实的TCP连接内的虚拟连接,AMQP命令都是通过信道发送出去的,不管是发布消息,订阅队列还是接收消息,这些动作由是通过信道完成;因为对于操作系统来说建立和销毁TCP都是非常昂贵的开销,所以引入了信道的概念,以复用一条TCP连接.

  • Consumer

    消息的消费者,表示一个从消息队列中取得消息的客户端应用程序.

  • Virtual Host

    虚拟主机,表示一批交换器,消息队列和相关对象;虚拟主机是共享相同的身份认证和加密环境的独立服务器域;每个vhost本质上就是一个mini版的RabbitMQ服务器,拥有自己的队列,交换器,绑定和权限机制;vhost是AMQP概念的基础,必须在连接时指定,RabbitMQ默认的vhost是/.

  • Broker

    表示消息队列服务器实体

    broker 2020-03-27 131401

安装RabbitMQ

rabbitmq和erlang版本对照:https://www.rabbitmq.com/whic...

windows

Linux

  • 准备:

    yum install build-essential openssl openssl-devel unixODBC unixODBC-devel make gcc gcc-c++ kernel-devel m4 ncurses-devel tk tc xz
  • 下载&安装:

    wget www.rabbitmq.com/releases/erlang/erlang-18.3-1.el7.centos.x86_64.rpm
    wget http://repo.iotti.biz/CentOS/7/x86_64/socat-1.7.3.2-5.el7.lux.x86_64.rpm
    wget www.rabbitmq.com/releases/rabbitmq-server/v3.6.5/rabbitmq-server-3.6.5-1.noarch.rpm
    #先安装erlang
    rpm -ivh erlang-18.3-1.el7.centos.x86_64.rpm
    #socat密钥
    rpm -ivh socat-1.7.3.2-1.1.el7.x86_64.rpm 
    #然后rabbitmq-server
    rpm -ivh socat-1.7.3.2-1.1.el7.x86_64.rpm 
  • 配置文件:

    vim /usr/lib/rabbitmq/lib/rabbitmq_server-3.6.5/ebin/rabbit.app
    比如修改密码、配置等等,例如:loopback_users 中的 <<"guest">>,删除尖括号和引号只保留[guest]

    env下可修改端口号等环境变量:

    image-20200702120334702

  • 服务启动和停止:

    启动 rabbitmq-server start &
    停止 rabbitmqctl stop_app

启用管理插件:rabbitmq-plugins enable rabbitmq_management
访问地址:http://192.168.107.132:15672/

如果启动失败可查看端口占用情况: lsof -i:5672

命令行与管控台操作

  • 服务启动: rabbitmqctl start_app
  • 服务停止: rabbitmqctl stop_app
  • 节点状态: rabbitmqctl status
  • 创建虚拟主机: rabbitmqctl add_vhost < vhostpath >
  • 查看所有虚拟主机: rabbitmqctl list_vhosts
  • 查看所有队列: rabbitmqctl list_queues
  • 清除队列里的消息: rabbitmqctl -p vhostpath purge_queue blue
  • 移除所有数据: rabbitmqctl reset (要在服务停止之后使用)
  • 组成集群命令: rabbitmqctl join_cluster < clusternode> [--ram]存储模式
  • 修改集群节点的存储模式: rabbitmqctl change_cluster_node_type disc | ram
  • 摘除节点: rabbitmqctl forget_cluster_node [--offline]

管控台可以管理监控 连接(Connections),信道(Channels),交换器(Exchanges),队列(Queues) ;

Admin中可监控和添加虚拟主机(Virtual Host)

与springboot整合

创建springboot项目选择依赖web,rabbitMQ

<!--rabbitMQ-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

spring-boot-starter-amqp引入了spring-messaging, spring-rabbit

自动配置类:RabbitAutoConfiguration

配置了rabbitConnectionFactory连接工厂

RabbitPtoperties封装了RabbitMQ的配置

RabbitTemplate用来发送和接受消息

AmqpAdminRabbitMQ系统管理功能组件

相关配置:

#rabbit主机地址
spring.rabbitmq.host=192.168.107.108
#用户名 密码 端口
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.port=5672
#虚拟主机地址
spring.rabbitmq.virtual-host=/
# 支持发布确认
spring.rabbitmq.publisher-confirms=false
# 支持发布返回
spring.rabbitmq.publisher-returns=false
# 虚拟主机名称
spring.rabbitmq.virtual-host=/
# 采用手动应答
# spring.rabbitmq.listener.acknowledge-mode=manual
spring:
  application:
    name: mq-api
  rabbitmq:
    host: 192.168.107.132
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    publisher-confirms: true #监听消息是否 到达 exchange
    publisher-returns: true #监听消息是否 没有到达 queue
    template:
      mandatory: true #自动删除不可达消息,默认为false
      listener:
        simple:
          acknowledge-mode: manual #手动ack

在测试类中注入RabbitTemplate,有两种发送消息的方法

//Message需要自己构造,可定制消息体内容和消息头
rabbitTemplate.send(exchange,routeKey,message);

//object默认作为消息体,只需要传入要发送的对象,自动序列化发送给rabbitmq
rabbitTemplate.convertAndSend(exchange,routeKey,object);

使用AmqpAdmin组件可以编码的方式创建和管理Queue,Bindings以及Exchange

/**
     * declareXXX()用来创建组件
     * removeXXX(),deleteXXX()用来删除
     */
    @Autowired
    private AmqpAdmin amqpAdmin;

    /**
     * 创建Exchange
     */
    @Test
    public void createExchange(){
        //传入对应类型的实现类
        amqpAdmin.declareExchange(new DirectExchange("amqpadmin.exchange"));
        System.out.println("创建完成");
    }

    /**
     * 创建Queue
     */
    @Test
    public void createQueue(){
        //名字 是否持久化
        amqpAdmin.declareQueue(new Queue("amqpadmin.queue",true));
    }

    /**
     * 创建绑定规则
     */
    @Test
    public void createBindings(){
        //绑定目的地 目的地类型 Exchange Routing key 参数
        amqpAdmin.declareBinding(new Binding("amqpadmin.queue",                                            Binding.DestinationType.QUEUE,
                  "amqpadmin.exchange","amqp.haha",null));
    }

Exchange接口的实现类:

image-20200514172523355

例子:

     @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 点对点方式 发送
     */
    @Test
    public void contextLoads() {
        Map<String,Object> map = new HashMap<>();
        map.put("msg","这是第一个消息");
        map.put("data", Arrays.asList("helloWorld",123,true));
        //对象被默认序列化后发送出去
        rabbitTemplate.convertAndSend("exchange.direct","user.news",map);
    }

    /**
     * 接收消息
     */
    @Test
    public void receive(){
        Object o = rabbitTemplate.receiveAndConvert("user.news");
        System.out.println(o.getClass());
        System.out.println(o);
    }

    /**
     * 广播
     */
    @Test
    public void sendMsg(){
        rabbitTemplate.convertAndSend("exchange.fanout","",new Book("三国演义","罗贯中"));
    }

JSON序列化:

RabbitTemplate中的MessageConverter的实现默认为SimpleMessageConverter也就是jdk序列化的方式

MessageConverter的实现改成Jackson2JsonMessageConverter,数据就会以JSON的格式发送了

==注意实现Serializable接口==

import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;

@Configuration
public class MyAMQPConfig {
    @Bean
    public MessageConverter messageConverter(){
        return new Jackson2JsonMessageConverter();
    }
    //或者
    @Bean
    public MessageConverter jsonMessageConverter(ObjectMapper objectMapper) {
        return new Jackson2JsonMessageConverter(objectMapper);
    }
}

@RabbitListener监听队列自动接收消息

启动类加上@EnableRabbit注解

import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
@Service
public class BookService {
    /**
     * 监听指定的队列,多个用逗号分隔
     */
    @RabbitListener(queues = "user.news")
    public void receive(Book book){
        System.out.println("收到消息: "+book);
    }
    /**
     * message对象可获得消息头和消息体
     */
    @RabbitListener(queues = "user")
    public void receive02(Message message){
        System.out.println(message.getBody());
        System.out.println(message.getMessageProperties());
    }
}

构建生产者&消费者

原生API

  • 消费者

    import com.rabbitmq.client.*;
    import org.springframework.stereotype.Component;
    import java.io.IOException;
    import java.util.concurrent.TimeoutException;
    
    @Component
    public class Consumer {
    
        public static void main(String[] args) throws IOException, TimeoutException {
    
            //创建连接工厂,设置连接信息
            ConnectionFactory factory = new ConnectionFactory();
            factory.setHost("192.168.107.132");
            factory.setPort(5672);
            factory.setVirtualHost("/");
    
            //创建连接
            Connection connection = factory.newConnection();
    
            //创建信道
            Channel channel = connection.createChannel();
            String queueName = "test001";
            //创建一个队列 队列名称, 是否持久化, 此channel独占这个队列, autoDelete, 拓展参数
            channel.queueDeclare(queueName, true, false, false, null);
    
            //创建消费者
            MessageConsumer consumer = new MessageConsumer(channel);
            //设置channel 队列名称, autoAck, consumer
            channel.basicConsume(queueName, true, consumer);
            //在Broker接收到该Consumer的ack前,
            //Consumer在同一个时间点最多被分配qos个Message
            channel.basicQos(3);
    
        }
        //拓展DefaultConsumer
        static class MessageConsumer extends DefaultConsumer {
    
                public MessageConsumer(Channel channel) {
                    super(channel);
                }
                //处理消息的逻辑
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                    super.handleDelivery(consumerTag, envelope, properties, body);
                    System.out.println("message: " + new String(body));
                    System.out.println("DeliveryTag: " + envelope.getDeliveryTag());
                }
        }
    }

    启动消费者后查看管控台,发现connection,channel已成功连接,队列已成功创建

  • 生产者

    import com.rabbitmq.client.Channel;
    import com.rabbitmq.client.Connection;
    import com.rabbitmq.client.ConnectionFactory;
    import java.io.IOException;
    import java.util.concurrent.TimeoutException;
    
    public class Producer {
    
        public static void main(String[] args) throws IOException, TimeoutException {
    
            //创建连接工厂,设置连接信息
            ConnectionFactory factory = new ConnectionFactory();
            factory.setHost("192.168.107.132");
            factory.setPort(5672);
            factory.setVirtualHost("/");
    
            //创建连接
            Connection connection = factory.newConnection();
    
            //创建信道
            Channel channel = connection.createChannel();
            String msg = "Hello RabbitMQ";
            //使用默认exchange 发布消息
            for (int i =0; i < 5; i++) {
                // exchange, routeKey, properties, message
                channel.basicPublish("", "test001", null, msg.getBytes());
                System.out.println("发送消息: " + msg);
            }
            //关闭资源
            channel.close();
            connection.close();
        }
    }
  • 测试

    • 启动消费者后运行生产者,收到消息
    • 运行生产者后消息未被消费,再启动消费者后收到消息
  • tip: QueueingConsumer已被废弃

    QueueingConsumer内部用LinkedBlockingQueue来存放消息的内容,而LinkedBlockingQueue:一个由链表结构组成的有界队列,照先进先出的顺序进行排序 ,未指定长度的话,默认 此队列的长度为Integer.MAX_VALUE,那么问题来了,如果生产者的速度远远大于消费者的速度,也许没等到队列阻塞的条件产生(长度达到Integer.MAX_VALUE)内存就完蛋了,在老的版本你可以通过设置 rabbitmq的prefetch属性channel.basicQos(prefetch)来处理这个问题如果不设置可能出现内存问题(比如因为网络问题只能向rabbitmq生产不能消费,消费者恢复网络之后就会有大量的数据涌入,出现内存问题,oom fgc等)。

    而且写法很不合理不符合事件驱动,什么时候停止while循环也不能写的很优雅,所以在更高的版本直接被移除。取而代之的是DefaultConsumer,你可以通过扩展DefaultConsumer来实现消费者

spring-boot

  • 消费者

    spring:
      application:
        name: mq-api
      rabbitmq:
        host: 192.168.107.132
        port: 5672
        username: guest
        password: guest
        virtual-host: /
        template:
          mandatory: true #自动删除不可达消息,默认为false
          listener:
            simple:
              acknowledge-mode: manual #手动ack
              concurrency: 5 #消费者的最小数量
              max-concurrency: 10 #消费者的最大数量

    消费者核心注解: @RabbitListener, @RabbitHandler

    @RabbitListener是一个组合注解, 与@QueueBinding, @Queue, @Exchange 组合使用, 一次性搞定消费端交换机, 队列, 绑定, 路由, 并且配置监听功能等, ==配置信息建议从配置文件动态加载==

        @RabbitListener(bindings = @QueueBinding(
                value = @Queue(value = "queue_1", durable = "true"),
                exchange = @Exchange(value = "exchange_1",
                        durable = "true",
                        type = "topic",
                        ignoreDeclarationExceptions = "true"),
                key = "springboot.#"
        ))
        @RabbitHandler
        public void onMessage(Message message, Channel channel) throws IOException {
            System.out.println("消费端: " + new String(message.getBody()));
            Long deliveryTag = message.getMessageProperties().getDeliveryTag();
            //手动ack
            channel.basicAck(deliveryTag, false);
        }

    @RabbitListener 可以标注在类上面,需配合 @RabbitHandler 注解一起使用

    @RabbitListener 标注在类上面表示当有收到消息的时候,就交给 @RabbitHandler 的方法处理,具体使用哪个方法处理,根据 MessageConverter 转换后的参数类型

    @Component
    @RabbitListener(queues = "consumer_queue")
    public class Receiver {
    
        @RabbitHandler
        public void processMessage1(String message) {
            System.out.println(message);
        }
    
        @RabbitHandler
        public void processMessage2(byte[] message) {
            System.out.println(new String(message));
        }
        
    }

    @Payload@Headers注解用来指定消息体, 和接受消息头参数

    @RabbitHandler
    public void onOrderMessage(@Payload OrderMessage orderMessage, Channel channel,
                               @Headers Map<String, Object> Headers) throws IOException {
        System.out.println("消费端: " + orderMessage.getContent());
        Long deliveryTag = (Long) Headers.get(AmqpHeaders.DELIVERY_TAG);
        //手动ack
        channel.basicAck(deliveryTag, false);
    }
  • 生产者

    publisher-confirms: true #监听消息是否 到达 exchange
    publisher-returns: true #监听消息是否 没有到达 queue
    import org.springframework.amqp.core.Message;
    import org.springframework.amqp.core.MessageProperties;
    import org.springframework.amqp.rabbit.connection.CorrelationData;
    import org.springframework.amqp.rabbit.core.RabbitTemplate;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    
    import java.time.LocalDateTime;
    import java.util.UUID;
    @Component
    public class RabbitSender {
    
        @Autowired
        private RabbitTemplate rabbitTemplate;
    
        final RabbitTemplate.ConfirmCallback confirmCallback = new RabbitTemplate.ConfirmCallback() {
            /**
             * 异步监听 消息是否到达 exchange
             *
             * @param correlationData 包含消息的唯一标识的对象
             * @param ack             true 标识 ack,false 标识 nack
             * @param cause           nack 的原因
             */
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                System.out.println("-----confirmCallback-----");
                System.out.println("correlationData: " + correlationData);
                System.out.println("ack: " + ack);
                System.out.println("cause: " + cause);
                if (ack){
                    // todo 操作数据库,将 correlationId 这条消息状态改为投递成功
                }
    
                //log.error("消息投递失败,ID为: {},错误信息: {}", correlationData.getId(), cause);
                // todo 操作数据库,将 correlationId 这条消息状态改为投递失败
            }
        };
    
        final RabbitTemplate.ReturnCallback returnCallback = new RabbitTemplate.ReturnCallback() {
            /**
             * 异步监听 消息是否到达 queue
             * 触发回调要满足的条件有两个:
             * 1.消息已经到达了 exchange 
             * 2.消息无法到达 queue (比如 exchange 找不到跟 routingKey 对应的 queue)
             *
             * @param message    返回的消息
             * @param replyCode  回复 code
             * @param replyText  回复 text
             * @param exchange   交换机
             * @param routingKey 路由键
             */
            @Override
            public void returnedMessage(org.springframework.amqp.core.Message message, int replyCode, String replyText, String exchange, String routingKey) {
                System.out.println("-----returnCallback-----");
                System.out.println("message: " + message);
                System.out.println("replyCode: " + replyCode + ", replyText: " + replyText);
                System.out.println("exchange: " + exchange + ", routingKey: " + routingKey);
                // todo 操作数据库,将 correlationId 这条消息状态改为投递失败
            }
        };
    
        public void send(String exchange, String routingKey, String msg) throws Exception {
            //消息头
            MessageProperties properties = new MessageProperties();
            properties.getHeaders().put("send_time", LocalDateTime.now());
            properties.setContentType("text/plain");
            //构建消息
            Message message = new Message(msg.getBytes(), properties);
            //指定confirmCallback
            rabbitTemplate.setConfirmCallback(confirmCallback);
            //指定returnCallback
            rabbitTemplate.setReturnCallback(returnCallback);
            //指定唯一id
            CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
            //发送消息
            rabbitTemplate.convertAndSend(exchange, routingKey, msg, correlationData);
        }
        
        public void sendOrder(OrderMessage orderMessage) throws Exception {
            //指定confirmCallback
            rabbitTemplate.setConfirmCallback(confirmCallback);
            //指定returnCallback
            rabbitTemplate.setReturnCallback(returnCallback);
            //id + 时间戳
            CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
            //发送消息
            rabbitTemplate.convertAndSend("exchange_2", "springboot.def", orderMessage, correlationData);
        }
    }

Exchange交换机

用来接收生产者发送的消息,并将这些消息路由给服务器中的队列;

Exchange有4种类型,不同类型的Exchange转发消息的策略有所区别:

  • direct(默认): 通过指定路由键点对点方式与队列绑定
  • fanout: 广播模式,将消息发送到所有队列
  • topic: 允许对路由键制定模糊匹配,单词之间用点隔开,#匹配0个或多个单词,*匹配一个单词
  • headers: 和默认基本一致,且性能不佳

交换机属性

  • Name: 交换机名称
  • Durability: 是否持久化
  • AutoDelete: 当最后一个绑定到Exchange上的队列被删除后,自动删除该Exchange
  • Internal: 当前Exchange是否用于RabbitMQ内部使用,默认false
  • Arguments: 拓展参数, 用于拓展AMQP协议自定制化使用

测试三种类型的交换器

  1. 点击管控台的Add a new exchange创建交换机

    Durability:是否持久化选择Durable

    exchange.direct交换器,Type为direct

    exchange.fanout交换器,Type为fanout

    exchange.topic交换器,Type为topic

  2. Add a new queue添加消息队列

    user user.news user.orders edc.news

  3. 给队列绑定交换器

    exchange.direct/Bindings 选择 To Queue,使用Routing key同名字分别绑定,点击Unbind可解绑,fanout与之同理

    topic用通配符绑定

    *.newsuser.#
    user.newsuser
    edc.newsuser.news
    user.orders
  4. 接着分别在三个Exchange发送消息测试

    Exchanges中点击Publish message ,输入Routing key发送消息,如:direct.exchange.msg.helloWorld

    在Queues中点击对应的queue,点击Get Message获取消息,Ack Mode选择Ack message requeue false会清除上一次的消息

结果:

  • direct通过指定路由键点对点方式与队列绑定,所以只有完全匹配Routing key的队列才会收到消息.
  • fanout会将消息广播发送到所有队列
  • topic会有选择的模糊匹配Routing key对应的队列

Message自定义消息属性

  1. 构建BasicProperties

    //自定义headers属性
            HashMap<String, Object> attr = new HashMap<>();
            attr.put("status", 1);
            attr.put("detail", "这是附加属性");
            //构建消息属性
            AMQP.BasicProperties properties = new AMQP.BasicProperties().builder()
                    .deliveryMode(2) //1:不持久化消息, 2:持久化消息
                    .contentEncoding("UTF-8") //字符集
                    .expiration("10000") //过期时间
                    .headers(attr) //自定义属性
                    .build();
  2. 发布时指定properties

    String msg = "Hello RabbitMQ";
    //使用默认exchange 发布消息
    for (int i =0; i < 5; i++) {
        // exchange, routeKey, properties, message
        channel.basicPublish("", "test001", properties, msg.getBytes());
        System.out.println("发送消息: " + msg);
    }
    
    channel.close();
    connection.close();
  3. 获取properties

    //处理消息的逻辑
    @Override
    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
        super.handleDelivery(consumerTag, envelope, properties, body);
        System.out.println("message: " + new String(body));
        System.out.println("DeliveryTag: " + envelope.getDeliveryTag());
        System.out.println(properties.getHeaders().get("detail"));
    }

AMQP事务

我们知道可以通过持久化(交换机、队列和消息持久化)来保障我们在服务器崩溃时,重启服务器消息数据不会丢失。

但是我们无法确认当消息的发布者在将消息发送出去之后,消息到底有没有正确到达Broker代理服务器呢?如果不进行特殊配置的话,默认情况下发布操作是不会返回任何信息给生产者的,也就是默认情况下我们的生产者是不知道消息有没有正确到达Broker的。如果在消息到达Broker之前已经丢失的话,持久化操作也解决不了这个问题,因为消息根本就没到达代理服务器,这个是没有办法进行持久化的,那么当我们遇到这个问题又该如何去解决呢?

RabbitMQ中的消息确认机制,通过消息确认机制我们可以确保我们的消息可靠送达到我们的用户手中,即使消息丢失掉,我们也可以通过进行重复分发确保用户可靠收到消息。

RabbitMQ提供了两种消息确认方式:

  • 通过AMQP事务机制实现,这也是AMQP协议层面提供的解决方案;
  • 通过将channel设置成confirm模式来实现;

RabbitMQ中与事务有关的主要有三个方法:

  • txSelect() : 主要用于将当前channel设置成transaction模式
  • txCommit() : 用于提交事务
  • txRollback() : txRollback用于回滚事务

当我们使用txSelect提交开始事务之后,我们就可以发布消息给Broke代理服务器,如果txCommit提交成功了,则消息一定到达了Broke了,如果在txCommit执行之前Broker出现异常崩溃或者由于其他原因抛出异常,这个时候我们便可以捕获异常通过txRollback方法进行回滚事务了。

所以RabbitMQ事务中的主要代码为:

try {
    // 开启事务
    channel.txSelect();
    // 往队列中发出一条消息,使用rabbitmq默认交换机
    channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
    // 提交事务
    channel.txCommit();
} catch (Exception e) {
    e.printStackTrace();
    // 事务回滚
    channel.txRollback();
}finally{
    // 关闭频道和连接
    channel.close();
    connection.close()
}

先进行事务提交,然后开始发送消息,最后提交事务。

在使用事务时,在application.properties中,需要将确认模式更改为false。

# 支持发布确认
spring.rabbitmq.publisher-confirms=false

消息可靠性投递

  • 保障消息成功发出
  • 保障MQ节点的成功接收
  • 发送端收到MQ节点Broker确认答应
  • 完善的消息补偿机制

解决方案一

image-20200703124116225

消息落库, 对消息状态进行打标

  1. 生产者首先将自己的业务数据落库(如订单),然后将生成的消息落库,发送状态为待发送(status:0), 如果持久化失败,将进行快速失败
  2. 发送消息到MQ Broker
  3. 异步监听Broker收到消息后返回的确认答应
  4. 更新数据库信息状态为已发送(status:1)
  5. 如果超过最大发送时限仍然没有收到Borker的答应,使用分布式定时任务获取所有状态为待发送的信息记录

    • 分布式任务重复获取的问题,需要保证同一时间只有一个定时任务在获取消息记录
    • 可能消息刚刚入库就被获取,造成不必要的重发,需要设置消息超时的最大容忍限制
  6. 尝试重新发送
  7. 超过最大重试次数仍然没有答应,消息状态更新为坏消息(status:2),需要人工干预

缺点: 数据库操作频繁,高并发场景不适合

解决方案二

消息的延迟投递, 做二次确认, 回调检查

  • Upstream: 上游服务,消息生产者
  • Downstream: 下游服务,消息消费者
  • MQ Broker: MQ集群
  • Callback: 回调服务

image-20200703131009257

  1. 上游服务将自己的业务数据持久化之后,进行第一次消息投递
  2. 延迟消息投递检查, 在第一次消息发送后几分钟进行第二次投递,对应check队列,被Callback服务监听
  3. 下游服务监听消息并处理消息
  4. 如果消息被成功处理,发送确认信息到confirm队列,被Callback服务监听
  5. Callback监听到confirm队列中消息被正确处理的答应,将消息记录入库
  6. 如果下游服务没有返回答应消息,MSG DB中就没有该消息的记录,此时check队列中延迟消息到达,检查DB中是否存在此条消息记录,也就是验证该消息是否在之前被正确处理,如果没有Callback服务将发送RPC通知上游服务,将该消息对应的业务数据进行重新一轮发送

优点: 减少了数据库操作,补偿服务和主业务解耦

消费端-冥等性保障

冥等性

任意多次执行操作对资源本身所产生的影响均与一次执行的影响相同

解决的问题:

在高并发情况下,难免会发送消息的重复投递

在海量订单产生的业务高峰期,如何避免消息的重复消费问题?

消费端实现冥等性,就意味着,我们的消息永远不会被消费多次,即使我们收到了多条一样的消息

解决方案一

唯一ID + 指纹码机制, 利用数据库主键去重

  • 生成全局唯一ID + 指纹码(可能是某种业务规则,或第三方提供的唯一标识),利用数据库主键唯一性去重
  • SELETE COUNT(1) FROM T_ORDER WHERE ID = 唯一ID + 指纹码,查到数据库没有此条记录,再进行Insert并消费,否则说明此条记录已经被操作了,丢弃

优点: 实现简单

缺点: 高并发下存在数据库写入的性能瓶颈

解决方案: 根据ID进行分库分表进行路由算法分摊压力

解决方案二

利用Redis的原子性实现冥等

需要考虑的问题:

  • 我们是否要进行数据落库, 如果落库的话, 关键解决的问题是数据库和缓存如何做到原子性?
  • 如果不进行落库,那么都存储到缓存中, 如何设置定时同步的策略?

confirm消息确认机制

消息的确认,是指生产者投递消息后, 如果Broker收到消息,则会给生产者一个答应

生产者进行接收答应,用来确定这条消息是否正常的发送到Broker,这种方式也是消息可靠性投递的核心保障

Producer-Confirm

默认情况下rabbitmq的消费者采用平均分配的方式消费队列中的消息,而且默认开启autoAck自动答应机制,消费者在接受到消息后就会向rabbitmq发送消息已被消费的信号,此时消息将被删除,如果消费者消费到一半的时候宕机,便导致了业务无法正常完成;

为了保证消息不会丢失,如果一个消费者宕机,我们希望将未被处理的消息交给另一个消费者

实现原理:

==生产者将信道设置成confirm模式,一旦信道进入confirm模式,所有在该信道上面发布的消息都将会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,broker就会发送一个确认给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了==,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker回传给生产者的确认消息中delivery-tag域包含了确认消息的序列号,此外broker也可以设置basic.ack的multiple域,表示到这个序列号之前的所有消息都已经得到了处理;

confirm模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果RabbitMQ因为自身内部错误导致消息丢失,就会发送一条nack消息,生产者应用程序同样可以在回调方法中处理该nack消息;

开启confirm模式的方法:

  • 在channel上开启确认模式: channel.confirmSelect()
  • 在channel上添加监听: addConfirmListener, 监听成功和失败的返回结果,根据具体的结果对消息进行重新发送,或记录日志等后续处理

生产者通过调用channel的confirmSelect方法将channel设置为confirm模式,(注意一点,已经在transaction事务模式的channel是不能再设置成confirm模式的,即这两种模式是不能共存的),如果没有设置no-wait标志的话,broker会返回confirm.select-ok表示同意发送者将当前channel信道设置为confirm模式(从目前RabbitMQ最新版本3.6来看,如果调用了channel.confirmSelect方法,默认情况下是直接将no-wait设置成false的,也就是默认情况下broker是必须回传confirm.select-ok的,而且我也没找到我们自己能够设置no-wait标志的方法);

注意:发布确认和事务。(两者不可同时使用)在channel为事务时,不可引入确认模式;同样channel为确认模式下,不可使用事务

原生API

//消费端
public class ConfirmConsumer {

    public static void main(String[] args) throws IOException, TimeoutException {

        //创建连接工厂,设置连接信息
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.107.132");
        factory.setPort(5672);
        factory.setVirtualHost("/");

        //创建连接
        Connection connection = factory.newConnection();

        //创建信道
        Channel channel = connection.createChannel();

        //指定消息确认模式
        channel.confirmSelect();

        String exchangeName = "test.confirm.exchange";
        String routingKey = "confirm.#";
        String queueName = "test.confirm.queue";

        //创建exchange和队列
        channel.exchangeDeclare(exchangeName, "topic", true);
        channel.queueDeclare(queueName, true, false, false, null);
        //绑定exchange和队列
        channel.queueBind(queueName, exchangeName, routingKey);

        //创建消费者
        MessageConsumer consumer = new MessageConsumer(channel);
        //设置channel 队列名称, autoAck, consumer
        channel.basicConsume(queueName, true, consumer);

    }
    static class MessageConsumer extends DefaultConsumer {

        public MessageConsumer(Channel channel) {
            super(channel);
        }
        //消息处理逻辑
        @Override
        public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
            super.handleDelivery(consumerTag, envelope, properties, body);
            System.out.println("消费端: " + new String(body));
        }
    }
}
public class ConfirmProducer {

    public static void main(String[] args) throws IOException, TimeoutException {

        //创建连接工厂,设置连接信息
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.107.132");
        factory.setPort(5672);
        factory.setVirtualHost("/");

        //创建连接
        Connection connection = factory.newConnection();

        //创建信道
        Channel channel = connection.createChannel();

        //指定消息确认模式
        channel.confirmSelect();

        String exchangeName = "test.confirm.exchange";
        String routingKey = "confirm.save";

        //发送消息
        String msg = "Hello RabbitMQ Send confirm message!";
        channel.basicPublish(exchangeName, routingKey, null, msg.getBytes());

        //----添加确认监听----
        channel.addConfirmListener(new ConfirmListener() {
            /**
             * 成功答应
             * @param deliveryTag 消息唯一标识
             * @param multiple 是否批量
             * @throws IOException
             */
            @Override
            public void handleAck(long deliveryTag, boolean multiple) throws IOException {
                System.out.println("----------------ack!--------------");
            }
            //否认答应
            @Override
            public void handleNack(long deliveryTag, boolean multiple) throws IOException {
                System.err.println("----------------no ack--------------");
            }
        });
    }
}

spring-boot

配置:

  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    publisher-confirms: true #监听消息是否 到达 exchange
    publisher-returns: true #监听消息是否 没有到达 queue
    template:
      mandatory: true #自动删除不可达消息,默认为false
    listener:
      simple:
        acknowledge-mode: manual

消息实体:

import java.io.Serializable;

/**
 * 订单的消息实体
 */
public class OrderMessage implements Serializable {

    /**
     * 业务id,在业务系统中的唯一。比如 订单id、支付id、商品id ,消息消费端可以通过该 id 避免消息重复消费
     */
    private String id;

    // 其他业务字段
    private String name;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

发送消息并异步监听:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.UUID;


/**
 * 发送消息并异步监听 ack
 */
@Component
public class OrderMessageSendAsync implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {

    private Logger logger = LoggerFactory.getLogger(OrderMessageSendAsync.class);

    private RabbitTemplate rabbitTemplate;

    /**
     * 通过构造函数注入 RabbitTemplate 依赖
     *
     * @param rabbitTemplate
     */
    @Autowired
    public OrderMessageSendAsync(RabbitTemplate rabbitTemplate) {
        this.rabbitTemplate = rabbitTemplate;
        // 设置消息到达 exchange 时,要回调的方法,每个 RabbitTemplate 只支持一个 ConfirmCallback
        rabbitTemplate.setConfirmCallback(this);
        // 设置消息无法到达 queue 时,要回调的方法
        rabbitTemplate.setReturnCallback(this);
    }

    /**
     * 发送消息
     *
     * @param exchange   交换机
     * @param routingKey 路由建
     * @param message    消息实体
     */
    public void sendMsg(String exchange, String routingKey, Object message) {
        // 构造包含消息的唯一id的对象,id 必须在该 channel 中始终唯一
        CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
        logger.info("ID为: {}", correlationData.getId());
        // todo 先将业务数据入库,在将 message 的数据库ID 、message的消息id message的初始状态(发送中)等信息入库

        // 完成 数据落库,消息状态打标后,就可以安心发送 message
        rabbitTemplate.convertAndSend(exchange, routingKey, message, correlationData);

        try {
            logger.info("发送消息的线程处于休眠状态, confirm 和 returnedMessage 方法依然处于异步监听状态");
            Thread.sleep(1000*15);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }


    /**
     * 异步监听 消息是否到达 exchange
     *
     * @param correlationData 包含消息的唯一标识的对象
     * @param ack             true 标识 ack,false 标识 nack
     * @param cause           nack 的原因
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {

        if (ack) {
            logger.info("消息投递成功,ID为: {}", correlationData.getId());
            // todo 操作数据库,将 correlationId 这条消息状态改为投递成功
            return;
        }

        logger.error("消息投递失败,ID为: {},错误信息: {}", correlationData.getId(), cause);
        // todo 操作数据库,将 correlationId 这条消息状态改为投递失败

    }

    /**
     * 异步监听 消息是否到达 queue
     * 触发回调要满足的条件有两个:1.消息已经到达了 exchange 2.消息无法到达 queue (比如 exchange 找不到跟 routingKey 对应的 queue)
     *
     * @param message    返回的消息
     * @param replyCode  回复 code
     * @param replyText  回复 text
     * @param exchange   交换机
     * @param routingKey 路由键
     */
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        // correlationId 就是发消息时设置的 id
        String correlationId = message.getMessageProperties().getHeaders().get("spring_returned_message_correlation").toString();

        logger.error("没有找到对应队列,消息投递失败,ID为: {}, replyCode {} , replyText {}, exchange {} routingKey {}",
                correlationId, replyCode, replyText, exchange, routingKey);
        // todo 操作数据库,将 correlationId 这条消息状态改为投递失败
    }
}

测试:

import com.alibaba.fastjson.JSONObject;
import com.wqlm.rabbitmq.send.MessageSend.OrderMessageSendAsync;
import com.wqlm.rabbitmq.send.message.OrderMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/")
public class SendMessageController {

    @Autowired
    private OrderMessageSendAsync orderMessageSendAsync;

    /**
     * 测试发送消息并异步接收响应
     */
    @GetMapping("/test")
    public void test(){
        OrderMessage orderMessage = new OrderMessage("123", "订单123");
        // 序列化成json ,OrderMessage 也可以 implements Serializable 这样就不需要序列化成json
        String message = JSONObject.toJSONString(orderMessage);
        orderMessageSendAsync.sendMsg("exchangeName", "routingKeyValue", message);
    }
}

springAMQP

SimpleMessageListenerContainer

简单消息监听容器: 我们使用SimpleMessageListenerContainer容器设置消费队列监听,然后设置具体的监听Listener进行消息消费具体逻辑的编写

  • SimpleMessageListenerContainer可监听多个队列
  • 设置事务特性, 事务管理器, 事务属性, 事务容量, 回滚消息等
  • 设置消费者数量, 最小最大数量, 批量消费
  • 设置消息签收模式 NONE, AUTO, MANUAL
  • 是否重回队列, 异常捕获handler函数
  • 设置消费则标签生成策略, 是否独占模式, 消费者属性等
  • 设置具体监听器, 转换器

注意: SimpleMessageListenerContainer可以在运行中动态的改变其消费者数量的大小, 接收消息的模式等; 很多基于RabbitMQ的自定制后台管理在进行动态设置的时候, 也是根据SpringAMQP这一特性取实现的

@Bean
public SimpleMessageListenerContainer messageListenerContainer(ConnectionFactory connectionFactory){
    SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
    container.setConnectionFactory(connectionFactory);
    container.setQueueNames("test.ack", "test.order.queue"); //监听的队列, 多个用逗号分隔
    //container.setQueues(queue001(), queue002()); //监听的队列, 注入方式
    container.setConcurrentConsumers(1); //当前的消费者数量
    container.setMaxConcurrentConsumers(5); //最大消费者数量
    container.setDefaultRequeueRejected(false); //默认不重回队列
    container.setAcknowledgeMode(AcknowledgeMode.MANUAL); //手动ack模式
    //container.setAfterReceivePostProcessors(MessagePostProcessor); 在接收到消息之前做什么

    //消费端标签生成策略 (可lambda)
    container.setConsumerTagStrategy(new ConsumerTagStrategy() {
        @Override
        public String createConsumerTag(String queue) {
            return queue + "_" + UUID.randomUUID().toString();
        }
    });

    //消息监听处理
    container.setMessageListener((ChannelAwareMessageListener) (message, channel) ->{

        System.out.println("===监听到消息===");
        System.out.println(new String(message.getBody()));

        if (message.getMessageProperties().getHeaders().get("err") == null){
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
            System.out.println("===消息已确认===");
        }else {
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),false);
            System.out.println("===拒绝消息===");
        }
    });

    return container;
}

MessageListenerAdapter

消息监听适配器: 允许你自定义MessageListener, 通过MessageListenerAdapter来适配

@Bean
public SimpleMessageListenerContainer messageListenerContainer(ConnectionFactory connectionFactory){
    SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
    container.setConnectionFactory(connectionFactory);
    container.setQueueNames("test.ack", "test.order.queue"); //监听的队列, 多个用逗号分隔
    //container.setQueues(queue001(), queue002()); //监听的队列, 注入方式
    container.setConcurrentConsumers(1); //当前的消费者数量
    container.setMaxConcurrentConsumers(5); //最大消费者数量
    container.setDefaultRequeueRejected(false); //默认不重回队列
    container.setAcknowledgeMode(AcknowledgeMode.MANUAL); //手动ack模式
    //container.setAfterReceivePostProcessors(MessagePostProcessor); 在接收到消息之前做什么

    //消费端标签生成策略 (可lambda)
    container.setConsumerTagStrategy(new ConsumerTagStrategy() {
        @Override
        public String createConsumerTag(String queue) {
            return queue + "_" + UUID.randomUUID().toString();
        }
    });

    //适配自定义消息监听处理
    MessageListenerAdapter adapter = new MessageListenerAdapter(new MessageDelegate());
    adapter.setDefaultListenerMethod("consumeMessage"); //改变默认方法
    adapter.setMessageConverter(new TextMessageConverter()); //消息转换器
    //设置消息监听器
    container.setMessageListener(adapter);

    return container;
}
/**
 * 自定义消息监听器
 */
@Component
public class MessageDelegate {

    public void handleMessage(String messageBody){
        System.out.println("默认方法, 消息内容: " + messageBody);
    }

    public void consumeMessage(byte[] messageBody){
        System.out.println("字节数组方法, 消息内容: " + messageBody);
    }
}

==自定义消息监听器的处理方法名称, 定义在MessageListenerAdapter中==:

public class MessageListenerAdapter extends AbstractAdaptableMessageListener {
    private final Map<String, String> queueOrTagToMethodName;
    public static final String ORIGINAL_DEFAULT_LISTENER_METHOD = "handleMessage";
    private Object delegate;
    private String defaultListenerMethod;

    public MessageListenerAdapter() {
        this.queueOrTagToMethodName = new HashMap();
        this.defaultListenerMethod = "handleMessage";
        this.delegate = this;
    }
    ...

你可以通过adapter.setDefaultListenerMethod("consumeMessage");==修改负责消息监听处理的方法, 以适配不同的消息处理方式==


consumeMessage的入参是byte[]类型, 假设我们发送的消息是String类型就会抛出异常, 可使用自定义MessageConvert来转换类型

/**
 * 文本消息转换器
 */
@Component
public class TextMessageConverter implements MessageConverter {

    // Object 转 Message
    @Override
    public Message toMessage(Object object, MessageProperties messageProperties) throws MessageConversionException {
        return new Message(object.toString().getBytes(), messageProperties);
    }

    // Message 转 Object
    @Override
    public Object fromMessage(Message message) throws MessageConversionException {
        String contentType = message.getMessageProperties().getContentType();

        if (null != contentType && contentType.contains("text")){
            return new String(message.getBody());
        }

        return message.getBody();
    }
}

使用adapter.setMessageConverter(new TextMessageConverter());==指定消息转换器, 并在发送消息时指定ContentType;==

//消息头
MessageProperties properties = new MessageProperties();
properties.getHeaders().put("send_time", LocalDateTime.now());
properties.setContentType("text/plain");

可以设置多个队列通过队列名或Tag与不同方法的映射绑定

queueOrTagToMethodName: 队列标识与方法名称组成的集合

==即指定队列里的消息会被所绑定的方法所接收处理==

MessageListenerAdapter adapter = new MessageListenerAdapter(new MessageDelegate());
HashMap<String, String> queueOrTagToMethodName = new HashMap<>();
queueOrTagToMethodName.put("order.queue", "handleOrder");
queueOrTagToMethodName.put("product", "handleProduct");
//设置队列与监听处理方法的映射绑定
adapter.setQueueOrTagToMethodName(queueOrTagToMethodName); 
container.setMessageListener(adapter);

消息监听器处理Ack

  • 消息通过 ACK 确认是否被正确接收,每个 Message 都要被确认(acknowledged),可以手动去 ACK 或自动 ACK
  • 自动确认会在消息发送给消费者后立即确认,但存在丢失消息的可能,如果消费端消费逻辑抛出异常,也就是消费端没有处理成功这条消息,那么就相当于丢失了消息
  • 如果消息已经被处理,但后续代码抛出异常,使用 Spring 进行管理的话消费端业务逻辑会进行回滚,这也同样造成了实际意义的消息丢失
  • 如果手动确认则当消费者调用 ack、nack、reject 几种方法进行确认,手动确认可以在业务失败后进行一些操作,如果消息未被 ACK 则会发送到下一个消费者
  • 如果某个服务没有 ACK ,则 RabbitMQ 不会再发送数据给它,因为 RabbitMQ 认为该服务的处理能力有限
  • ACK 机制还可以起到限流作用,比如在接收到某条消息时休眠几秒钟
  • 消息确认模式有:

    • AcknowledgeMode.NONE:自动确认
    • AcknowledgeMode.AUTO:根据情况确认
    • AcknowledgeMode.MANUAL:手动确认

确认消息(局部方法处理消息)

  • 默认情况下消息消费者是自动 ack (确认)消息的,如果要手动 ack(确认)则需要修改确认模式为 manual

    spring:
      rabbitmq:
        listener:
          simple:
            acknowledge-mode: manual
  • 或在 RabbitListenerContainerFactory 中进行开启手动 ack

    @Bean
    public RabbitListenerContainerFactory<?> rabbitListenerContainerFactory(ConnectionFactory connectionFactory){
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        factory.setMessageConverter(new Jackson2JsonMessageConverter());
        factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);             //开启手动 ack
        return factory;
    }
  • 确认消息 @RabbitHandler

    @RabbitHandler
    public void processMessage2(String message,Channel channel,@Header(AmqpHeaders.DELIVERY_TAG) long tag) {
        System.out.println(message);
        try {
            channel.basicAck(tag,false); // 确认消息
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
  • 需要注意的 basicAck 方法需要传递两个参数

    • deliveryTag(唯一标识 ID):当一个消费者向 RabbitMQ 注册后,会建立起一个 Channel ,RabbitMQ 会用 basic.deliver 方法向消费者推送消息,这个方法携带了一个 delivery tag, 它代表了 RabbitMQ 向该 Channel 投递的这条消息的唯一标识 ID,是一个单调递增的正整数,delivery tag 的范围仅限于 Channel
    • multiple:为了减少网络流量,手动确认可以被批处理,当该参数为 true 时,则可以一次性确认 delivery_tag 小于等于传入值的所有消息

手动否认 拒绝消息

  • 发送一条错误消息

    public void sendError(String exchange, String routeKey, String body){
            MessageProperties properties = new MessageProperties();
            properties.getHeaders().put("error", "这是一条错误的消息");
            properties.setMessageId(UUID.randomUUID().toString());
            Message message = new Message(body.getBytes(), properties);
            rabbitTemplate.send(exchange, routeKey, message);
            System.out.println("发送了错误信息");
        }
  • 消费者获取消息时检查到头部包含 error 则 nack 消息

    @RabbitListener(queues = "test.ack")
    @RabbitHandler
    public void processMessage(String message, Channel channel,@Headers Map<String,Object> map) {
        System.out.println(message);
        if (map.get("error")!= null){
            System.out.println("错误的消息");
            try {
                channel.basicNack((Long)map.get(AmqpHeaders.DELIVERY_TAG),false,true);      //否认消息
                return;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        try {
            channel.basicAck((Long)map.get(AmqpHeaders.DELIVERY_TAG),false);            //确认消息
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
  • 此时控制台重复打印,说明该消息被 nack 后一直重新入队列然后一直重新消费

    hello
    错误的消息
    hello
    错误的消息
    hello
    错误的消息
    hello
    错误的消息
  • 也可以拒绝该消息,消息会被丢弃,不会重回队列

    channel.basicReject((Long)map.get(AmqpHeaders.DELIVERY_TAG),false); //拒绝消息

确认消息(全局处理消息)

  • 自动确认涉及到一个问题就是如果在处理消息的时候抛出异常,消息处理失败,但是因为自动确认而导致 Rabbit 将该消息删除了,造成消息丢失

    //NONE
    @Bean
    public SimpleMessageListenerContainer messageListenerContainer(ConnectionFactory connectionFactory){
        SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        container.setQueueNames("consumer_queue");                 // 监听的队列
        container.setAcknowledgeMode(AcknowledgeMode.NONE);     // NONE 代表自动确认
        container.setMessageListener((MessageListener) message -> {         //消息监听处理
            System.out.println("====接收到消息=====");
            System.out.println(new String(message.getBody()));
            //相当于自己的一些消费逻辑抛错误
            throw new NullPointerException("consumer fail");
        });
        return container;
    }
  • 手动确认消息

    //MANUAL
    @Bean
    public SimpleMessageListenerContainer messageListenerContainer(ConnectionFactory connectionFactory){
        SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        container.setQueueNames("consumer_queue"); // 监听的队列, 多个用逗号分隔
        //container.setQueues(queue001(), queue002()); // 监听的队列, 注入方式
        container.setAcknowledgeMode(AcknowledgeMode.MANUAL); // 手动确认
        
        container.setMessageListener((ChannelAwareMessageListener) (message, channel) -> {      //消息处理
            System.out.println("====监听到消息=====");
            System.out.println(new String(message.getBody()));
            if(message.getMessageProperties().getHeaders().get("error") == null){
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
                System.out.println("消息已经确认");
            }else {
                //channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,false);
                channel.basicReject(message.getMessageProperties().getDeliveryTag(),false);
                System.out.println("消息拒绝");
            }
    
        });
        return container;
    }
  • AcknowledgeMode 除了 NONE 和 MANUAL 之外还有 AUTO ,它会根据方法的执行情况来决定是否确认还是拒绝(是否重新入queue)

    • 如果消息成功被消费(成功的意思是在消费的过程中没有抛出异常),则自动确认
    • 当抛出 AmqpRejectAndDontRequeueException 异常的时候,则消息会被拒绝,且 requeue = false(不重新入队列)
    • 当抛出 ImmediateAcknowledgeAmqpException 异常,则消费者会被确认
    • 其他的异常,则消息会被拒绝,且 requeue = true(==如果此时只有一个消费者监听该队列,则有发生死循环的风险,多消费端也会造成资源的极大浪费,这个在开发过程中一定要避免的==)。可以通过 setDefaultRequeueRejected(默认是true)去设置
//AUTO
@Bean
public SimpleMessageListenerContainer messageListenerContainer(ConnectionFactory connectionFactory){
    SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
    container.setConnectionFactory(connectionFactory);
    container.setQueueNames("consumer_queue");              // 监听的队列
    container.setAcknowledgeMode(AcknowledgeMode.AUTO);     // 根据情况确认消息
    container.setMessageListener((MessageListener) (message) -> {
        System.out.println("====接收到消息=====");
        System.out.println(new String(message.getBody()));
        //抛出NullPointerException异常则重新入队列
        //throw new NullPointerException("消息消费失败");
        //当抛出的异常是AmqpRejectAndDontRequeueException异常的时候,则消息会被拒绝,且requeue=false
        //throw new AmqpRejectAndDontRequeueException("消息消费失败");
        //当抛出ImmediateAcknowledgeAmqpException异常,则消费者会被确认
        throw new ImmediateAcknowledgeAmqpException("消息消费失败");
    });
    return container;
}

消息可靠总结

  • 持久化

    • exchange要持久化
    • queue要持久化
    • message要持久化
  • 消息确认

    • 启动消费返回(@ReturnList注解,生产者就可以知道哪些消息没有发出去)
    • 生产者和Server(broker)之间的消息确认
    • 消费者和Server(broker)之间的消息确认

Return消息机制

Return Listener 用于处理一些不可路由的消息

我们的消息生产者,通过指定一个Exchange和Routingkey,把消息送达到某一个队列中取,然后我们的消费者监听队列,进行消费处理操作

但是在某些情况下,如果我们在发送消息的时候,当前的exchange不存在或者指定的路由key路由不到,这个时候需要监听这种不可达的消息,就要使用Return Listener

关键配置项:

  • Mandatory: 如果为true, 则监听器会接收到路由不可达的消息,然后进行后续处理; 如果为false, 那么broker端自动删除改消息

消费端:

public class ReturnConsumer {

    public static void main(String[] args) throws IOException, TimeoutException {

        //创建连接工厂,设置连接信息
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.107.132");
        factory.setPort(5672);
        factory.setVirtualHost("/");

        //创建连接
        Connection connection = factory.newConnection();
        //创建信道
        Channel channel = connection.createChannel();

        String exchangeName = "test.return.exchange";
        String routingKey = "return.#";
        String queueName = "test.return.queue";

        //创建exchange
        channel.exchangeDeclare(exchangeName, "topic", true, false, null);
        //创建一个队列 队列名称, 是否持久化, 此channel独占这个队列, autoDelete, 拓展参数
        channel.queueDeclare(queueName, true, false, false, null);
        //绑定
        channel.queueBind(queueName, exchangeName, routingKey);

        //创建消费者
        MessageConsumer consumer = new MessageConsumer(channel);
        //指定channel的消费者 autoAck
        channel.basicConsume(queueName, true, consumer);

    }
    static class MessageConsumer extends DefaultConsumer {

        public MessageConsumer(Channel channel) {
            super(channel);
        }
        //处理消息的逻辑
        @Override
        public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
            super.handleDelivery(consumerTag, envelope, properties, body);
            System.out.println("message: " + new String(body));
            System.out.println("DeliveryTag: " + envelope.getDeliveryTag());
        }
    }
}

生产端:

public class ReturnProducer {

    public static void main(String[] args) throws IOException, TimeoutException {

        //创建连接工厂,设置连接信息
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.107.132");
        factory.setPort(5672);
        factory.setVirtualHost("/");

        //创建连接
        Connection connection = factory.newConnection();
        //创建信道
        Channel channel = connection.createChannel();

        String exchangeName = "test.return.exchange";
        String routingKey = "return.save";
        String routingKeyError = "abc.save";
        String msg = "Hello RabbitMQ Return Listener!";

        //---添加return listener---
        channel.addReturnListener(new ReturnListener() {
            /**
             * 监听不可达消息
             * @param replyCode 响应码
             * @param replyText 响应文本
             * @param exchange 交换器
             * @param routingKey 路由键
             * @param basicProperties 属性
             * @param body 消息
             * @throws IOException
             */
            @Override
            public void handleReturn(int replyCode, String replyText, String exchange,
                                     String routingKey, AMQP.BasicProperties basicProperties, byte[] body) throws IOException {
                System.out.println("------------------handle return---------------");
                System.out.println("replyCode: " + replyCode);
                System.out.println("replyText: " + replyText);
                System.out.println("exchange: " + exchange);
                System.out.println("routingKey: " + routingKey);
                System.out.println("properties: " + basicProperties.getMessageId());
                System.out.println("body: " + new String(body));
            }
        });

        /* Mandatory = true 才能使监听起效! */
        //发送消息 exchange, routingKey, Mandatory, properties, body
        channel.basicPublish(exchangeName, routingKey, true, null, msg.getBytes());
        //发送消息到不存在的routingKey
        channel.basicPublish(exchangeName, routingKeyError, true, null, msg.getBytes());
    }
}

发送消息到不存在的routingKey,将会触发handleReturn回调,打印信息如下:

------------------handle return---------------
replyCode: 312
replyText: NO_ROUTE
exchange: test.return.exchange
routingKey: abc.save
properties: null
body: Hello RabbitMQ Return Listener!

==Mandatory = true 才能使监听起效!==

消费端限流

假设一个场景, 首先我们RabbitM服务器有上万条未处理的消息,此时开启一个消费端,会出现下面的情况:

巨量的消息瞬间被推送过来,单个消费端无法同时处理这么多的数据,造成服务的崩溃

RabbitMQ提供了一种qos(服务治理保证)功能, 即在非自动确认消息的前提下,如果一定数目的消息(通过基于consume或者channel设置Qos的值) 未被确认前,不进行消费新的消息

实际开发中 autoAck 一定是false

void BasicQos(int prefetchSize, short prefetchConut, bool global)

  • prefetchSize: 消息大小的限制, 一般设置为0不限制
  • prefetchConut: 在Broker接收到该Consumer的ack之前,Consumer在同一个时间点最多被分配最多处理消息数量
  • global: true应用在监听此channel的所有consume, false仅应用在当前consume

prefetchSize和global这两项,rabbitmq没有实现,暂且不研究;

prefetchConut只在no_ask=false的情况下生效

消费端:

public class LimitConsumer {

    public static void main(String[] args) throws IOException, TimeoutException {

        //创建连接工厂,设置连接信息
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.107.132");
        factory.setPort(5672);
        factory.setVirtualHost("/");

        //创建连接
        Connection connection = factory.newConnection();
        //创建信道
        Channel channel = connection.createChannel();

        String exchangeName = "test.qos.exchange";
        String queueName = "test.qos.queue";
        String routingKey = "qos.#";

        //创建exchange
        channel.exchangeDeclare(exchangeName, "topic", true, false, null);
        //创建一个队列 队列名称, 是否持久化, 此channel独占这个队列, autoDelete, 拓展参数
        channel.queueDeclare(queueName, true, false, false, null);
        //绑定
        channel.queueBind(queueName, exchangeName, routingKey);

        //----QOS限流, 每次处理1条消息----
        channel.basicQos(0, 1, false);
        //设置consumer 设置autoAck false
        channel.basicConsume(queueName, false, new MessageConsumer(channel));

    }
    static class MessageConsumer extends DefaultConsumer {

        //channel
        private Channel channel;

        public MessageConsumer(Channel channel) {
            super(channel);
            this.channel = channel;
        }
        //处理消息的逻辑
        @Override
        public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
            super.handleDelivery(consumerTag, envelope, properties, body);
            System.out.println("message: " + new String(body));
            System.out.println("DeliveryTag: " + envelope.getDeliveryTag());
            //ack答应
            channel.basicAck(envelope.getDeliveryTag(), false);
        }
    }
}

如果将basicAck注释掉,consumer将只会收到第一条信息

生产端:

public class LimitProducer {

    public static void main(String[] args) throws IOException, TimeoutException {

        //创建连接工厂,设置连接信息
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.107.132");
        factory.setPort(5672);
        factory.setVirtualHost("/");

        //创建连接
        Connection connection = factory.newConnection();
        //创建信道
        Channel channel = connection.createChannel();

        String exchangeName = "test.qos.exchange";
        String routingKey = "qos.save";
        String msg = "Hello RabbitMQ QOS Message!";

        //发送5条消息
        for (int i = 0; i < 5; i++){
            channel.basicPublish(exchangeName, routingKey, true, null, msg.getBytes());
        }
    }
}

消费端Ack与重回队列

ack的三种答应:

  • basicAck : 返回正确的答应
  • basicNack: 返回否定的答应,且 requeue = true 消息会重回队列,循环重发
  • basicReject: 返回拒绝的答应, 消息会被丢弃

消费端的重回队列:

  • 消费端重回队列是为了对没有处理成功的消息, 把消息重新回递给Broker
  • 一般在实际应用中,都会关闭重回队列,也就是false

消费端:

public class ReQueueConsumer {

    public static void main(String[] args) throws IOException, TimeoutException {

        //创建连接工厂,设置连接信息
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.107.132");
        factory.setPort(5672);
        factory.setVirtualHost("/");

        //创建连接
        Connection connection = factory.newConnection();
        //创建信道
        Channel channel = connection.createChannel();

        String exchangeName = "test.ack.exchange";
        String queueName = "test.ack.queue";
        String routingKey = "ack.#";

        //创建exchange
        channel.exchangeDeclare(exchangeName, "topic", true, false, null);
        //创建一个队列 队列名称, 是否持久化, 此channel独占这个队列, autoDelete, 拓展参数
        channel.queueDeclare(queueName, true, false, false, null);
        //绑定
        channel.queueBind(queueName, exchangeName, routingKey);

        //设置consumer autoAck = false
        channel.basicConsume(queueName, false, new MessageConsumer(channel));

    }
    static class MessageConsumer extends DefaultConsumer {

        //channel
        private Channel channel;

        public MessageConsumer(Channel channel) {
            super(channel);
            this.channel = channel;
        }
        //处理消息的逻辑
        @Override
        public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
            super.handleDelivery(consumerTag, envelope, properties, body);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            if((Integer)properties.getHeaders().get("num") == 0){
                //否定答应 消息唯一标识, 是否批量处理, 是否重回队列
                channel.basicNack(envelope.getDeliveryTag(), false, true);
                System.out.println("重回队列, DeliveryTag: " + envelope.getDeliveryTag());
            }else {
                //ack答应
                channel.basicAck(envelope.getDeliveryTag(), false);
                System.out.println("message: " + new String(body));
                System.out.println("DeliveryTag: " + envelope.getDeliveryTag());
            }
        }
    }
}

num为0的消息被否认ack,接着处理完其他几条消息后, num为0的消息不断的重回队列

生产端:

public class ReQueueProducer {

    public static void main(String[] args) throws IOException, TimeoutException {

        //创建连接工厂,设置连接信息
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.107.132");
        factory.setPort(5672);
        factory.setVirtualHost("/");

        //创建连接
        Connection connection = factory.newConnection();
        //创建信道
        Channel channel = connection.createChannel();

        String exchangeName = "test.ack.exchange";
        String queueName = "test.ack.queue";
        String routingKey = "ack.save";

        String msg = "Hello RabbitMQ ACK Message!";

        //发送5条信息
        for(int i = 0; i < 5; i++){

            HashMap<String, Object> headers = new HashMap<>();
            headers.put("num", i);
            AMQP.BasicProperties properties = new AMQP.BasicProperties().builder()
                    .deliveryMode(2) //持久化
                    .contentEncoding("UTF-8")
                    .expiration("20000")
                    .headers(headers)
                    .build();

            channel.basicPublish(exchangeName, routingKey, true, properties, msg.getBytes());
        }
    }
}

TTL消息/队列

TTL是 Time To Live 的缩写, 也就是生存时间

RabbitMQ支持消息的过期时间, 在发送消息时可以进行指定

new AMQP.BasicProperties().builder()
                    .expiration("20000")

RabbitMQ支持队列级消息的过期时间, 从消息入队开始计算, 只要超过了队列的超时时间配置, 那么消息会自动的清除

image-20200704160309883

在创建时可指定 Arguments 下面的参数:

  • message-ttl : 消息过期时间, 毫秒
  • max-length: 消息最大长度
  • ...

测试TTL队列

  • 新建exchange, 指定路由键为 ttl.abc 绑定到上面创建的 exp.queue 队列
  • 发送一条消息

  • 10秒之后这条消息会被清除

    image-20200704161315752

DLX死信队列

死信队列: Dead-Letter-Exchange

RabbitMQ中的死信队列是和exchange息息相关的

利用DLX, 当消息在一个队列中变成死信(dead message)之后, 它能被重新publish到另一个Exchange, 这个Exchange就是DLX

消息变成死信有以下几种情况

  • 消息被拒绝(basic.reject / basic.nack),并且requeue = false
  • 消息TTL过期
  • 队列达到最大长度

死信处理过程

  • DLX也是一个正常的Exchange,和一般的Exchange没有区别,它能在任何的队列上被指定,实际上就是设置某个队列的属性。
  • 当这个队列中有死信时,RabbitMQ就会自动的将这个消息重新发布到设置的Exchange上去,进而被路由到另一个队列。
  • 可以监听这个队列中的消息做相应的处理。

死信队列设置

  1. 设置死信队列的exchange和queue, 然后进行绑定:

    Exchange: dlx.exchange

    Queue: dlx.queue

    RoutingKey: #

  2. 然后正常声明交换机, 队列, 绑定, 只不过需要在队列上加一个参数设置死信交换机:

    arguments.put("x-dead-letter-exchange", "dlx.exchange");

如此, ==消息在过期, 无法requeue, 或达到队列最大长度时, 消息可以直接路由到死信队列==

消费端:

public class DLXConsumer {

    public static void main(String[] args) throws IOException, TimeoutException {

        //创建连接工厂,设置连接信息
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.107.132");
        factory.setPort(5672);
        factory.setVirtualHost("/");

        //创建连接
        Connection connection = factory.newConnection();
        //创建信道
        Channel channel = connection.createChannel();

        //普通的交换机和队列以及路由
        String exchangeName = "test.order.exchange";
        String queueName = "test.order.queue";
        String routingKey = "order.#";

        //创建exchange
        channel.exchangeDeclare(exchangeName, "topic", true, false, null);

        //-----设置死信队列exchange----
        String DLX_exchange = "dlx.exchange";
        HashMap<String, Object> arguments = new HashMap<>();
        arguments.put("x-dead-letter-exchange", DLX_exchange);
        //创建一个队列 设置arguments
        channel.queueDeclare(queueName, true, false, false, arguments);
        //绑定
        channel.queueBind(queueName, exchangeName, routingKey);

        //----声明死信队列和exchange并绑定----
        String DLX_queue = "dlx.queue";
        channel.exchangeDeclare(DLX_exchange, "topic", true, false, null);
        channel.queueDeclare(DLX_queue, true, false ,false, null);
        channel.queueBind(DLX_queue, DLX_exchange, "#");

        //设置consumer autoAck = false
        channel.basicConsume(queueName, false, new MessageConsumer(channel));
    }
    static class MessageConsumer extends DefaultConsumer {

        //channel
        private Channel channel;

        public MessageConsumer(Channel channel) {
            super(channel);
            this.channel = channel;
        }
        //处理消息的逻辑
        @Override
        public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
            super.handleDelivery(consumerTag, envelope, properties, body);
            //NACK否认答应
            channel.basicNack(envelope.getDeliveryTag(), false, false);
            System.out.println("message: " + new String(body));
            System.out.println("DeliveryTag: " + envelope.getDeliveryTag());
        }
    }
}

生产端:

public class DLXProducer {

    public static void main(String[] args) throws IOException, TimeoutException {

        //创建连接工厂,设置连接信息
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.107.132");
        factory.setPort(5672);
        factory.setVirtualHost("/");

        //创建连接
        Connection connection = factory.newConnection();
        //创建信道
        Channel channel = connection.createChannel();

        String exchangeName = "test.order.exchange";
        String routingKey = "order.save";

        String msg = "Hello RabbitMQ DLX Message!";

        for(int i = 0; i < 5; i++){

            AMQP.BasicProperties properties = new AMQP.BasicProperties().builder()
                    .deliveryMode(2) //持久化
                    .contentEncoding("UTF-8")
                    .expiration("10000")  //过期时间10秒
                    .build();
            channel.basicPublish(exchangeName, routingKey, true, properties, msg.getBytes());
        }
    }
}

Spring Cloud Stream整合

img

屏蔽底层消息中间件的差异,降低切换成本,统一消息的编程模型

Spring Cloud Stream相当于是一个消息的中间代理, 消息的生产与消费, 可以使用不同的消息中间件

Spring Cloud Stream 为 Kafka 和 Rabbit MQ 提供了 Binder 实现

Spring Cloud Stream 中的几个重要概念:

  • Destination Binders:目标绑定器,目标指的是 kafka 还是 RabbitMQ,绑定器就是封装了目标中间件的包。如果操作的是 kafka 就使用 kafka binder ,如果操作的是 RabbitMQ 就使用 rabbitmq binder。
  • Destination Bindings:外部消息传递系统和应用程序之间的桥梁,提供消息的“生产者”和“消费者”(由目标绑定器创建)
  • Message:一种规范化的数据结构,生产者和消费者基于这个数据结构通过外部消息系统与目标绑定器和其他应用程序通信。
组成说明
Middleware中间件,目前只支持RabbitMQ和Kafka
BinderBinder是应用与消息中间件之间的封装,可以很方便的连接,动态的改变消息类型
@Input表示输入通道,通过该输入通道接收到的消息进入应用程序
@Output表示输出通道,发布的消息将通过该通道离开应用程序
@StreamListener监听队列,用于消费者队列的消息接收
@EnableBinding指信道channel和exchange绑定在一起title: RabbitMQ学习笔记 # 标题

date: 2020/3/26 15:50:00 # 时间
categories: # 分类

  • MQ

RabbitMQ

大多应用中,可以通过消息服务中间件来提升系统异步通信,拓展解耦能力

消息服务中两个重要概念:

  • 消息代理(message broker)
  • 目的地(destination)

当消息发送者发送消息以后,将由消息代理接管,消息代理保证消息传递到指定目的地.

消息队列主要有两种形式的目的地,对应两种通信方式:

  • 队列(queue):点对点消息通信(point-to-point)
  • 主题(topic):发布(publish) 订阅(subscribe) 消息通信

<!--more-->

点对点式:

消息发送者发送消息,消息代理将其放入一个队列中,消息接收者从中获取消息内容,消息读取后被移出队列;消息只有唯一的发送者和接受者,但并不是说只能有一个接收者.(多个接收者来拿这个消息,只能有一个拿到)

发布订阅式:

发送者发布消息到主题,多个接收者订阅这个主题,那么就会在消息到达时同时收到消息.

JMS(Java Message Service) JAVA消息服务:

基于JVM消息代理的规范. ActiveMQ.HornetMQ是JMS实现

AMQP(Advanced Message Queuing Protocol):

高级消息队列协议,也是消息代理的规范,兼容JMS,RabbitMQ是AMQP的实现

spring-jms和spring-rabbit提供了以上两种规范的支持

  • 需要ConnectionFactory的实现来连接消息代理
  • 提供JmsTemplate,RabbitTemplate来发送消息
  • @JmsListener,@RabbitListener注解在方法上监听消息代理发布的消息
  • @EnableJms,@EnableRabbit开启支持
  • 自动配置类:JmsAutoConfiguration,RabbitAutoConfiguration

RabbitMQ是由erlang开发的AMQP的开源实现

核心概念

  • Message

    消息,消息是不具名的,它由消息头和消息体组成;消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括routing-key(路由键),priority(相对于其他消息的优先权),delivery-mode(指出该消息可能需要持久化存储)等.

  • Publisher

    消息的生产者,也是一个向交换器发布消息的客户端程序.

  • Exchange

    交换器,用来接收生产者发送的消息,并将这些消息路由给服务器中的队列;Exchange有4种类型,不同类型的Exchange转发消息的策略有所区别.

    • direct(默认):通过指定路由键点对点方式与队列绑定
    • fanout:广播模式,将消息发送到所有队列
    • topic:允许对路由键制定模糊匹配,单词之间用点隔开,#匹配0个或多个单词,*匹配一个单词
    • headers 和默认基本一致,且性能不佳
  • Queue

    消息队列,用来保存消息直到发送给消费者;它是消息的容器,也是消息的终点;一个消息可投入一个或多个队列;消息一直在队列里面,等待消费者连接到这个队列将其取走.

  • Binding

    绑定,用于消息队列和交换器之间的关联;一个绑定器就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表.Exchange和Queue的绑定可以是多对多的关系.

  • Connection

    网络连接,比如一个TCP连接.

  • Channel

    信道,多路复用连接中的一条独立的双向数据流通道;信道是建立在真实的TCP连接内的虚拟连接,AMQP命令都是通过信道发送出去的,不管是发布消息,订阅队列还是接收消息,这些动作由是通过信道完成;因为对于操作系统来说建立和销毁TCP都是非常昂贵的开销,所以引入了信道的概念,以复用一条TCP连接.

  • Consumer

    消息的消费者,表示一个从消息队列中取得消息的客户端应用程序.

  • Virtual Host

    虚拟主机,表示一批交换器,消息队列和相关对象;虚拟主机是共享相同的身份认证和加密环境的独立服务器域;每个vhost本质上就是一个mini版的RabbitMQ服务器,拥有自己的队列,交换器,绑定和权限机制;vhost是AMQP概念的基础,必须在连接时指定,RabbitMQ默认的vhost是/.

  • Broker

    表示消息队列服务器实体

    broker 2020-03-27 131401

安装RabbitMQ

rabbitmq和erlang版本对照:https://www.rabbitmq.com/which-erlang.html

windows

Linux

  • 准备:

    yum install build-essential openssl openssl-devel unixODBC unixODBC-devel make gcc gcc-c++ kernel-devel m4 ncurses-devel tk tc xz

  • 下载&安装:

    wget www.rabbitmq.com/releases/erlang/erlang-18.3-1.el7.centos.x86_64.rpm
    wget http://repo.iotti.biz/CentOS/...
    wget www.rabbitmq.com/releases/rabbitmq-server/v3.6.5/rabbitmq-server-3.6.5-1.noarch.rpm

    先安装erlang

    rpm -ivh erlang-18.3-1.el7.centos.x86_64.rpm

    socat密钥

    rpm -ivh socat-1.7.3.2-1.1.el7.x86_64.rpm

    然后rabbitmq-server

    rpm -ivh socat-1.7.3.2-1.1.el7.x86_64.rpm

  • 配置文件:

    vim /usr/lib/rabbitmq/lib/rabbitmq_server-3.6.5/ebin/rabbit.app
    比如修改密码、配置等等,例如:loopback_users 中的 <<"guest">>,删除尖括号和引号只保留[guest]

    env下可修改端口号等环境变量:

    image-20200702120334702

  • 服务启动和停止:

    启动 rabbitmq-server start &
    停止 rabbitmqctl stop_app

启用管理插件:rabbitmq-plugins enable rabbitmq_management 访问地址:http://192.168.107.132:15672/

如果启动失败可查看端口占用情况: lsof -i:5672

命令行与管控台操作

  • 服务启动: rabbitmqctl start_app
  • 服务停止: rabbitmqctl stop_app
  • 节点状态: rabbitmqctl status
  • 创建虚拟主机: rabbitmqctl add_vhost < vhostpath >
  • 查看所有虚拟主机:rabbitmqctl list_vhosts
  • 查看所有队列: rabbitmqctl list_queues
  • 清除队列里的消息: rabbitmqctl -p vhostpath purge_queue blue
  • 移除所有数据: rabbitmqctl reset (要在服务停止之后使用)
  • 组成集群命令: rabbitmqctl join_cluster < clusternode> [--ram]存储模式
  • 修改集群节点的存储模式: rabbitmqctl change_cluster_node_type disc | ram
  • 摘除节点:rabbitmqctl forget_cluster_node [--offline]

管控台可以管理监控 连接(Connections),信道(Channels),交换器(Exchanges),队列(Queues) ;

Admin中可监控和添加虚拟主机(Virtual Host)

与springboot整合

创建springboot项目选择依赖web,rabbitMQ

<!--rabbitMQ-->
<dependency>

<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>

</dependency>

spring-boot-starter-amqp引入了spring-messaging, spring-rabbit

自动配置类:RabbitAutoConfiguration

配置了rabbitConnectionFactory连接工厂

RabbitPtoperties封装了RabbitMQ的配置

RabbitTemplate用来发送和接受消息

AmqpAdminRabbitMQ系统管理功能组件

相关配置:

rabbit主机地址

spring.rabbitmq.host=192.168.107.108

用户名 密码 端口

spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.port=5672

虚拟主机地址

spring.rabbitmq.virtual-host=/

支持发布确认

spring.rabbitmq.publisher-confirms=false

支持发布返回

spring.rabbitmq.publisher-returns=false

虚拟主机名称

spring.rabbitmq.virtual-host=/

采用手动应答

spring.rabbitmq.listener.acknowledge-mode=manual

spring:
application:

name: mq-api

rabbitmq:

host: 192.168.107.132
port: 5672
username: guest
password: guest
virtual-host: /
publisher-confirms: true #监听消息是否 到达 exchange
publisher-returns: true #监听消息是否 没有到达 queue
template:
  mandatory: true #自动删除不可达消息,默认为false
  listener:
    simple:
      acknowledge-mode: manual #手动ack

在测试类中注入RabbitTemplate,有两种发送消息的方法

//Message需要自己构造,可定制消息体内容和消息头
rabbitTemplate.send(exchange,routeKey,message);

//object默认作为消息体,只需要传入要发送的对象,自动序列化发送给rabbitmq
rabbitTemplate.convertAndSend(exchange,routeKey,object);

使用AmqpAdmin组件可以编码的方式创建和管理Queue,Bindings以及Exchange

/**

 * declareXXX()用来创建组件
 * removeXXX(),deleteXXX()用来删除
 */
@Autowired
private AmqpAdmin amqpAdmin;

/**
 * 创建Exchange
 */
@Test
public void createExchange(){
    //传入对应类型的实现类
    amqpAdmin.declareExchange(new DirectExchange("amqpadmin.exchange"));
    System.out.println("创建完成");
}

/**
 * 创建Queue
 */
@Test
public void createQueue(){
    //名字 是否持久化
    amqpAdmin.declareQueue(new Queue("amqpadmin.queue",true));
}

/**
 * 创建绑定规则
 */
@Test
public void createBindings(){
    //绑定目的地 目的地类型 Exchange Routing key 参数
    amqpAdmin.declareBinding(new Binding("amqpadmin.queue",                                            Binding.DestinationType.QUEUE,
              "amqpadmin.exchange","amqp.haha",null));
}

Exchange接口的实现类:

image-20200514172523355

例子:

 @Autowired
private RabbitTemplate rabbitTemplate;

/**
 * 点对点方式 发送
 */
@Test
public void contextLoads() {
    Map<String,Object> map = new HashMap<>();
    map.put("msg","这是第一个消息");
    map.put("data", Arrays.asList("helloWorld",123,true));
    //对象被默认序列化后发送出去
    rabbitTemplate.convertAndSend("exchange.direct","user.news",map);
}

/**
 * 接收消息
 */
@Test
public void receive(){
    Object o = rabbitTemplate.receiveAndConvert("user.news");
    System.out.println(o.getClass());
    System.out.println(o);
}

/**
 * 广播
 */
@Test
public void sendMsg(){
    rabbitTemplate.convertAndSend("exchange.fanout","",new Book("三国演义","罗贯中"));
}

JSON序列化:

RabbitTemplate中的MessageConverter的实现默认为SimpleMessageConverter也就是jdk序列化的方式

MessageConverter的实现改成Jackson2JsonMessageConverter,数据就会以JSON的格式发送了

注意实现Serializable接口

import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;

@Configuration
public class MyAMQPConfig {

@Bean
public MessageConverter messageConverter(){
    return new Jackson2JsonMessageConverter();
}
//或者
@Bean
public MessageConverter jsonMessageConverter(ObjectMapper objectMapper) {
    return new Jackson2JsonMessageConverter(objectMapper);
}

}

@RabbitListener监听队列自动接收消息

启动类加上@EnableRabbit注解

import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
@Service
public class BookService {

/**
 * 监听指定的队列,多个用逗号分隔
 */
@RabbitListener(queues = "user.news")
public void receive(Book book){
    System.out.println("收到消息: "+book);
}
/**
 * message对象可获得消息头和消息体
 */
@RabbitListener(queues = "user")
public void receive02(Message message){
    System.out.println(message.getBody());
    System.out.println(message.getMessageProperties());
}

}

构建生产者&消费者

原生API

  • 消费者

    import com.rabbitmq.client.*;
    import org.springframework.stereotype.Component;
    import java.io.IOException;
    import java.util.concurrent.TimeoutException;

    @Component
    public class Consumer {

    public static void main(String[] args) throws IOException, TimeoutException {
    
        //创建连接工厂,设置连接信息
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.107.132");
        factory.setPort(5672);
        factory.setVirtualHost("/");
    
        //创建连接
        Connection connection = factory.newConnection();
    
        //创建信道
        Channel channel = connection.createChannel();
        String queueName = "test001";
        //创建一个队列 队列名称, 是否持久化, 此channel独占这个队列, autoDelete, 拓展参数
        channel.queueDeclare(queueName, true, false, false, null);
    
        //创建消费者
        MessageConsumer consumer = new MessageConsumer(channel);
        //设置channel 队列名称, autoAck, consumer
        channel.basicConsume(queueName, true, consumer);
        //在Broker接收到该Consumer的ack前,
        //Consumer在同一个时间点最多被分配qos个Message
        channel.basicQos(3);
    
    }
    //拓展DefaultConsumer
    static class MessageConsumer extends DefaultConsumer {
    
            public MessageConsumer(Channel channel) {
                super(channel);
            }
            //处理消息的逻辑
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                super.handleDelivery(consumerTag, envelope, properties, body);
                System.out.println("message: " + new String(body));
                System.out.println("DeliveryTag: " + envelope.getDeliveryTag());
            }
    }

    }

    启动消费者后查看管控台,发现connection,channel已成功连接,队列已成功创建

  • 生产者

    import com.rabbitmq.client.Channel;
    import com.rabbitmq.client.Connection;
    import com.rabbitmq.client.ConnectionFactory;
    import java.io.IOException;
    import java.util.concurrent.TimeoutException;

    public class Producer {

    public static void main(String[] args) throws IOException, TimeoutException {
    
        //创建连接工厂,设置连接信息
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.107.132");
        factory.setPort(5672);
        factory.setVirtualHost("/");
    
        //创建连接
        Connection connection = factory.newConnection();
    
        //创建信道
        Channel channel = connection.createChannel();
        String msg = "Hello RabbitMQ";
        //使用默认exchange 发布消息
        for (int i =0; i < 5; i++) {
            // exchange, routeKey, properties, message
            channel.basicPublish("", "test001", null, msg.getBytes());
            System.out.println("发送消息: " + msg);
        }
        //关闭资源
        channel.close();
        connection.close();
    }

    }

  • 测试

    • 启动消费者后运行生产者,收到消息
    • 运行生产者后消息未被消费,再启动消费者后收到消息
  • tip: QueueingConsumer已被废弃

    QueueingConsumer内部用LinkedBlockingQueue来存放消息的内容,而LinkedBlockingQueue:一个由链表结构组成的有界队列,照先进先出的顺序进行排序 ,未指定长度的话,默认 此队列的长度为Integer.MAX_VALUE,那么问题来了,如果生产者的速度远远大于消费者的速度,也许没等到队列阻塞的条件产生(长度达到Integer.MAX_VALUE)内存就完蛋了,在老的版本你可以通过设置 rabbitmq的prefetch属性channel.basicQos(prefetch)来处理这个问题如果不设置可能出现内存问题(比如因为网络问题只能向rabbitmq生产不能消费,消费者恢复网络之后就会有大量的数据涌入,出现内存问题,oom fgc等)。

    而且写法很不合理不符合事件驱动,什么时候停止while循环也不能写的很优雅,所以在更高的版本直接被移除。取而代之的是DefaultConsumer,你可以通过扩展DefaultConsumer来实现消费者

spring-boot

  • 消费者

    spring:
    application:

    name: mq-api

    rabbitmq:

    host: 192.168.107.132
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    template:
      mandatory: true #自动删除不可达消息,默认为false
      listener:
        simple:
          acknowledge-mode: manual #手动ack
          concurrency: 5 #消费者的最小数量
          max-concurrency: 10 #消费者的最大数量
    

    消费者核心注解: @RabbitListener, @RabbitHandler

    @RabbitListener是一个组合注解, 与@QueueBinding, @Queue, @Exchange 组合使用, 一次性搞定消费端交换机, 队列, 绑定, 路由, 并且配置监听功能等, 配置信息建议从配置文件动态加载

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(value = "queue_1", durable = "true"),
            exchange = @Exchange(value = "exchange_1",
                    durable = "true",
                    type = "topic",
                    ignoreDeclarationExceptions = "true"),
            key = "springboot.#"
    ))
    @RabbitHandler
    public void onMessage(Message message, Channel channel) throws IOException {
        System.out.println("消费端: " + new String(message.getBody()));
        Long deliveryTag = message.getMessageProperties().getDeliveryTag();
        //手动ack
        channel.basicAck(deliveryTag, false);
    }
    

    @RabbitListener 可以标注在类上面,需配合 @RabbitHandler 注解一起使用

    @RabbitListener 标注在类上面表示当有收到消息的时候,就交给 @RabbitHandler 的方法处理,具体使用哪个方法处理,根据 MessageConverter 转换后的参数类型

    @Component
    @RabbitListener(queues = "consumer_queue")
    public class Receiver {

    @RabbitHandler
    public void processMessage1(String message) {
        System.out.println(message);
    }
    
    @RabbitHandler
    public void processMessage2(byte[] message) {
        System.out.println(new String(message));
    }
    

    }

    @Payload@Headers注解用来指定消息体, 和接受消息头参数

    @RabbitHandler
    public void onOrderMessage(@Payload OrderMessage orderMessage, Channel channel,

                           @Headers Map<String, Object> Headers) throws IOException {
    System.out.println("消费端: " + orderMessage.getContent());
    Long deliveryTag = (Long) Headers.get(AmqpHeaders.DELIVERY_TAG);
    //手动ack
    channel.basicAck(deliveryTag, false);

    }

  • 生产者

    publisher-confirms: true #监听消息是否 到达 exchange
    publisher-returns: true #监听消息是否 没有到达 queue

    import org.springframework.amqp.core.Message;
    import org.springframework.amqp.core.MessageProperties;
    import org.springframework.amqp.rabbit.connection.CorrelationData;
    import org.springframework.amqp.rabbit.core.RabbitTemplate;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;

    import java.time.LocalDateTime;
    import java.util.UUID;
    @Component
    public class RabbitSender {

    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    final RabbitTemplate.ConfirmCallback confirmCallback = new RabbitTemplate.ConfirmCallback() {
        /**
         * 异步监听 消息是否到达 exchange
         *
         * @param correlationData 包含消息的唯一标识的对象
         * @param ack             true 标识 ack,false 标识 nack
         * @param cause           nack 的原因
         */
        @Override
        public void confirm(CorrelationData correlationData, boolean ack, String cause) {
            System.out.println("-----confirmCallback-----");
            System.out.println("correlationData: " + correlationData);
            System.out.println("ack: " + ack);
            System.out.println("cause: " + cause);
            if (ack){
                // todo 操作数据库,将 correlationId 这条消息状态改为投递成功
            }
    
            //log.error("消息投递失败,ID为: {},错误信息: {}", correlationData.getId(), cause);
            // todo 操作数据库,将 correlationId 这条消息状态改为投递失败
        }
    };
    
    final RabbitTemplate.ReturnCallback returnCallback = new RabbitTemplate.ReturnCallback() {
        /**
         * 异步监听 消息是否到达 queue
         * 触发回调要满足的条件有两个:
         * 1.消息已经到达了 exchange 
         * 2.消息无法到达 queue (比如 exchange 找不到跟 routingKey 对应的 queue)
         *
         * @param message    返回的消息
         * @param replyCode  回复 code
         * @param replyText  回复 text
         * @param exchange   交换机
         * @param routingKey 路由键
         */
        @Override
        public void returnedMessage(org.springframework.amqp.core.Message message, int replyCode, String replyText, String exchange, String routingKey) {
            System.out.println("-----returnCallback-----");
            System.out.println("message: " + message);
            System.out.println("replyCode: " + replyCode + ", replyText: " + replyText);
            System.out.println("exchange: " + exchange + ", routingKey: " + routingKey);
            // todo 操作数据库,将 correlationId 这条消息状态改为投递失败
        }
    };
    
    public void send(String exchange, String routingKey, String msg) throws Exception {
        //消息头
        MessageProperties properties = new MessageProperties();
        properties.getHeaders().put("send_time", LocalDateTime.now());
        properties.setContentType("text/plain");
        //构建消息
        Message message = new Message(msg.getBytes(), properties);
        //指定confirmCallback
        rabbitTemplate.setConfirmCallback(confirmCallback);
        //指定returnCallback
        rabbitTemplate.setReturnCallback(returnCallback);
        //指定唯一id
        CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
        //发送消息
        rabbitTemplate.convertAndSend(exchange, routingKey, msg, correlationData);
    }
    
    public void sendOrder(OrderMessage orderMessage) throws Exception {
        //指定confirmCallback
        rabbitTemplate.setConfirmCallback(confirmCallback);
        //指定returnCallback
        rabbitTemplate.setReturnCallback(returnCallback);
        //id + 时间戳
        CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
        //发送消息
        rabbitTemplate.convertAndSend("exchange_2", "springboot.def", orderMessage, correlationData);
    }

    }

Exchange交换机

用来接收生产者发送的消息,并将这些消息路由给服务器中的队列;

Exchange有4种类型,不同类型的Exchange转发消息的策略有所区别:

  • direct(默认): 通过指定路由键点对点方式与队列绑定
  • fanout: 广播模式,将消息发送到所有队列
  • topic: 允许对路由键制定模糊匹配,单词之间用点隔开,#匹配0个或多个单词,*匹配一个单词
  • headers: 和默认基本一致,且性能不佳

交换机属性

  • Name: 交换机名称
  • Durability: 是否持久化
  • AutoDelete: 当最后一个绑定到Exchange上的队列被删除后,自动删除该Exchange
  • Internal: 当前Exchange是否用于RabbitMQ内部使用,默认false
  • Arguments: 拓展参数, 用于拓展AMQP协议自定制化使用

测试三种类型的交换器

  1. 点击管控台的Add a new exchange创建交换机

    Durability:是否持久化选择Durable

    exchange.direct交换器,Type为direct

    exchange.fanout交换器,Type为fanout

    exchange.topic交换器,Type为topic

  2. Add a new queue添加消息队列

    user user.news user.orders edc.news

  3. 给队列绑定交换器

    exchange.direct/Bindings 选择 To Queue,使用Routing key同名字分别绑定,点击Unbind可解绑,fanout与之同理

    topic用通配符绑定

    *.news

    user.#

    user.news

    user

    edc.news

    user.news

    user.orders

  4. 接着分别在三个Exchange发送消息测试

    Exchanges中点击Publish message ,输入Routing key发送消息,如:direct.exchange.msg.helloWorld

    在Queues中点击对应的queue,点击Get Message获取消息,Ack Mode选择Ack message requeue false会清除上一次的消息

结果:

  • direct通过指定路由键点对点方式与队列绑定,所以只有完全匹配Routing key的队列才会收到消息.
  • fanout会将消息广播发送到所有队列
  • topic会有选择的模糊匹配Routing key对应的队列

Message自定义消息属性

  1. 构建BasicProperties

    //自定义headers属性

        HashMap<String, Object> attr = new HashMap<>();
        attr.put("status", 1);
        attr.put("detail", "这是附加属性");
        //构建消息属性
        AMQP.BasicProperties properties = new AMQP.BasicProperties().builder()
                .deliveryMode(2) //1:不持久化消息, 2:持久化消息
                .contentEncoding("UTF-8") //字符集
                .expiration("10000") //过期时间
                .headers(attr) //自定义属性
                .build();
    
  2. 发布时指定properties

    String msg = "Hello RabbitMQ";
    //使用默认exchange 发布消息
    for (int i =0; i < 5; i++) {

    // exchange, routeKey, properties, message
    channel.basicPublish("", "test001", properties, msg.getBytes());
    System.out.println("发送消息: " + msg);

    }

    channel.close();
    connection.close();

  3. 获取properties

    //处理消息的逻辑
    @Override
    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {

    super.handleDelivery(consumerTag, envelope, properties, body);
    System.out.println("message: " + new String(body));
    System.out.println("DeliveryTag: " + envelope.getDeliveryTag());
    System.out.println(properties.getHeaders().get("detail"));

    }

AMQP事务

我们知道可以通过持久化(交换机、队列和消息持久化)来保障我们在服务器崩溃时,重启服务器消息数据不会丢失。

但是我们无法确认当消息的发布者在将消息发送出去之后,消息到底有没有正确到达Broker代理服务器呢?如果不进行特殊配置的话,默认情况下发布操作是不会返回任何信息给生产者的,也就是默认情况下我们的生产者是不知道消息有没有正确到达Broker的。如果在消息到达Broker之前已经丢失的话,持久化操作也解决不了这个问题,因为消息根本就没到达代理服务器,这个是没有办法进行持久化的,那么当我们遇到这个问题又该如何去解决呢?

RabbitMQ中的消息确认机制,通过消息确认机制我们可以确保我们的消息可靠送达到我们的用户手中,即使消息丢失掉,我们也可以通过进行重复分发确保用户可靠收到消息。

RabbitMQ提供了两种消息确认方式:

  • 通过AMQP事务机制实现,这也是AMQP协议层面提供的解决方案;
  • 通过将channel设置成confirm模式来实现;

RabbitMQ中与事务有关的主要有三个方法:

  • txSelect() : 主要用于将当前channel设置成transaction模式
  • txCommit() : 用于提交事务
  • txRollback() : txRollback用于回滚事务

当我们使用txSelect提交开始事务之后,我们就可以发布消息给Broke代理服务器,如果txCommit提交成功了,则消息一定到达了Broke了,如果在txCommit执行之前Broker出现异常崩溃或者由于其他原因抛出异常,这个时候我们便可以捕获异常通过txRollback方法进行回滚事务了。

所以RabbitMQ事务中的主要代码为:

try {

// 开启事务
channel.txSelect();
// 往队列中发出一条消息,使用rabbitmq默认交换机
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
// 提交事务
channel.txCommit();

} catch (Exception e) {

e.printStackTrace();
// 事务回滚
channel.txRollback();

}finally{

// 关闭频道和连接
channel.close();
connection.close()

}

先进行事务提交,然后开始发送消息,最后提交事务。

在使用事务时,在application.properties中,需要将确认模式更改为false。

支持发布确认

spring.rabbitmq.publisher-confirms=false

消息可靠性投递

  • 保障消息成功发出
  • 保障MQ节点的成功接收
  • 发送端收到MQ节点Broker确认答应
  • 完善的消息补偿机制

解决方案一

image-20200703124116225

消息落库, 对消息状态进行打标

  1. 生产者首先将自己的业务数据落库(如订单),然后将生成的消息落库,发送状态为待发送(status:0), 如果持久化失败,将进行快速失败
  2. 发送消息到MQ Broker
  3. 异步监听Broker收到消息后返回的确认答应
  4. 更新数据库信息状态为已发送(status:1)
  5. 如果超过最大发送时限仍然没有收到Borker的答应,使用分布式定时任务获取所有状态为待发送的信息记录

    • 分布式任务重复获取的问题,需要保证同一时间只有一个定时任务在获取消息记录
    • 可能消息刚刚入库就被获取,造成不必要的重发,需要设置消息超时的最大容忍限制
  6. 尝试重新发送
  7. 超过最大重试次数仍然没有答应,消息状态更新为坏消息(status:2),需要人工干预

缺点: 数据库操作频繁,高并发场景不适合

解决方案二

消息的延迟投递, 做二次确认, 回调检查

  • Upstream: 上游服务,消息生产者
  • Downstream: 下游服务,消息消费者
  • MQ Broker: MQ集群
  • Callback: 回调服务

image-20200703131009257

  1. 上游服务将自己的业务数据持久化之后,进行第一次消息投递
  2. 延迟消息投递检查, 在第一次消息发送后几分钟进行第二次投递,对应check队列,被Callback服务监听
  3. 下游服务监听消息并处理消息
  4. 如果消息被成功处理,发送确认信息到confirm队列,被Callback服务监听
  5. Callback监听到confirm队列中消息被正确处理的答应,将消息记录入库
  6. 如果下游服务没有返回答应消息,MSG DB中就没有该消息的记录,此时check队列中延迟消息到达,检查DB中是否存在此条消息记录,也就是验证该消息是否在之前被正确处理,如果没有Callback服务将发送RPC通知上游服务,将该消息对应的业务数据进行重新一轮发送

优点: 减少了数据库操作,补偿服务和主业务解耦

消费端-冥等性保障

冥等性

任意多次执行操作对资源本身所产生的影响均与一次执行的影响相同

解决的问题:

在高并发情况下,难免会发送消息的重复投递

在海量订单产生的业务高峰期,如何避免消息的重复消费问题?

消费端实现冥等性,就意味着,我们的消息永远不会被消费多次,即使我们收到了多条一样的消息

解决方案一

唯一ID + 指纹码机制, 利用数据库主键去重

  • 生成全局唯一ID + 指纹码(可能是某种业务规则,或第三方提供的唯一标识),利用数据库主键唯一性去重
  • SELETE COUNT(1) FROM T_ORDER WHERE ID = 唯一ID + 指纹码,查到数据库没有此条记录,再进行Insert并消费,否则说明此条记录已经被操作了,丢弃

优点: 实现简单

缺点: 高并发下存在数据库写入的性能瓶颈

解决方案: 根据ID进行分库分表进行路由算法分摊压力

解决方案二

利用Redis的原子性实现冥等

需要考虑的问题:

  • 我们是否要进行数据落库, 如果落库的话, 关键解决的问题是数据库和缓存如何做到原子性?
  • 如果不进行落库,那么都存储到缓存中, 如何设置定时同步的策略?

confirm消息确认机制

消息的确认,是指生产者投递消息后, 如果Broker收到消息,则会给生产者一个答应

生产者进行接收答应,用来确定这条消息是否正常的发送到Broker,这种方式也是消息可靠性投递的核心保障

Producer-Confirm

默认情况下rabbitmq的消费者采用平均分配的方式消费队列中的消息,而且默认开启autoAck自动答应机制,消费者在接受到消息后就会向rabbitmq发送消息已被消费的信号,此时消息将被删除,如果消费者消费到一半的时候宕机,便导致了业务无法正常完成;

为了保证消息不会丢失,如果一个消费者宕机,我们希望将未被处理的消息交给另一个消费者

实现原理:

生产者将信道设置成confirm模式,一旦信道进入confirm模式,所有在该信道上面发布的消息都将会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,broker就会发送一个确认给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker回传给生产者的确认消息中delivery-tag域包含了确认消息的序列号,此外broker也可以设置basic.ack的multiple域,表示到这个序列号之前的所有消息都已经得到了处理;

confirm模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果RabbitMQ因为自身内部错误导致消息丢失,就会发送一条nack消息,生产者应用程序同样可以在回调方法中处理该nack消息;

开启confirm模式的方法:

  • 在channel上开启确认模式: channel.confirmSelect()
  • 在channel上添加监听: addConfirmListener, 监听成功和失败的返回结果,根据具体的结果对消息进行重新发送,或记录日志等后续处理

生产者通过调用channel的confirmSelect方法将channel设置为confirm模式,(注意一点,已经在transaction事务模式的channel是不能再设置成confirm模式的,即这两种模式是不能共存的),如果没有设置no-wait标志的话,broker会返回confirm.select-ok表示同意发送者将当前channel信道设置为confirm模式(从目前RabbitMQ最新版本3.6来看,如果调用了channel.confirmSelect方法,默认情况下是直接将no-wait设置成false的,也就是默认情况下broker是必须回传confirm.select-ok的,而且我也没找到我们自己能够设置no-wait标志的方法);

注意:发布确认和事务。(两者不可同时使用)在channel为事务时,不可引入确认模式;同样channel为确认模式下,不可使用事务

原生API

//消费端
public class ConfirmConsumer {

public static void main(String[] args) throws IOException, TimeoutException {

    //创建连接工厂,设置连接信息
    ConnectionFactory factory = new ConnectionFactory();
    factory.setHost("192.168.107.132");
    factory.setPort(5672);
    factory.setVirtualHost("/");

    //创建连接
    Connection connection = factory.newConnection();

    //创建信道
    Channel channel = connection.createChannel();

    //指定消息确认模式
    channel.confirmSelect();

    String exchangeName = "test.confirm.exchange";
    String routingKey = "confirm.#";
    String queueName = "test.confirm.queue";

    //创建exchange和队列
    channel.exchangeDeclare(exchangeName, "topic", true);
    channel.queueDeclare(queueName, true, false, false, null);
    //绑定exchange和队列
    channel.queueBind(queueName, exchangeName, routingKey);

    //创建消费者
    MessageConsumer consumer = new MessageConsumer(channel);
    //设置channel 队列名称, autoAck, consumer
    channel.basicConsume(queueName, true, consumer);

}
static class MessageConsumer extends DefaultConsumer {

    public MessageConsumer(Channel channel) {
        super(channel);
    }
    //消息处理逻辑
    @Override
    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
        super.handleDelivery(consumerTag, envelope, properties, body);
        System.out.println("消费端: " + new String(body));
    }
}

}

public class ConfirmProducer {

public static void main(String[] args) throws IOException, TimeoutException {

    //创建连接工厂,设置连接信息
    ConnectionFactory factory = new ConnectionFactory();
    factory.setHost("192.168.107.132");
    factory.setPort(5672);
    factory.setVirtualHost("/");

    //创建连接
    Connection connection = factory.newConnection();

    //创建信道
    Channel channel = connection.createChannel();

    //指定消息确认模式
    channel.confirmSelect();

    String exchangeName = "test.confirm.exchange";
    String routingKey = "confirm.save";

    //发送消息
    String msg = "Hello RabbitMQ Send confirm message!";
    channel.basicPublish(exchangeName, routingKey, null, msg.getBytes());

    //----添加确认监听----
    channel.addConfirmListener(new ConfirmListener() {
        /**
         * 成功答应
         * @param deliveryTag 消息唯一标识
         * @param multiple 是否批量
         * @throws IOException
         */
        @Override
        public void handleAck(long deliveryTag, boolean multiple) throws IOException {
            System.out.println("----------------ack!--------------");
        }
        //否认答应
        @Override
        public void handleNack(long deliveryTag, boolean multiple) throws IOException {
            System.err.println("----------------no ack--------------");
        }
    });
}

}

spring-boot

配置:

rabbitmq:

host: 127.0.0.1
port: 5672
username: guest
password: guest
virtual-host: /
publisher-confirms: true #监听消息是否 到达 exchange
publisher-returns: true #监听消息是否 没有到达 queue
template:
  mandatory: true #自动删除不可达消息,默认为false
listener:
  simple:
    acknowledge-mode: manual

消息实体:

import java.io.Serializable;

/**

  • 订单的消息实体
    */

public class OrderMessage implements Serializable {

/**
 * 业务id,在业务系统中的唯一。比如 订单id、支付id、商品id ,消息消费端可以通过该 id 避免消息重复消费
 */
private String id;

// 其他业务字段
private String name;

public String getId() {
    return id;
}

public void setId(String id) {
    this.id = id;
}

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

}

发送消息并异步监听:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.UUID;

/**

  • 发送消息并异步监听 ack
    */

@Component
public class OrderMessageSendAsync implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {

private Logger logger = LoggerFactory.getLogger(OrderMessageSendAsync.class);

private RabbitTemplate rabbitTemplate;

/**
 * 通过构造函数注入 RabbitTemplate 依赖
 *
 * @param rabbitTemplate
 */
@Autowired
public OrderMessageSendAsync(RabbitTemplate rabbitTemplate) {
    this.rabbitTemplate = rabbitTemplate;
    // 设置消息到达 exchange 时,要回调的方法,每个 RabbitTemplate 只支持一个 ConfirmCallback
    rabbitTemplate.setConfirmCallback(this);
    // 设置消息无法到达 queue 时,要回调的方法
    rabbitTemplate.setReturnCallback(this);
}

/**
 * 发送消息
 *
 * @param exchange   交换机
 * @param routingKey 路由建
 * @param message    消息实体
 */
public void sendMsg(String exchange, String routingKey, Object message) {
    // 构造包含消息的唯一id的对象,id 必须在该 channel 中始终唯一
    CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
    logger.info("ID为: {}", correlationData.getId());
    // todo 先将业务数据入库,在将 message 的数据库ID 、message的消息id message的初始状态(发送中)等信息入库

    // 完成 数据落库,消息状态打标后,就可以安心发送 message
    rabbitTemplate.convertAndSend(exchange, routingKey, message, correlationData);

    try {
        logger.info("发送消息的线程处于休眠状态, confirm 和 returnedMessage 方法依然处于异步监听状态");
        Thread.sleep(1000*15);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

}


/**
 * 异步监听 消息是否到达 exchange
 *
 * @param correlationData 包含消息的唯一标识的对象
 * @param ack             true 标识 ack,false 标识 nack
 * @param cause           nack 的原因
 */
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {

    if (ack) {
        logger.info("消息投递成功,ID为: {}", correlationData.getId());
        // todo 操作数据库,将 correlationId 这条消息状态改为投递成功
        return;
    }

    logger.error("消息投递失败,ID为: {},错误信息: {}", correlationData.getId(), cause);
    // todo 操作数据库,将 correlationId 这条消息状态改为投递失败

}

/**
 * 异步监听 消息是否到达 queue
 * 触发回调要满足的条件有两个:1.消息已经到达了 exchange 2.消息无法到达 queue (比如 exchange 找不到跟 routingKey 对应的 queue)
 *
 * @param message    返回的消息
 * @param replyCode  回复 code
 * @param replyText  回复 text
 * @param exchange   交换机
 * @param routingKey 路由键
 */
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
    // correlationId 就是发消息时设置的 id
    String correlationId = message.getMessageProperties().getHeaders().get("spring_returned_message_correlation").toString();

    logger.error("没有找到对应队列,消息投递失败,ID为: {}, replyCode {} , replyText {}, exchange {} routingKey {}",
            correlationId, replyCode, replyText, exchange, routingKey);
    // todo 操作数据库,将 correlationId 这条消息状态改为投递失败
}

}

测试:

import com.alibaba.fastjson.JSONObject;
import com.wqlm.rabbitmq.send.MessageSend.OrderMessageSendAsync;
import com.wqlm.rabbitmq.send.message.OrderMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/")
public class SendMessageController {

@Autowired
private OrderMessageSendAsync orderMessageSendAsync;

/**
 * 测试发送消息并异步接收响应
 */
@GetMapping("/test")
public void test(){
    OrderMessage orderMessage = new OrderMessage("123", "订单123");
    // 序列化成json ,OrderMessage 也可以 implements Serializable 这样就不需要序列化成json
    String message = JSONObject.toJSONString(orderMessage);
    orderMessageSendAsync.sendMsg("exchangeName", "routingKeyValue", message);
}

}

springAMQP

SimpleMessageListenerContainer

简单消息监听容器: 我们使用SimpleMessageListenerContainer容器设置消费队列监听,然后设置具体的监听Listener进行消息消费具体逻辑的编写

  • SimpleMessageListenerContainer可监听多个队列
  • 设置事务特性, 事务管理器, 事务属性, 事务容量, 回滚消息等
  • 设置消费者数量, 最小最大数量, 批量消费
  • 设置消息签收模式 NONE, AUTO, MANUAL
  • 是否重回队列, 异常捕获handler函数
  • 设置消费则标签生成策略, 是否独占模式, 消费者属性等
  • 设置具体监听器, 转换器

注意: SimpleMessageListenerContainer可以在运行中动态的改变其消费者数量的大小, 接收消息的模式等; 很多基于RabbitMQ的自定制后台管理在进行动态设置的时候, 也是根据SpringAMQP这一特性取实现的

@Bean
public SimpleMessageListenerContainer messageListenerContainer(ConnectionFactory connectionFactory){

SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.setQueueNames("test.ack", "test.order.queue"); //监听的队列, 多个用逗号分隔
//container.setQueues(queue001(), queue002()); //监听的队列, 注入方式
container.setConcurrentConsumers(1); //当前的消费者数量
container.setMaxConcurrentConsumers(5); //最大消费者数量
container.setDefaultRequeueRejected(false); //默认不重回队列
container.setAcknowledgeMode(AcknowledgeMode.MANUAL); //手动ack模式
//container.setAfterReceivePostProcessors(MessagePostProcessor); 在接收到消息之前做什么

//消费端标签生成策略 (可lambda)
container.setConsumerTagStrategy(new ConsumerTagStrategy() {
    @Override
    public String createConsumerTag(String queue) {
        return queue + "_" + UUID.randomUUID().toString();
    }
});

//消息监听处理
container.setMessageListener((ChannelAwareMessageListener) (message, channel) ->{

    System.out.println("===监听到消息===");
    System.out.println(new String(message.getBody()));

    if (message.getMessageProperties().getHeaders().get("err") == null){
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        System.out.println("===消息已确认===");
    }else {
        channel.basicReject(message.getMessageProperties().getDeliveryTag(),false);
        System.out.println("===拒绝消息===");
    }
});

return container;

}

MessageListenerAdapter

消息监听适配器: 允许你自定义MessageListener, 通过MessageListenerAdapter来适配

@Bean
public SimpleMessageListenerContainer messageListenerContainer(ConnectionFactory connectionFactory){

SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.setQueueNames("test.ack", "test.order.queue"); //监听的队列, 多个用逗号分隔
//container.setQueues(queue001(), queue002()); //监听的队列, 注入方式
container.setConcurrentConsumers(1); //当前的消费者数量
container.setMaxConcurrentConsumers(5); //最大消费者数量
container.setDefaultRequeueRejected(false); //默认不重回队列
container.setAcknowledgeMode(AcknowledgeMode.MANUAL); //手动ack模式
//container.setAfterReceivePostProcessors(MessagePostProcessor); 在接收到消息之前做什么

//消费端标签生成策略 (可lambda)
container.setConsumerTagStrategy(new ConsumerTagStrategy() {
    @Override
    public String createConsumerTag(String queue) {
        return queue + "_" + UUID.randomUUID().toString();
    }
});

//适配自定义消息监听处理
MessageListenerAdapter adapter = new MessageListenerAdapter(new MessageDelegate());
adapter.setDefaultListenerMethod("consumeMessage"); //改变默认方法
adapter.setMessageConverter(new TextMessageConverter()); //消息转换器
//设置消息监听器
container.setMessageListener(adapter);

return container;

}

/**

  • 自定义消息监听器
    */

@Component
public class MessageDelegate {

public void handleMessage(String messageBody){
    System.out.println("默认方法, 消息内容: " + messageBody);
}

public void consumeMessage(byte[] messageBody){
    System.out.println("字节数组方法, 消息内容: " + messageBody);
}

}

自定义消息监听器的处理方法名称, 定义在MessageListenerAdapter中:

public class MessageListenerAdapter extends AbstractAdaptableMessageListener {

private final Map<String, String> queueOrTagToMethodName;
public static final String ORIGINAL_DEFAULT_LISTENER_METHOD = "handleMessage";
private Object delegate;
private String defaultListenerMethod;

public MessageListenerAdapter() {
    this.queueOrTagToMethodName = new HashMap();
    this.defaultListenerMethod = "handleMessage";
    this.delegate = this;
}
...

你可以通过adapter.setDefaultListenerMethod("consumeMessage");修改负责消息监听处理的方法, 以适配不同的消息处理方式


consumeMessage的入参是byte[]类型, 假设我们发送的消息是String类型就会抛出异常, 可使用自定义MessageConvert来转换类型

/**

  • 文本消息转换器
    */

@Component
public class TextMessageConverter implements MessageConverter {

// Object 转 Message
@Override
public Message toMessage(Object object, MessageProperties messageProperties) throws MessageConversionException {
    return new Message(object.toString().getBytes(), messageProperties);
}

// Message 转 Object
@Override
public Object fromMessage(Message message) throws MessageConversionException {
    String contentType = message.getMessageProperties().getContentType();

    if (null != contentType && contentType.contains("text")){
        return new String(message.getBody());
    }

    return message.getBody();
}

}

使用adapter.setMessageConverter(new TextMessageConverter());指定消息转换器, 并在发送消息时指定ContentType;

//消息头
MessageProperties properties = new MessageProperties();
properties.getHeaders().put("send_time", LocalDateTime.now());
properties.setContentType("text/plain");

可以设置多个队列通过队列名或Tag与不同方法的映射绑定

_queueOrTagToMethodName_: 队列标识与方法名称组成的集合

即指定队列里的消息会被所绑定的方法所接收处理

MessageListenerAdapter adapter = new MessageListenerAdapter(new MessageDelegate());
HashMap<String, String> queueOrTagToMethodName = new HashMap<>();
queueOrTagToMethodName.put("order.queue", "handleOrder");
queueOrTagToMethodName.put("product", "handleProduct");
//设置队列与监听处理方法的映射绑定
adapter.setQueueOrTagToMethodName(queueOrTagToMethodName);
container.setMessageListener(adapter);

消息监听器处理Ack

  • 消息通过 ACK 确认是否被正确接收,每个 Message 都要被确认(acknowledged),可以手动去 ACK 或自动 ACK
  • 自动确认会在消息发送给消费者后立即确认,但存在丢失消息的可能,如果消费端消费逻辑抛出异常,也就是消费端没有处理成功这条消息,那么就相当于丢失了消息
  • 如果消息已经被处理,但后续代码抛出异常,使用 Spring 进行管理的话消费端业务逻辑会进行回滚,这也同样造成了实际意义的消息丢失
  • 如果手动确认则当消费者调用 ack、nack、reject 几种方法进行确认,手动确认可以在业务失败后进行一些操作,如果消息未被 ACK 则会发送到下一个消费者
  • 如果某个服务没有 ACK ,则 RabbitMQ 不会再发送数据给它,因为 RabbitMQ 认为该服务的处理能力有限
  • ACK 机制还可以起到限流作用,比如在接收到某条消息时休眠几秒钟
  • 消息确认模式有:

    • AcknowledgeMode.NONE:自动确认
    • AcknowledgeMode.AUTO:根据情况确认
    • AcknowledgeMode.MANUAL:手动确认

确认消息(局部方法处理消息)

  • 默认情况下消息消费者是自动 ack (确认)消息的,如果要手动 ack(确认)则需要修改确认模式为 manual

    spring:
    rabbitmq:

    listener:
      simple:
        acknowledge-mode: manual
    
  • 或在 RabbitListenerContainerFactory 中进行开启手动 ack

    @Bean
    public RabbitListenerContainerFactory<?> rabbitListenerContainerFactory(ConnectionFactory connectionFactory){

    SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
    factory.setConnectionFactory(connectionFactory);
    factory.setMessageConverter(new Jackson2JsonMessageConverter());
    factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);             //开启手动 ack
    return factory;

    }

  • 确认消息 @RabbitHandler

    @RabbitHandler
    public void processMessage2(String message,Channel channel,@Header(AmqpHeaders.DELIVERY_TAG) long tag) {

    System.out.println(message);
    try {
        channel.basicAck(tag,false); // 确认消息
    } catch (IOException e) {
        e.printStackTrace();
    }

    }

  • 需要注意的 basicAck 方法需要传递两个参数

    • deliveryTag(唯一标识 ID):当一个消费者向 RabbitMQ 注册后,会建立起一个 Channel ,RabbitMQ 会用 basic.deliver 方法向消费者推送消息,这个方法携带了一个 delivery tag, 它代表了 RabbitMQ 向该 Channel 投递的这条消息的唯一标识 ID,是一个单调递增的正整数,delivery tag 的范围仅限于 Channel
    • multiple:为了减少网络流量,手动确认可以被批处理,当该参数为 true 时,则可以一次性确认 delivery_tag 小于等于传入值的所有消息

手动否认 拒绝消息

  • 发送一条错误消息

    public void sendError(String exchange, String routeKey, String body){

        MessageProperties properties = new MessageProperties();
        properties.getHeaders().put("error", "这是一条错误的消息");
        properties.setMessageId(UUID.randomUUID().toString());
        Message message = new Message(body.getBytes(), properties);
        rabbitTemplate.send(exchange, routeKey, message);
        System.out.println("发送了错误信息");
    }
    
  • 消费者获取消息时检查到头部包含 error 则 nack 消息

    @RabbitListener(queues = "test.ack")
    @RabbitHandler
    public void processMessage(String message, Channel channel,@Headers Map<String,Object> map) {

    System.out.println(message);
    if (map.get("error")!= null){
        System.out.println("错误的消息");
        try {
            channel.basicNack((Long)map.get(AmqpHeaders.DELIVERY_TAG),false,true);      //否认消息
            return;
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    try {
        channel.basicAck((Long)map.get(AmqpHeaders.DELIVERY_TAG),false);            //确认消息
    } catch (IOException e) {
        e.printStackTrace();
    }

    }

  • 此时控制台重复打印,说明该消息被 nack 后一直重新入队列然后一直重新消费

    hello
    错误的消息
    hello
    错误的消息
    hello
    错误的消息
    hello
    错误的消息

  • 也可以拒绝该消息,消息会被丢弃,不会重回队列

    channel.basicReject((Long)map.get(AmqpHeaders.DELIVERY_TAG),false); //拒绝消息

确认消息(全局处理消息)

  • 自动确认涉及到一个问题就是如果在处理消息的时候抛出异常,消息处理失败,但是因为自动确认而导致 Rabbit 将该消息删除了,造成消息丢失

    //NONE
    @Bean
    public SimpleMessageListenerContainer messageListenerContainer(ConnectionFactory connectionFactory){

    SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
    container.setConnectionFactory(connectionFactory);
    container.setQueueNames("consumer_queue");                 // 监听的队列
    container.setAcknowledgeMode(AcknowledgeMode.NONE);     // NONE 代表自动确认
    container.setMessageListener((MessageListener) message -> {         //消息监听处理
        System.out.println("====接收到消息=====");
        System.out.println(new String(message.getBody()));
        //相当于自己的一些消费逻辑抛错误
        throw new NullPointerException("consumer fail");
    });
    return container;

    }

  • 手动确认消息

    //MANUAL
    @Bean
    public SimpleMessageListenerContainer messageListenerContainer(ConnectionFactory connectionFactory){

    SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
    container.setConnectionFactory(connectionFactory);
    container.setQueueNames("consumer_queue"); // 监听的队列, 多个用逗号分隔
    //container.setQueues(queue001(), queue002()); // 监听的队列, 注入方式
    container.setAcknowledgeMode(AcknowledgeMode.MANUAL); // 手动确认
    
    container.setMessageListener((ChannelAwareMessageListener) (message, channel) -> {      //消息处理
        System.out.println("====监听到消息=====");
        System.out.println(new String(message.getBody()));
        if(message.getMessageProperties().getHeaders().get("error") == null){
        channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
            System.out.println("消息已经确认");
        }else {
            //channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,false);
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),false);
            System.out.println("消息拒绝");
        }
    
    });
    return container;

    }

  • AcknowledgeMode 除了 NONE 和 MANUAL 之外还有 AUTO ,它会根据方法的执行情况来决定是否确认还是拒绝(是否重新入queue)

    • 如果消息成功被消费(成功的意思是在消费的过程中没有抛出异常),则自动确认
    • 当抛出 AmqpRejectAndDontRequeueException 异常的时候,则消息会被拒绝,且 requeue = false(不重新入队列)
    • 当抛出 ImmediateAcknowledgeAmqpException 异常,则消费者会被确认
    • 其他的异常,则消息会被拒绝,且 requeue = true(如果此时只有一个消费者监听该队列,则有发生死循环的风险,多消费端也会造成资源的极大浪费,这个在开发过程中一定要避免的)。可以通过 setDefaultRequeueRejected(默认是true)去设置

//AUTO
@Bean
public SimpleMessageListenerContainer messageListenerContainer(ConnectionFactory connectionFactory){

SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.setQueueNames("consumer_queue");              // 监听的队列
container.setAcknowledgeMode(AcknowledgeMode.AUTO);     // 根据情况确认消息
container.setMessageListener((MessageListener) (message) -> {
    System.out.println("====接收到消息=====");
    System.out.println(new String(message.getBody()));
    //抛出NullPointerException异常则重新入队列
    //throw new NullPointerException("消息消费失败");
    //当抛出的异常是AmqpRejectAndDontRequeueException异常的时候,则消息会被拒绝,且requeue=false
    //throw new AmqpRejectAndDontRequeueException("消息消费失败");
    //当抛出ImmediateAcknowledgeAmqpException异常,则消费者会被确认
    throw new ImmediateAcknowledgeAmqpException("消息消费失败");
});
return container;

}

消息可靠总结

  • 持久化

    • exchange要持久化
    • queue要持久化
    • message要持久化
  • 消息确认

    • 启动消费返回(@ReturnList注解,生产者就可以知道哪些消息没有发出去)
    • 生产者和Server(broker)之间的消息确认
    • 消费者和Server(broker)之间的消息确认

Return消息机制

Return Listener 用于处理一些不可路由的消息

我们的消息生产者,通过指定一个Exchange和Routingkey,把消息送达到某一个队列中取,然后我们的消费者监听队列,进行消费处理操作

但是在某些情况下,如果我们在发送消息的时候,当前的exchange不存在或者指定的路由key路由不到,这个时候需要监听这种不可达的消息,就要使用Return Listener

关键配置项:

  • _Mandatory_: 如果为true, 则监听器会接收到路由不可达的消息,然后进行后续处理; 如果为false, 那么broker端自动删除改消息

消费端:

public class ReturnConsumer {

public static void main(String[] args) throws IOException, TimeoutException {

    //创建连接工厂,设置连接信息
    ConnectionFactory factory = new ConnectionFactory();
    factory.setHost("192.168.107.132");
    factory.setPort(5672);
    factory.setVirtualHost("/");

    //创建连接
    Connection connection = factory.newConnection();
    //创建信道
    Channel channel = connection.createChannel();

    String exchangeName = "test.return.exchange";
    String routingKey = "return.#";
    String queueName = "test.return.queue";

    //创建exchange
    channel.exchangeDeclare(exchangeName, "topic", true, false, null);
    //创建一个队列 队列名称, 是否持久化, 此channel独占这个队列, autoDelete, 拓展参数
    channel.queueDeclare(queueName, true, false, false, null);
    //绑定
    channel.queueBind(queueName, exchangeName, routingKey);

    //创建消费者
    MessageConsumer consumer = new MessageConsumer(channel);
    //指定channel的消费者 autoAck
    channel.basicConsume(queueName, true, consumer);

}
static class MessageConsumer extends DefaultConsumer {

    public MessageConsumer(Channel channel) {
        super(channel);
    }
    //处理消息的逻辑
    @Override
    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
        super.handleDelivery(consumerTag, envelope, properties, body);
        System.out.println("message: " + new String(body));
        System.out.println("DeliveryTag: " + envelope.getDeliveryTag());
    }
}

}

生产端:

public class ReturnProducer {

public static void main(String[] args) throws IOException, TimeoutException {

    //创建连接工厂,设置连接信息
    ConnectionFactory factory = new ConnectionFactory();
    factory.setHost("192.168.107.132");
    factory.setPort(5672);
    factory.setVirtualHost("/");

    //创建连接
    Connection connection = factory.newConnection();
    //创建信道
    Channel channel = connection.createChannel();

    String exchangeName = "test.return.exchange";
    String routingKey = "return.save";
    String routingKeyError = "abc.save";
    String msg = "Hello RabbitMQ Return Listener!";

    //---添加return listener---
    channel.addReturnListener(new ReturnListener() {
        /**
         * 监听不可达消息
         * @param replyCode 响应码
         * @param replyText 响应文本
         * @param exchange 交换器
         * @param routingKey 路由键
         * @param basicProperties 属性
         * @param body 消息
         * @throws IOException
         */
        @Override
        public void handleReturn(int replyCode, String replyText, String exchange,
                                 String routingKey, AMQP.BasicProperties basicProperties, byte[] body) throws IOException {
            System.out.println("------------------handle return---------------");
            System.out.println("replyCode: " + replyCode);
            System.out.println("replyText: " + replyText);
            System.out.println("exchange: " + exchange);
            System.out.println("routingKey: " + routingKey);
            System.out.println("properties: " + basicProperties.getMessageId());
            System.out.println("body: " + new String(body));
        }
    });

    /* Mandatory = true 才能使监听起效! */
    //发送消息 exchange, routingKey, Mandatory, properties, body
    channel.basicPublish(exchangeName, routingKey, true, null, msg.getBytes());
    //发送消息到不存在的routingKey
    channel.basicPublish(exchangeName, routingKeyError, true, null, msg.getBytes());
}

}

发送消息到不存在的routingKey,将会触发handleReturn回调,打印信息如下:

------------------handle return---------------
replyCode: 312
replyText: NO_ROUTE
exchange: test.return.exchange
routingKey: abc.save
properties: null
body: Hello RabbitMQ Return Listener!

Mandatory = true 才能使监听起效!

消费端限流

假设一个场景, 首先我们RabbitM服务器有上万条未处理的消息,此时开启一个消费端,会出现下面的情况:

巨量的消息瞬间被推送过来,单个消费端无法同时处理这么多的数据,造成服务的崩溃

RabbitMQ提供了一种qos(服务治理保证)功能, 即在非自动确认消息的前提下,如果一定数目的消息(通过基于consume或者channel设置Qos的值) 未被确认前,不进行消费新的消息

实际开发中 autoAck 一定是false

void BasicQos(int prefetchSize, short prefetchConut, bool global)

  • prefetchSize: 消息大小的限制, 一般设置为0不限制
  • prefetchConut: 在Broker接收到该Consumer的ack之前,Consumer在同一个时间点最多被分配最多处理消息数量
  • global: true应用在监听此channel的所有consume, false仅应用在当前consume

prefetchSize和global这两项,rabbitmq没有实现,暂且不研究;

prefetchConut只在no_ask=false的情况下生效

消费端:

public class LimitConsumer {

public static void main(String[] args) throws IOException, TimeoutException {

    //创建连接工厂,设置连接信息
    ConnectionFactory factory = new ConnectionFactory();
    factory.setHost("192.168.107.132");
    factory.setPort(5672);
    factory.setVirtualHost("/");

    //创建连接
    Connection connection = factory.newConnection();
    //创建信道
    Channel channel = connection.createChannel();

    String exchangeName = "test.qos.exchange";
    String queueName = "test.qos.queue";
    String routingKey = "qos.#";

    //创建exchange
    channel.exchangeDeclare(exchangeName, "topic", true, false, null);
    //创建一个队列 队列名称, 是否持久化, 此channel独占这个队列, autoDelete, 拓展参数
    channel.queueDeclare(queueName, true, false, false, null);
    //绑定
    channel.queueBind(queueName, exchangeName, routingKey);

    //----QOS限流, 每次处理1条消息----
    channel.basicQos(0, 1, false);
    //设置consumer 设置autoAck false
    channel.basicConsume(queueName, false, new MessageConsumer(channel));

}
static class MessageConsumer extends DefaultConsumer {

    //channel
    private Channel channel;

    public MessageConsumer(Channel channel) {
        super(channel);
        this.channel = channel;
    }
    //处理消息的逻辑
    @Override
    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
        super.handleDelivery(consumerTag, envelope, properties, body);
        System.out.println("message: " + new String(body));
        System.out.println("DeliveryTag: " + envelope.getDeliveryTag());
        //ack答应
        channel.basicAck(envelope.getDeliveryTag(), false);
    }
}

}

如果将basicAck注释掉,consumer将只会收到第一条信息

生产端:

public class LimitProducer {

public static void main(String[] args) throws IOException, TimeoutException {

    //创建连接工厂,设置连接信息
    ConnectionFactory factory = new ConnectionFactory();
    factory.setHost("192.168.107.132");
    factory.setPort(5672);
    factory.setVirtualHost("/");

    //创建连接
    Connection connection = factory.newConnection();
    //创建信道
    Channel channel = connection.createChannel();

    String exchangeName = "test.qos.exchange";
    String routingKey = "qos.save";
    String msg = "Hello RabbitMQ QOS Message!";

    //发送5条消息
    for (int i = 0; i < 5; i++){
        channel.basicPublish(exchangeName, routingKey, true, null, msg.getBytes());
    }
}

}

消费端Ack与重回队列

ack的三种答应:

  • basicAck : 返回正确的答应
  • basicNack: 返回否定的答应,且 requeue = true 消息会重回队列,循环重发
  • basicReject: 返回拒绝的答应, 消息会被丢弃

消费端的重回队列:

  • 消费端重回队列是为了对没有处理成功的消息, 把消息重新回递给Broker
  • 一般在实际应用中,都会关闭重回队列,也就是false

消费端:

public class ReQueueConsumer {

public static void main(String[] args) throws IOException, TimeoutException {

    //创建连接工厂,设置连接信息
    ConnectionFactory factory = new ConnectionFactory();
    factory.setHost("192.168.107.132");
    factory.setPort(5672);
    factory.setVirtualHost("/");

    //创建连接
    Connection connection = factory.newConnection();
    //创建信道
    Channel channel = connection.createChannel();

    String exchangeName = "test.ack.exchange";
    String queueName = "test.ack.queue";
    String routingKey = "ack.#";

    //创建exchange
    channel.exchangeDeclare(exchangeName, "topic", true, false, null);
    //创建一个队列 队列名称, 是否持久化, 此channel独占这个队列, autoDelete, 拓展参数
    channel.queueDeclare(queueName, true, false, false, null);
    //绑定
    channel.queueBind(queueName, exchangeName, routingKey);

    //设置consumer autoAck = false
    channel.basicConsume(queueName, false, new MessageConsumer(channel));

}
static class MessageConsumer extends DefaultConsumer {

    //channel
    private Channel channel;

    public MessageConsumer(Channel channel) {
        super(channel);
        this.channel = channel;
    }
    //处理消息的逻辑
    @Override
    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
        super.handleDelivery(consumerTag, envelope, properties, body);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        if((Integer)properties.getHeaders().get("num") == 0){
            //否定答应 消息唯一标识, 是否批量处理, 是否重回队列
            channel.basicNack(envelope.getDeliveryTag(), false, true);
            System.out.println("重回队列, DeliveryTag: " + envelope.getDeliveryTag());
        }else {
            //ack答应
            channel.basicAck(envelope.getDeliveryTag(), false);
            System.out.println("message: " + new String(body));
            System.out.println("DeliveryTag: " + envelope.getDeliveryTag());
        }
    }
}

}

num为0的消息被否认ack,接着处理完其他几条消息后, num为0的消息不断的重回队列

生产端:

public class ReQueueProducer {

public static void main(String[] args) throws IOException, TimeoutException {

    //创建连接工厂,设置连接信息
    ConnectionFactory factory = new ConnectionFactory();
    factory.setHost("192.168.107.132");
    factory.setPort(5672);
    factory.setVirtualHost("/");

    //创建连接
    Connection connection = factory.newConnection();
    //创建信道
    Channel channel = connection.createChannel();

    String exchangeName = "test.ack.exchange";
    String queueName = "test.ack.queue";
    String routingKey = "ack.save";

    String msg = "Hello RabbitMQ ACK Message!";

    //发送5条信息
    for(int i = 0; i < 5; i++){

        HashMap<String, Object> headers = new HashMap<>();
        headers.put("num", i);
        AMQP.BasicProperties properties = new AMQP.BasicProperties().builder()
                .deliveryMode(2) //持久化
                .contentEncoding("UTF-8")
                .expiration("20000")
                .headers(headers)
                .build();

        channel.basicPublish(exchangeName, routingKey, true, properties, msg.getBytes());
    }
}

}

TTL消息/队列

TTL是 Time To Live 的缩写, 也就是生存时间

RabbitMQ支持消息的过期时间, 在发送消息时可以进行指定

new AMQP.BasicProperties().builder()

                .expiration("20000")

RabbitMQ支持队列级消息的过期时间, 从消息入队开始计算, 只要超过了队列的超时时间配置, 那么消息会自动的清除

image-20200704160309883

在创建时可指定 Arguments 下面的参数:

  • message-ttl : 消息过期时间, 毫秒
  • max-length: 消息最大长度
  • ...

测试TTL队列

  • 新建exchange, 指定路由键为 ttl.abc 绑定到上面创建的 exp.queue 队列
  • 发送一条消息

    image-20200704161110566

  • 10秒之后这条消息会被清除

    image-20200704161315752

DLX死信队列

死信队列: Dead-Letter-Exchange

RabbitMQ中的死信队列是和exchange息息相关的

利用DLX, 当消息在一个队列中变成死信(dead message)之后, 它能被重新publish到另一个Exchange, 这个Exchange就是DLX

消息变成死信有以下几种情况

  • 消息被拒绝(basic.reject / basic.nack),并且requeue = false
  • 消息TTL过期
  • 队列达到最大长度

死信处理过程

  • DLX也是一个正常的Exchange,和一般的Exchange没有区别,它能在任何的队列上被指定,实际上就是设置某个队列的属性。
  • 当这个队列中有死信时,RabbitMQ就会自动的将这个消息重新发布到设置的Exchange上去,进而被路由到另一个队列。
  • 可以监听这个队列中的消息做相应的处理。

死信队列设置

  1. 设置死信队列的exchange和queue, 然后进行绑定:

    Exchange: dlx.exchange

    Queue: dlx.queue

    RoutingKey: #

  2. 然后正常声明交换机, 队列, 绑定, 只不过需要在队列上加一个参数设置死信交换机:

    arguments.put("x-dead-letter-exchange", "dlx.exchange");

如此, 消息在过期, 无法requeue, 或达到队列最大长度时, 消息可以直接路由到死信队列

消费端:

public class DLXConsumer {

public static void main(String[] args) throws IOException, TimeoutException {

    //创建连接工厂,设置连接信息
    ConnectionFactory factory = new ConnectionFactory();
    factory.setHost("192.168.107.132");
    factory.setPort(5672);
    factory.setVirtualHost("/");

    //创建连接
    Connection connection = factory.newConnection();
    //创建信道
    Channel channel = connection.createChannel();

    //普通的交换机和队列以及路由
    String exchangeName = "test.order.exchange";
    String queueName = "test.order.queue";
    String routingKey = "order.#";

    //创建exchange
    channel.exchangeDeclare(exchangeName, "topic", true, false, null);

    //-----设置死信队列exchange----
    String DLX_exchange = "dlx.exchange";
    HashMap<String, Object> arguments = new HashMap<>();
    arguments.put("x-dead-letter-exchange", DLX_exchange);
    //创建一个队列 设置arguments
    channel.queueDeclare(queueName, true, false, false, arguments);
    //绑定
    channel.queueBind(queueName, exchangeName, routingKey);

    //----声明死信队列和exchange并绑定----
    String DLX_queue = "dlx.queue";
    channel.exchangeDeclare(DLX_exchange, "topic", true, false, null);
    channel.queueDeclare(DLX_queue, true, false ,false, null);
    channel.queueBind(DLX_queue, DLX_exchange, "#");

    //设置consumer autoAck = false
    channel.basicConsume(queueName, false, new MessageConsumer(channel));
}
static class MessageConsumer extends DefaultConsumer {

    //channel
    private Channel channel;

    public MessageConsumer(Channel channel) {
        super(channel);
        this.channel = channel;
    }
    //处理消息的逻辑
    @Override
    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
        super.handleDelivery(consumerTag, envelope, properties, body);
        //NACK否认答应
        channel.basicNack(envelope.getDeliveryTag(), false, false);
        System.out.println("message: " + new String(body));
        System.out.println("DeliveryTag: " + envelope.getDeliveryTag());
    }
}

}

生产端:

public class DLXProducer {

public static void main(String[] args) throws IOException, TimeoutException {

    //创建连接工厂,设置连接信息
    ConnectionFactory factory = new ConnectionFactory();
    factory.setHost("192.168.107.132");
    factory.setPort(5672);
    factory.setVirtualHost("/");

    //创建连接
    Connection connection = factory.newConnection();
    //创建信道
    Channel channel = connection.createChannel();

    String exchangeName = "test.order.exchange";
    String routingKey = "order.save";

    String msg = "Hello RabbitMQ DLX Message!";

    for(int i = 0; i < 5; i++){

        AMQP.BasicProperties properties = new AMQP.BasicProperties().builder()
                .deliveryMode(2) //持久化
                .contentEncoding("UTF-8")
                .expiration("10000")  //过期时间10秒
                .build();
        channel.basicPublish(exchangeName, routingKey, true, properties, msg.getBytes());
    }
}

}

Spring Cloud Stream整合

img

屏蔽底层消息中间件的差异,降低切换成本,统一消息的编程模型

Spring Cloud Stream相当于是一个消息的中间代理, 消息的生产与消费, 可以使用不同的消息中间件

Spring Cloud Stream 为 Kafka 和 Rabbit MQ 提供了 Binder 实现

Spring Cloud Stream 中的几个重要概念:

  • _Destination Binders_:目标绑定器,目标指的是 kafka 还是 RabbitMQ,绑定器就是封装了目标中间件的包。如果操作的是 kafka 就使用 kafka binder ,如果操作的是 RabbitMQ 就使用 rabbitmq binder。
  • _Destination Bindings_:外部消息传递系统和应用程序之间的桥梁,提供消息的“生产者”和“消费者”(由目标绑定器创建)
  • _Message_:一种规范化的数据结构,生产者和消费者基于这个数据结构通过外部消息系统与目标绑定器和其他应用程序通信。

组成

说明

Middleware

中间件,目前只支持RabbitMQ和Kafka

Binder

Binder是应用与消息中间件之间的封装,可以很方便的连接,动态的改变消息类型

@Input

表示输入通道,通过该输入通道接收到的消息进入应用程序

@Output

表示输出通道,发布的消息将通过该通道离开应用程序

@StreamListener

监听队列,用于消费者队列的消息接收

@EnableBinding

指信道channel和exchange绑定在一起


打了个冷颤
19 声望0 粉丝

且听风吟


引用和评论

0 条评论