7.死信队列

DLX:Dead-Letter-Exchange,死信交换器、死信邮箱

RabbitMq进阶知识_初始化

如下情况下一个消息会进入DLX(Dead Letter Exchange)死信交换机。

死信交换机和死信队列其实就是对“死”信息进行转发和存储的结构,“死”信息可能是过期、队列达到最大长度被挤出来或者消费者拒收并且不重新投递的消息。

7.1消息过期

MessageProperties messageProperties=new MessageProperties();
//设置此条消息的过期时间为10秒
messageProperties.setExpiration("10000");

7.2 队列过期

Map<String, Object> arguments =new HashMap<>();
//指定死信交换机,通过x-dead-letter-exchange 来设置
arguments.put("x-dead-letter-exchange",EXCHANGE_DLX);
//设置死信路由key,value 为死信交换机和死信队列绑定的key,要一模一样,因为死信交换机是直连交换机
arguments.put("x-dead-letter-routing-key",BINDING_DLX_KEY);
//队列的过期时间
arguments.put("x-message-ttl",10000);
return new Queue(QUEUE_NORMAL,true,false,false,arguments);

7.3 队列达到最大长度(先入队的消息会被发送到DLX)

Map<String, Object> arguments = new HashMap<String, Object>();
//设置队列的最大长度 ,对头的消息会被挤出变成死信
arguments.put("x-max-length", 5);

7.4 消费者拒绝消息不进行重新投递

从正常的队列接收消息,但是对消息不进行确认,并且不对消息进行重新投递,此时消息就进入死信队列。

application.yml启动手动确认

spring:
    rabbitmq:
        listener:
            simple:
                acknowledge-mode: manual
/**
    * 监听正常的那个队列的名字,不是监听那个死信队列
    * 我们从正常的队列接收消息,但是对消息不进行确认,并且不对消息进行重新投递,此时消息就进入死信队列
    * channel 消息信道(是连接下的一个消息信道,一个连接下有多个消息信息,发消息/接消息都是通过信道完成的)
*/
@RabbitListener(queues = {RabbitConfig.QUEUE})
public void process(Message message, Channel channel) {
    System.out.println("接收到的消息:" + message);
    //对消息不确认, ack单词是 确认 的意思
    // void basicNack(long deliveryTag, boolean multiple, boolean requeue)
    // deliveryTag:消息的一个数字标签
    // multiple:翻译成中文是多个的意思,如果是true表示对小于deliveryTag标签下的消息都进行Nack不确认,false表示只对当前deliveryTag标签的消息Nack
    // requeue:如果是true表示消息被Nack后,重新发送到队列,如果是false,消息被Nack后,不会重新发送到队列
    try {
        System.out.println("deliveryTag = "+message.getMessageProperties().getDeliveryTag() );
        //要开启rabbitm消息消费的手动确认模式,然后才这么写代码;
        channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

7.5 消费者拒绝消息

开启手动确认模式,并拒绝消息,不重新投递,则进入死信队列。

/**
* 监听正常的那个队列的名字,不是监听那个死信队列
* 我们从正常的队列接收消息,但是对消息不进行确认,并且不对消息进行重新投递,此时消息就进入死信队列
*
* channel 消息信道(是连接下的一个消息信道,一个连接下有多个消息信息,发消息/接消息都是通过信道完成的)
*/
@RabbitListener(queues = {RabbitConfig.QUEUE})
public void process(Message message, Channel channel) {
    System.out.println("接收到的消息:" + message);
    //对消息不确认, ack单词是 确认 的意思
    // void basicNack(long deliveryTag, boolean multiple, boolean requeue)
    // deliveryTag:消息的一个数字标签
    // multiple:翻译成中文是多个的意思,如果是true表示对小于deliveryTag标签下的消息都进行Nack不确认,false表示只对当前deliveryTag标签的消息Nack
    // requeue:如果是true表示消息被Nack后,重新发送到队列,如果是false,消息被Nack后,不会重新发送到队列
    try {
        System.out.println("deliveryTag = " + message.getMessageProperties().getDeliveryTag());
        //要开启rabbitm消息消费的手动确认模式,然后才这么写代码;
        channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

8.延迟队列

场景:有一个订单,15分钟内如果不支付,就把该订单设置为交易关闭,那么就不能支付了。这类实现延迟任务的场景就可以用延迟队列来实现,当然除了延迟队列来实现,也可以有一些其他实现。

8.1 定时任务

每隔3秒扫一次数据库,查询过期的订单然后进行处理。

优点:简单,容易实现;

缺点:1.存在延迟(延迟时间不准确),如果你每隔一分钟扫一次,那么就有可能延迟1分钟;

2.如果订单量很大,每次扫描数据库,性能较差

8.2 被动取消

当用户查询订单的时候,判断订单是否超时,超时了就取消(交易关闭)。

优点:对于服务器而言,压力小

缺点:1.用户不查询订单,将永远处于待支付状态,会对数据统计等功能造成影响;

2.用户打开订单页面,有可能比较慢,因为要处理大量订单,用户体验稍差。

8.3 JDK延迟队列(单体应用,不能分布式)

DelayedQueue,无界阻塞队列,该队列只有在延迟期满的时候才能从中获取元素。

优点:实现简单,任务延迟低;

缺点:服务重启,宕机,数据丢失;只适合单机版,不适合集群;订单量大,可能内存不足而发生异常;

8.4 采用消息中间件(rabbitmq)

1.Rabbitmq本身不支持队列,可以使用TTL结合DLX的方式来实现消息的延迟投递,即把DLX跟某个队列绑定,到了指定时间,消息过期后,就会从DLX路由到这个队列,消费者可以从这个队列取走消息。

RabbitMq进阶知识_System_02

2.存在的问题:每次消费者从队头取消息,如果先发送的消息,消息延迟时间长,会影响后面的延迟时间段的消息的消费。

解决:不同延迟时间的消息要发到不同的队列上,同一个队列的消息,它的延迟时间应该一样。

RabbitMq进阶知识_延迟时间_03

8.5 使用rabbitmq-delayed-message-exchange延迟插件

启用插件

rabbitmq-plugins enable rabbitmq_delayed_message_exchange

RabbitMq进阶知识_初始化_04

消息发送后不会直接投递到队列,而是存储到Mnesia(嵌入式数据库),检查x-delay时间(消息头部);

延迟插件在RabbitMQ 3.5.7及以上的版本才支持,依赖Erlang/OPT 18.0及以上运行环境;

Mnesia是一个小型数据库,不适合于大量延迟消息的实现。

解决了消息过期时间不一致的问题。

配置代码

@Component
@Slf4j
public class RabbitConfig {
    public static final String EXCHANGE = "exchange:plugin";
    public static final String QUEUE = "queue.plugin";
    public static final String KEY = "plugin";
    @Bean
    public CustomExchange customExchange() {
        Map<String, Object> arguments = new HashMap<>();
        arguments.put("x-delayed-type", "direct");
        // CustomExchange(String name, String type, boolean durable, boolean autoDelete, Map<String, Object> arguments)
        return new CustomExchange(EXCHANGE, "x-delayed-message", true, false, arguments);
    }
    @Bean
    public Queue queue() {
        return QueueBuilder.durable(QUEUE).build();
    }
    @Bean
    public Binding binding(CustomExchange customExchange, Queue queue) {
        return BindingBuilder.bind(queue).to(customExchange).with(KEY).noargs();
    }
}

发消息参考代码

MessageProperties messageProperties=new MessageProperties();
messageProperties.setHeader("x-delay",16000);
String msg = "hello world";
Message message=new Message(msg.getBytes(),messageProperties);
rabbitTemplate.convertAndSend(RabbitConfig.EXCHANGE, "plugin", message);
log.info("发送完毕,发送时间为:{}",new Date());

9.消息的可靠性

消息的可靠性投递就是要保证消息投递过程中每一个环节都要成功,那么这肯定会牺牲一些性能,性能与可靠性是无法兼得的;

如果业务实时一致性要求不是特别高的场景,可以牺牲一些可靠性来换取性能。

RabbitMq进阶知识_初始化_05

①代表消息从生产者发送到Exchange;

②代表消息从Exchange路由到Queue;

③代表消息在Queue中存储;

④代表消费者监听Queue并消费消息。

9.1确保消息发送到RabbitMQ服务器的交换机上

可能因为网络或者Broker的问题导致①失败,而此时应该让生产者知道消息是否正确发送到了Broker的exchange中;

有两种解决方案:

第一种:开启Confirm(确认)模式;(异步)

第二种:开启Transaction(事务)模式;(性能低,实际项目中很少用)

9.1.1Confirm模式简介

消息的confirm确认机制,是指生产者投递消息后,到达了消息服务器Broker里面的exchange交换机,则会给生产者一个应答,生产者接收到应答,用来确定这条消息是否正确地发送到Broker的exchange中,这也是消息可靠性投递的重要保障。

RabbitMq进阶知识_System_06

9.1.2具体代码

1 配置文件application.yml 开启确认模式:spring.rabbitmq.publisher-confirm-type=correlated
2 写一个类实现implements RabbitTemplate.ConfirmCallback,判断成功和失败的ack结果,可以根据具体的结果,如果ack为false,对消息进行重新发送或记录日志等处理;
设置rabbitTemplate的确认回调方法
3 rabbitTemplate.setConfirmCallback(messageConfirmCallBack);

参考代码

@Component
public class MessageConfirmCallBack implements RabbitTemplate.ConfirmCallback {

    /**
     * 交换机收到消息后,会回调该方法
     *
     * @param correlationData  相关联的数据
     * @param ack  有两个取值,true和false,true表示成功:消息正确地到达交换机,反之false就是消息没有正确地到达交换机
     * @param cause 消息没有正确地到达交换机的原因是什么
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        System.out.println("correlationData = " + correlationData);
        System.out.println("ack = " + ack);
        System.out.println("cause = " + cause);

        if (ack) {
            //正常
        } else {
            //不正常的,可能需要记日志或重新发送
        }
    }
}

发消息参考代码(会设置回调的类)

@Service
public class MessageService {

    @Resource
    private RabbitTemplate rabbitTemplate;

    @Resource
    private MessageConfirmCallBack messageConfirmCallBack;

    @PostConstruct //bean在初始化的时候,会调用一次该方法,只调用一次,起到初始化的作用
    public void init() {
        rabbitTemplate.setConfirmCallback(messageConfirmCallBack);
    }

    /**
     * 发送消息
     */
    public void sendMessage() {
        //关联数据对象
        CorrelationData correlationData = new CorrelationData();
        correlationData.setId("O159899323"); //比如设置一个订单ID,到时候在confirm回调里面,你就可以知道是哪个订单没有发送到交换机上去
        rabbitTemplate.convertAndSend(RabbitConfig.EXCHANGE + 123, "info", "hello", correlationData);
        System.out.println("消息发送完毕......");
    }
}

9.2 确保消息路由到正确的队列

可能因为路由关键字错误,或者队列不存在,或者队列名称错误导致②失败。

使用return模式,可以实现消息无法路由的时候返回给生产者。

另一种方式就是使用备份交换机(alternate-exchange),无法路由的消息会发送到这个备份交换机上。

9.2.1 return模式

注意配置文件中,开启退回模式

spring.rabbitmq.publisher-returns: true

使用rabbitTemplate.setReturnsCallback设置回退函数,当消息从exchange路由到queue失败后,则会将消息回退给producer,并执行回调函数returnedMessage。

@Component
public class MessageReturnCallBack implements RabbitTemplate.ReturnsCallback {

    /**
     * 当消息从交换机 没有正确地 到达队列,则会触发该方法
     * 如果消息从交换机 正确地 到达队列了,那么就不会触发该方法
     *
     * @param returned
     */
    @Override
    public void returnedMessage(ReturnedMessage returned) {
        System.out.println("消息return模式:" + returned);
    }
}

发送消息代码

@Service
public class MessageService {

    @Resource
    private RabbitTemplate rabbitTemplate;

    @Resource
    private MessageReturnCallBack messageReturnCallBack;

    @PostConstruct //bean在初始化的时候,会调用一次该方法,只调用一次,起到初始化的作用
    public void init() {
        rabbitTemplate.setReturnsCallback(messageReturnCallBack);
    }

    /**
     * 发送消息
     */
    public void sendMessage() {
        rabbitTemplate.convertAndSend(RabbitConfig.EXCHANGE, "info123", "hello");
        System.out.println("消息发送完毕......");
    }
}

代码二:发送消息时直接实现RabbitTemplate.ReturnsCallback

@Service
public class MessageService implements RabbitTemplate.ReturnsCallback {

    @Resource
    private RabbitTemplate rabbitTemplate;

    @PostConstruct //bean在初始化的时候,会调用一次该方法,只调用一次,起到初始化的作用
    public void init() {
        rabbitTemplate.setReturnsCallback(this);
    }

    /**
     * 当消息从交换机 没有正确地 到达队列,则会触发该方法
     * 如果消息从交换机 正确地 到达队列了,那么就不会触发该方法
     *
     * @param returned
     */
    @Override
    public void returnedMessage(ReturnedMessage returned) {
        System.out.println("消息return模式:" + returned);
    }

    /**
     * 发送消息
     */
    public void sendMessage() {
        rabbitTemplate.convertAndSend(RabbitConfig.EXCHANGE, "info123", "hello");
        System.out.println("消息发送完毕......");
    }
}

代码三:同时实现RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback

@Service
public class MessageService implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback {

    @Resource
    private RabbitTemplate rabbitTemplate;

    @PostConstruct //bean在初始化的时候,会调用一次该方法,只调用一次,起到初始化的作用
    public void init() {
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnsCallback(this);
    }

    /**
     * 交换机收到消息后,会回调该方法
     *
     * @param correlationData  相关联的数据
     * @param ack  有两个取值,true和false,true表示成功:消息正确地到达交换机,反之false就是消息没有正确地到达交换机
     * @param cause 消息没有正确地到达交换机的原因是什么
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        System.out.println("correlationData = " + correlationData);
        System.out.println("ack = " + ack);
        System.out.println("cause = " + cause);

        if (ack) {
            //正常
        } else {
            //不正常的
        }
    }

    /**
     * 当消息从交换机 没有正确地 到达队列,则会触发该方法
     * 如果消息从交换机 正确地 到达队列了,那么就不会触发该方法
     *
     * @param returned
     */
    @Override
    public void returnedMessage(ReturnedMessage returned) {
        System.out.println("消息return模式:" + returned);
    }

    /**
     * 发送消息
     */
    public void sendMessage() {
        //关联数据对象
        CorrelationData correlationData = new CorrelationData();
        correlationData.setId("O159899323"); //比如设置一个订单ID,到时候在confirm回调里面,你就可以知道是哪个订单没有发送到交换机上去
        rabbitTemplate.convertAndSend(RabbitConfig.EXCHANGE, "info123", "hello", correlationData);
        System.out.println("消息发送完毕......");
    }
}

9.2.2 备用交换机

备用交换机使用场景

当消息经过交换机准备路由给队列的时候,发现没有对应的队列可以投递消息,在rabbitmq中会默认丢弃消息,如果我们想要监测哪些消息没有被投递到对应的队列,我们可以用备用交换机来实现,可以接收备用交换机的消息,然后记录日志或发送报警信息。

主要代码和注意事项

备用交换机示例如下:

注意:备用交换机一般使用fanout交换机

测试时:指定一个错误路由

重点:普通交换机设置参数绑定到备用交换机

Map<String, Object> arguments = new HashMap<>();
//指定当前正常的交换机的备用交换机是谁
arguments.put("alternate-exchange", EXCHANGE_ALTERNATE); 
//DirectExchange(String name, boolean durable, boolean autoDelete, Map<String, Object> arguments)
return new DirectExchange(EXCHANGE, true, false, arguments);
//return ExchangeBuilder.directExchange(EXCHANGE).withArguments(args).build();

配置代码

@Configuration
public class RabbitConfig {

    //交换机的名字,就是一个字符串
    public static final String EXCHANGE = "exchange";
    //队列的名字,就是一个字符串
    public static final String QUEUE = "queue";
    //定义的一个路由键
    public static final String INFO = "info";

    //-------------------------------------------
    //交换机的名字,就是一个字符串
    public static final String EXCHANGE_ALTERNATE = "exchange.alternate";
    //队列的名字,就是一个字符串
    public static final String QUEUE_ALTERNATE = "queue.alternate";
    //定义的一个路由键
    public static final String ALTERNATE = "alternate";

    @Bean
    public DirectExchange directExchange() {
        Map<String, Object> arguments = new HashMap<>();
        arguments.put("alternate-exchange", EXCHANGE_ALTERNATE); //指定当前正常的交换机的备用交换机是谁
        //DirectExchange(String name, boolean durable, boolean autoDelete, Map<String, Object> arguments)
        return new DirectExchange(EXCHANGE, true, false, arguments);
    }

    /**
     * 声明一个队列
     *
     * @return
     */
    @Bean
    public Queue queue() {
        return QueueBuilder.durable(QUEUE).build();
    }

    /**
     * @Qualifier 限定bean的名字是 directExchange 的Bean
     *
     * @param directExchange
     * @return
     */
    @Bean
    public Binding binding(DirectExchange directExchange, Queue queue) {
        return BindingBuilder.bind(queue).to(directExchange).with(INFO);
    }

    //-----------------------------------------

    /**
     * 备用交换机需要用Fanout交换机;
     *
     * @return
     */
    @Bean
    public FanoutExchange alternateExchange() {
        //DirectExchange(String name, boolean durable, boolean autoDelete, Map<String, Object> arguments)
        return new FanoutExchange(EXCHANGE_ALTERNATE, true, false);
    }
    @Bean
    public Queue alternateQueue() {
        return QueueBuilder.durable(QUEUE_ALTERNATE).build();
    }
    @Bean
    public Binding alternateBnding(FanoutExchange alternateExchange, Queue alternateQueue){
        return BindingBuilder.bind(alternateQueue).to(alternateExchange);
        }
    }

发送消息代码

@Service
public class MessageService {

    @Resource
    private RabbitTemplate rabbitTemplate;

    /**
     * 发送消息
     */
    public void sendMessage() {
        //我们故意写错路由key,由于我们正常交换机设置了备用交换机,所以该消息就会进入备用交换机
        //从而进入备用对列,我们可以写一个程序接收备用对列的消息,接收到后通知相关人员进行处理
        //如果正常交换机没有设置备用交换机,则该消息会被抛弃。
            rabbitTemplate.convertAndSend(RabbitConfig.EXCHANGE, "info1223", "hello");
            System.out.println("消息发送完毕......");
        }
    }

9.3确保消息在队列正确地存储

可能因为系统宕机、重启、关闭等情况导致存在在队列的消息丢失,即③出现问题;

解决方案:

(1)队列持久化

QueueBuilder.durable(QUEUE).build();

(2)交换机持久化

ExchangeBuilder.directExchange(EXCHANGE).durable(true).build();

(3)消息持久化

MessageProperties messageProperties = new MessageProperties();
//设置消息持久化,当然它默认就是持久化,所以可以不用设置,可以查看源码
 messageProperties.setDeliveryMode(MessageDeliveryMode.PERSISTENT);

(4)集群、镜像队列、高可用

9.4确保消息从队列正确地投递到消费者

采用消费者消费时的手动ack确认机制来保证;

如果消费者收到消息后未来得及处理即发生异常,或者处理过程中发生异常,会导致④失败。为了保证消息从队列可靠地到达消费者,RabbitMQ提供了消息确认机制(message acknowledgement)。

#开启手动ack消息消费确认
spring.rabbitmq.listener.simple.acknowledge-mode=manual

消费者在订阅队列时,通过上面的配置,不自动确认,采用手动确认,RabbitMQ会等待消费者显式地回复确认信号后才从队列中删除消息;

如果消息消费失败,也可以调用basicReject()或者basicNack()来拒绝当前消息而不是确认。如果requeue参数设置为true,可以把这条消息重新存入队列,以便发给下一个消费者(当然,只有一个消费者地时候,这种方式可能会出现无限循环重复消费的情况,可以投递到新的队列中,或者只打印异常日志)。

代码参考7.4和7.5

10.消息的幂等性

幂等性:对于一个资源,不管请求一次还是请求多次,对该资源本身造成的影响应该是相同的,不能因为重复的请求而对该资源重复造成影响;

比如一个订单支付两次,但是只会扣款一次,第二次支付不会扣款,说明这个支付接口是具有幂等性的。

数据库操作的select,delete都是具有幂等性的。

如何避免消息的重复消费问题?(消费者消费时的幂等性)

全局唯一ID+redis。

生产者在发送消息时,为每条消息设置一个全局唯一的messageId,消费者拿到消息后,使用setnx命令,将messageId作为key放到redis中;setnx(messageId, 1),若返回1,说明之前没有消费过,正常消费;如返回0,说明这条消息之前已消费过,抛弃。

参考代码:

//1、把消息的唯一ID写入redis
    boolean flag = stringRedisTemplate.opsForValue().setIfAbsent("idempotent:" + orders.getId(), String.valueOf(orders.getId())); //如果redis中该key不存在,那么就设置,存在就不设置
    if (flag) { //key不存在返回true
        //相当于是第一次消费该消息
        //TODO 处理业务
        System.out.println("正常处理业务....." + orders.getId());
    }

11.交换机详细属性

11.1 具体参数

1.Name:交换机名称,就是一个字符串;

2.Type:交换机类型,direct,topic,fanout,headers四种;

3.Durability:持久化,声明交换机是否持久化,代表交换机在服务器重启后是否还存在;

4.Auto delete:是否自动删除,曾经有队列绑定到该交换机,后来解绑了,那就会自动删除交换机;

5.Internal:内部使用的,如果是yes,客户端无法直接发消息到此交换机,它只能用于交换机与交换机绑定;

6.Arguments:只有一个取值alternate-exchange,表示备用交换机。

12.队列详细属性

12.1 具体参数

Type:队列类型

Name:队列名称,就是一个字符串,随便一个字符串就可以

Durability:声明队列是否持久化,代表队列在服务器重启后是否还存在

Auto delete:是否自动删除,如果为true,当没有消费者连接到这个队列的时候,队列会自动删除

Exclusive:exclusive属性的队列只对首次声明它的连接可见,并且在连接断开时自动删除;基本上不设置它,设置成false

Arguments:队列的其他属性,例如指定DLX(死信交换机等):

1.x-expires: Number

当队列在指定的时间未被访问,则队列将被自动删除;

2.x-message-ttl: Number

发布的消息在队列中存在多长时间后被取消(单位毫秒);

3.x-overflow: String

设置队列溢出行为,当达到队列的最大长度时,消息会发生什么,有效值为Drop Head或Reject Publish,默认值为Drop Head,即丢弃队列头部的消息;

4.x-max-length: Number

队列所能容下消息的最大长度,当超过长度后,新消息将会覆盖最前面的消息,类似于redis的LRU算法;

5.x-single-active-consumer:默认为false

激活单一的消费者,也就是该队列只能有一个消费者消费消息;

6.x-max-length-bytes: Numer

限定队列的最大占用空间,当超出后也使用类似于redis的LRU算法;

7.x-dead-letter-exchange: String

指定队列关联的死信交换机,有时候我们希望当队列的消息达到上限后溢出的消息不会被删除,而是走到另一个队列中保存起来;

8.x-dead-letter-routing-key: String

指定死信交换机的路由键,一般和6一起定义;

9.x-max-priority: Number

如果将一个队列加上优先级参数,那么该队列为优先级队列;

(1)给队列加上优先级参数使其成为优先级队列

x-max-priority=255【0-255取值范围】

(2)给消息加上优先级属性

通过优先级特性,将一个队列实现插队消费

MessageProperties messageProperties=new MessageProperties();
messageProperties.setPriority(8);

10.x-queue-mode: String(理解下即可)

懒队列,在磁盘上尽可能多地保留消息以减少RAM使用,如果未设置,则队列将保留内存缓存以尽可能快的传递消息;

11.x-queue-master-locator: String(用的较少)

在集群模式下设置队列分配到的主节点位置信息。

每个queue都有一个master节点,所有对于queue的操作都是事先在master上完成,之后在slave上进行相同的操作;

每个不同的queue可以坐落在不同的集群节点上,这些queue如果配置了镜像队列,那么会有1个master和多个slave。

基本上所有的操作都落在master上,那么如果这些queues的master都落在个别的服务器节点上,而其他节点又很空闲,这样就无法做到负载均衡,那么势必会影响性能;

关于master queue host的分配有几种策略,可以在queue声明的时候使用x-queue-master-locator参数,或者在policy上设置queue-master-locator,或者直接在rabbitmq的配置文件中定义queue-master-locator,有三种可供选择的策略:

(1)min-masters:选择master queue数最少的哪个服务节点host

(2)client-local:选择与client相连接的那个服务节点host

(3)random:随机分配

12.2 参考代码

@Configuration
public class RabbitConfig {

    public static final String EXCHANGE = "exchange";
    public static final String QUEUE = "queue";
    public static final String KEY = "info";

    QueueBuilder builder;

    @Bean
    public DirectExchange directExchange() {
        return ExchangeBuilder.directExchange(EXCHANGE).build();
    }

    @Bean
    public Queue queue() {
        Map<String, Object> arguments = new HashMap<>();
        //arguments.put("x-expires", 5000);

        //arguments.put("x-max-length", 5);
        //arguments.put("x-overflow", "reject-publish");

        arguments.put("x-single-active-consumer", false); //TODO ???

        //arguments.put("x-max-length-bytes", 20); // 单位是字节

        //arguments.put("x-max-priority", 10); // 0-255 //表示把当前声明的这个队列设置成了优先级队列,那么该队列它允许消息插队


        //将队列设置为延迟模式,在磁盘上保留尽可能多的消息,以减少RAM内存的使用,如果未设置,队列将保留内存缓存以尽可能快地传递消息;
        //有时候我们把这种队列叫:惰性队列
        //arguments.put("x-queue-mode", "lazy");

        //设置队列版本。默认为版本1。
        //版本1有一个基于日志的索引,它嵌入了小消息。
        //版本2有一个不同的索引,可以在许多场景中提高内存使用率和性能,并为以前嵌入的消息提供了按队列存储。
        //arguments.put("x-queue-version", 2);

        // x-queue-master-locator:在集群模式下设置镜像队列的主节点信息。
        //arguments.put("x-queue-master-locator", QueueBuilder.LeaderLocator.clientLocal.getValue());


        //-------------------------
        //arguments.put("x-expires", 10000); //自动过期,10秒
        //arguments.put("x-message-ttl", 10000); //自动过期,10秒,不会删除队列
        //QueueBuilder 类里面有定义,设置队列溢出行为,当达到队列的最大长度时消息会发生什么,有效值是drop-head、reject-publish
        //arguments.put("x-max-length", 5);
        //arguments.put("x-overflow", QueueBuilder.Overflow.dropHead.getValue());

        //表示队列是否是单一活动消费者,true时,注册的消费组内只有一个消费者消费消息,其他被忽略,false时消息循环分发给所有消费者(默认false)
        //arguments.put("x-single-active-consumer", true);

        // x-max-length-bytes,队列消息内容占用最大空间,受限于内存大小,超过该阈值则从队列头部开始删除消息;
        //arguments.put("x-max-length-bytes", 10);

        //参数是1到255之间的正整数,表示队列应该支持的最大优先级,数字越大代表优先级越高,没有设置priority优先级字段,那么priority字段值默认为0;如果优先级队列priority属性被设置为比x-max-priority大,那么priority的值被设置为x-max-priority的值。
        //arguments.put("x-max-priority", 10);

        //将队列设置为延迟模式,在磁盘上保留尽可能多的消息,以减少RAM的使用;如果未设置,队列将保留内存缓存以尽可能快地传递消息;
        //arguments.put("x-queue-mode", "lazy");

        arguments.put("x-queue-version", 2);

        // x-queue-master-locator:在集群模式下设置镜像队列的主节点信息。
        arguments.put("x-queue-master-locator", QueueBuilder.LeaderLocator.clientLocal.getValue());
        //---------------------------------------------


        // Queue(String name, boolean durable, boolean exclusive, boolean autoDelete, @Nullable Map<String, Object> arguments)
        return new Queue(QUEUE, true, false, false, arguments);
    }

    @Bean
    public Binding binding(DirectExchange directExchange, Queue queue) {
        return BindingBuilder.bind(queue).to(directExchange).with(KEY);
    }
}

实验durable参数:重启rabbitmq-server,队列丢失;

实验auto delete参数:加入接收者,发现停掉服务,那么就没有消费者了,队列就会自动删除。

13.RabbitMQ集群与高可用

RabbitMQ的集群分两种模式,一种是默认集群模式,一种是镜像集群模式;

在RabbitMQ集群中所有的节点(一个节点就是一个RabbitMQ的broker服务器)被归为两类:一类是磁盘节点,一类是内存节点;

磁盘节点会把集群的所有信息(比如交换机、绑定、队列等信息)持久化到磁盘中,而内存节点只会将这些消息保存到内存中,如果该节点宕机或重启,内存节点的数据会全部丢失,而磁盘节点的数据不会丢失。

13.1 默认集群模式

默认集群模式也叫普通集群模式或者内置集群模式;

RabbitMQ默认集群模式,只会把交换机、队列、虚拟主机等元数据信息在各个节点同步,而具体队列中的消息内容不会在各个节点中同步;

RabbitMq进阶知识_初始化_07

元数据:

队列元数据:队列名称和属性(是否可持久化,是否自动删除)

交换机元数据:交换机名称、类型和属性

绑定元数据:交换机和队列的绑定列表

vhost元数据:vhost内的相关属性,如安全属性等

当用户访问其中任何一个RabbitMQ节点时,查询到的queue/user/exchange/vhost等信息都是相同的;

当集群中队列的具体信息数据只在队列的拥有者节点保存,其他节点只知道队列的元数据和指向该节点的指针,所以其他节点接收到不属于该节点队列的消息时会将该消息传递到该队列的拥有者节点上;

为什么集群不复制队列内容和状态到所有节点

(1)存储空间

(2)性能

如果消息需要复制到集群中每个节点,网络开销不可避免,持久化消息还需要写磁盘,占用磁盘空间;

那么问题来了

如果有一个消息生产者或者消费者通过amqp-client的客户端连接到节点1进行消息的发送和接收,那么此时集群中的消息收发只与节点1相关,这个没有任何问题;

如果客户端相连的是节点2或者节点3(队列1数据不在该节点上),那么情况又会是怎么样呢?

如果消息生产者所连接的是节点2或者节点3,此时队列1的完整数据不在该节点上,那么在发送消息过程中这两个节点主要起了一个路由转发作用,根据这两个节点上的元数据(也就是指向queue的owner node的指针)转发至节点1上,最终发送的消息还是会存储至节点1的队列1上;

同样,如果消费者连接节点2或者节点3,那这两个节点也会作为路由节点起到转发作用,将会从节点1的队列中获取消息进行消息。

有什么意义,去掉节点2和3有什么不同

13.2 镜像集群模式

镜像模式是基于默认集群模式加上一定的配置得来的;

在默认模式下的RabbitMQ集群会把所有节点的交换机、绑定、队列的元数据进行复制确保所有节点都有一份相同的元数据信息,队列数据分为两种:一种是队列的元数据信息(比如队列的最大容量、队列名称等配置信息);另一种是队列里面的消息;

镜像模式则是把所有的队列数据完全同步,包括元数据信息和消息数据信息,当然这对性能会有一定影响,当对数据可靠性要求较高时,可以使用镜像模式;

镜像队列配置命令:

./rabbitmqctl set_policy [-p Vhost] Name Pattern Definition [Priority]

-p Vhost:可选参数,针对指定vhost下的queue进行设置;

Name:policy的名称,自己取名即可

Pattern:queue的匹配模式(正则表达式)

Definition:镜像定义,包括三个部分ha-mode, ha-params, ha-sync-mode;(json格式)

{“ha-mode”:”exactly”,”ha-params”:2}

  ha-mode:指明镜像队列的模式,有效值为 all/exactly/nodes

  --all:表示在集群中所有的节点上进行镜像

  --exactly:表示在指定个数的节点上进行镜像,节点的个数由ha-params指定

  --nodes:表示在指定的节点上进行镜像,节点名称通过ha-params指定

  ha-params:ha-mode模式需要用到的参数

  ha-sync-mode:进行队列中消息的同步方式,有效值为automatic和manual

Priority:可选参数,policy的优先级

比如像配置所有以policy_开头的队列进行镜像,镜像数量为2,那么命令如下(在任意节点执行如下命令):

./rabbitmqctl set_policy -p powernode ha_policy "^policy_" '{"ha-mode":"exactly","ha-params":2,"ha-sync-mode":"automatic"}'

如果要在所有节点所有队列上进行镜像,则(可在任意节点执行如下命令):

所有节点、所有虚拟主机、所有队列都进行镜像:

./rabbitmqctl set_policy ha-all "^" '{"ha-mode":"all"}'

在默认集群模式的基础上,执行上面这些命令就可以把一个默认集群模式变成镜像集群模式。

14.在SpringBoot应用中使用RabbitMQ

SpringBoot应用可以完成自动配置及依赖注入---可以通过spring直接提供与mq的连接对象

14.1 消息生产者

  • 创建SpringBoot应用,添加依赖

RabbitMq进阶知识_System_08

  • 配置application.yml
server:
	port: 9001
spring:
	application:
		name: producer
	rabbitmq:
		host: 47.96.11.185
		port: 5672
		virtual-host: host1
		username: ytao
		password: admin123
  • 发送消息
@Service
public class TestService{
    @Resource
    private AmqpTemplate amqpTemplate;
    
    public void sendMsg(String msg){
        //1.发送消息到队列
        amqpTemplate.convertAndSend("queue1",msg);
        
        //2.发送消息到交换机(订阅交换机)
        amqpTemplate.convertAndSend("ex1","",msg);
        
        //3.发送消息到交换机(路由交换机)
        amqpTemplate.convertAndSend("ex2","a",msg);
    }
}

14.2 消息消费者

  • 创建项目添加依赖
  • 配置yml
  • 接收消息
@Service
//@RabbitListener(queues={"queue1","queue2"})
@RabbitListener(queues="queue1")
public class ReceiveMsgService{
	@RabbitHandler
	public void receiveMsg(String msg){
        System.out.println("接受消息:"+msg);
    }
}

使用@RabbitListener对消息队列进行监听,只要服务启动,消息队列一有消息就可以接收到,不需要调用这个函数。

15.RabbitMQ保证消息顺序

15.1 保证顺序性的意义

消息队列中的若干消息如果是对同一个数据进行操作,这些操作具有前后的关系,必须要按前后的顺序执行,否则可能会造成数据异常。

比如通过mysql binlog进行两个数据库的数据同步,由于对数据库的数据操作是具有顺序性的,如果操作顺序搞反,就可能会造成不可估量的错误。比如对数据库一条数据一次进行了插入 -> 更新 -> 删除操作,如果在同步过程中,消息的顺序变成了删除 -> 插入 -> 更新,那么原本应该被删除的数据,就没有被删除,造成数据的不一致问题。

15.2 出现错乱的场景

15.2.1 错乱场景一

  • 一个queue,多个consumer去消费,这样就会造成顺序的错误。consumer从mq里面读取数据是有序的,但是每个consumer的执行时间是不固定的,无法保证先读到消息的consumer一定先完成操作,这样就会出现消息并没有按照顺序执行,造成数据顺序错误。

RabbitMq进阶知识_延迟时间_09

15.2.2 错乱场景二

一个queue对应一个consumer,但是consumer里面进行了多线程消费,这样也会造成消息消费顺序错误。

RabbitMq进阶知识_System_10

15.3 保证消息的消费顺序

15.3.1 解决方案一

拆分成多个queue,每个queue一个consumer,把具有先后顺序的消息发送到同一个queue里面。就是多一些queue而已,确实是麻烦点;这样也会造成吞吐量下降,可以在消费者内部采用多线程的方式去消费。

RabbitMq进阶知识_初始化_11

15.3.2 解决方案二

或者就是一个queue但是对应一个consumer,然后这个consumer内部用内存队列做排队,然后分发给底层不同的worker来处理。

RabbitMq进阶知识_延迟时间_12


声明:文中内容及图片源自各种教程以及帖子。