消息队列关键点

消息队列关键点

没有最优配置,不同使用场景不同配置,理解消息队列是基础,以RabbitMQ为基础进行讲解

三个方面讲解
  1. rabbitmq基本概念

  2. 常见问题

  3. 应用场景

rabbitmq基本概念

开发语言

  • Erlang,actor并发模型(scala也采用此并发模型)

协议

  • AMQP,Advanced Message Queuing Protocol,提供统一消息服务的应用层标准高级消息队列协议(高级消息队列协议

标准协议,跨语言支持较好

核心概念

AMQP协议核心概念

1、server:又称broker,接受客户端连接,实现AMQP实体服务。
2、connection:连接和具体broker网络连接。
3、channel:网络信道,几乎所有操作都在channel中进行,channel是消息读写的通道。客户端可以建立多个channel,每个channel表示一个会话任务。
4、message:消息,服务器和应用程序之间传递的数据,由properties和body组成。properties可以对消息进行修饰,比如消息的优先级,延迟等高级特性;body是消息实体内容。
5、Virtual host:虚拟主机,用于逻辑隔离,最上层消息的路由。一个Virtual host可以若干个Exchange和Queue,同一个Virtual host不能有同名的Exchange或Queue。
6、Exchange:交换机,接受消息,根据路由键转发消息到绑定的队列上。
7、banding:Exchange和Queue之间的虚拟连接,binding中可以包括routing key
8、routing key:一个路由规则,虚拟机根据他来确定如何路由 一条消息。
9、Queue:消息队列,用来存放消息的队列。

exchange类型(先要声明一个队列,队列是基本存储单元,由exchange和key决定消息存储在哪个队列。)
  • direct,topic,fanout

    1、Direct Exchange(完全匹配),所有发送到Direct Exchange的消息被转发到RouteKey 中指定的Queue,Direct Exchange可以使用默认的默认的Exchange (default Exchange),默认的Exchange会绑定所有的队列,所以Direct可以直接使用Queue名(作为routing key )绑定。或者消费者和生产者的routing key完全匹配。
    2、Toptic Exchange(模糊匹配),是指发送到Topic Exchange的消息被转发到所有关心的Routing key中指定topic的Queue上。Exchange 将routing key和某Topic进行模糊匹配,此时队列需要绑定一个topic。所谓模糊匹配就是可以使用通配符,“#”可以匹配一个或多个词,“”只匹配一个词比如“log.#”可以匹配“log.info.test” "log. "就只能匹配log.error。
    3、Fanout Exchange(与key无关):不处理路由键,只需简单的将队列绑定到交换机上。发送到改交换机上的消息都会被发送到与该交换机绑定的队列上。Fanout转发是最快的。

消息模型
  • 队列模型,单播(direct)
    生产者往某个队列里面发送消息,一个队列可以存储多个生产者的消息,一个队列也可以有多个消费者, 但是消费者之间是竞争关系,即每条消息只能被一个消费者消费。

  • 发布/订阅模型,(广播)fanout

    为了解决一条消息能被多个消费者消费的问题,发布/订阅模型就来了。该模型是将消息发往一个Topic即主题中,所有订阅了这个 Topic 的订阅者都能消费这条消息。

消息接收
  • rabbitmq的队列是基本存储单元,不再被分区或者分片,对于我们已经创建了的队列,消费端要指定从哪一个队列接收消息。

  • 当rabbitmq队列拥有多个消费者的时候,队列收到的消息将以轮询的分发方式发送给消费者。每条消息只会发送给订阅列表里的一个消费者,不会重复。

  • 消息推模式,对消费端不友好

    如果某些消费者的任务比较繁重,那么可以设置basicQos限制信道上消费者能保持的最大未确认消息的数量,在达到上限时,rabbitmq不再向这个消费者发送任何消息。

    • 消费端限流

      假设我们有个场景,首先,我们有个rabbitMQ服务器上有上万条消息未消费,然后我们随便打开一个消费者客户端,会出现:巨量的消息瞬间推送过来,但是我们的消费端无法同时处理这么多数据。

      rabbitMQ提供了一种qos(服务质量保证)的功能,即非自动确认消息的前提下,如果有一定数目的消息(通过consumer或者Channel设置qos)未被确认,不进行新的消费。

      void basicQOS(unit prefetchSize,ushort prefetchCount,Boolean global)方法。

      prefetchSize:0 单条消息的大小限制。0就是不限制,一般都是不限制。
      prefetchCount: 设置一个固定的值,告诉rabbitMQ不要同时给一个消费者推送多余N个消息,即一旦有N个消息还没有ack,则consumer将block掉,直到有消息ack
      global:truefalse 是否将上面的设置用于channel,也是就是说上面设置的限制是用于channel级别的还是consumer的级别的。

消息存储

  • 内存、磁盘。支持少量堆积(看机器配置,200W消息堆积性能下降特别厉害)

    rabbitmq的消息分为持久化的消息和非持久化消息,不管是持久化的消息还是非持久化的消息都可以写入到磁盘。持久化的消息在到达队列时就写入到磁盘,并且如果可以,持久化的消息也会在内存中保存一份备份,这样可以提高一定的性能,当内存吃紧的时候会从内存中清除。非持久化的消息一般只存在于内存中,在内存吃紧的时候会被换入到磁盘中,以节省内存。
    引入镜像队列机制,可将重要队列“复制”到集群中的其他broker上,保证这些队列的消息不会丢失。配置镜像的队列,都包含一个主节点master和多个从节点slave,如果master失效,加入时间最长的slave会被提升为新的master,除发送消息外的所有动作都向master发送,然后由master将命令执行结果广播给各个slave,rabbitmq会让master均匀地分布在不同的服务器上,而同一个队列的slave也会均匀地分布在不同的服务器上,保证负载均衡和高可用性。

吞吐量TPS

  • 持久化场景下的吞吐量只有 2.6 万(8C16G)

消息重复

  • at least once、at most once

    • 不支持exactly only once

顺序消息

  • 不支持

消息确认

  • 支持

    1>发送方确认机制,消息被投递到所有匹配的队列后,返回成功。如果消息和队列是可持久化的,那么在写入磁盘后,返回成功。支持批量确认和异步确认。
    2>接收方确认机制,设置autoAck为false,需要显式确认,设置autoAck为true,自动确认。
    当autoAck为false的时候,rabbitmq队列会分成两部分,一部分是等待投递给consumer的消息,一部分是已经投递但是没收到确认的消息。如果一直没有收到确认信号,并且consumer已经断开连接,rabbitmq会安排这个消息重新进入队列,投递给原来的消费者或者下一个消费者。
    未确认的消息不会有过期时间,如果一直没有确认,并且没有断开连接,rabbitmq会一直等待,rabbitmq允许一条消息处理的时间可以很久很久。

    • confirm 确认消息、Return返回消息

      confirm确认消息。
      在Channel上开启确认模式:channel.confirmSelect()
      在channel上添加监听:addConfirmListener,监听成功和失败的结果,具体结果对消息进行重新发送或者记录日志。
      return消息机制
      Return消息机制处理一些不可路由的消息,我们的生产者通过指定一个Exchange和Routinkey,把消息送达到某一个队列中去,然后我们消费者监听队列进行消费处理!
      在某些情况下,如果我们在发送消息的时候当Exchange不存在或者指定的路由key路由找不到,这个时候如果我们需要监听这种不可到达的消息,就要使用Return Listener!
      Mandatory 设置为true则会监听器会接受到路由不可达的消息,然后处理。如果设置为false,broker将会自动删除该消息。

消息重试

  • 不支持,但是可以利用消息确认机制实现

    rabbitmq接收方确认机制,设置autoAck为false。
    当autoAck为false的时候,rabbitmq队列会分成两部分,一部分是等待投递给consumer的消息,一部分是已经投递但是没收到确认的消息。如果一直没有收到确认信号,并且consumer已经断开连接,rabbitmq会安排这个消息重新进入队列,投递给原来的消费者或者下一个消费者。

消息回溯

  • 不支持

    • 消费完消息删除

消息事务

  • 支持

    客户端将信道设置为事务模式,只有当消息被rabbitMq接收,事务才能提交成功,否则在捕获异常后进行回滚。使用事务会使得性能有所下降

管理界面

  • 支持较好

集群方式

  • 集群模式

    • 主备模式

      实现rabbitMQ高可用集群,一般在并发量和数据不大的情况下,这种模式好用简单。又称warren模式。(区别于主从模式,主从模式主节点提供写操作,从节点提供读操作,主备模式从节点不提供任何读写操作,只做备份)如果主节点宕机备份从节点会自动切换成主节点,提供服务。

    • 集群模式

      经典方式就是Mirror模式

      镜像队列,是rabbitMQ数据高可用的解决方案,主要是实现数据同步,一般来说是由2-3节点实现数据同步,(对于100%消息可靠性解决方案一般是3个节点)

      多活模式:这种模式也是实现异地数据复制的主流模式,因为shovel模式配置相对复杂,所以一般来说实现异地集群都是使用这种双活,多活的模式,这种模式需要依赖rabbitMQ的federation插件,可以实现持续可靠的AMQP数据。
      rabbitMQ部署架构采用双中心模式(多中心)在两套(或多套)数据中心个部署一套rabbitMQ集群,各中心的rabbitMQ服务需要为提供正常的消息业务外,中心之间还需要实现部分队列消息共享。

      federation插件是一个不需要构建Cluster,而在Brokers之间传输消息的高性能插件,federation可以在brokers或者cluster之间传输消息,连接的双方可以使用不同的users或者virtual host双方也可以使用不同版本的erlang或者rabbitMQ版本。federation插件可以使用AMQP协议作为通讯协议,可以接受不连续的传输。

并发度

  • 极高

    本身是用Erlang语言写的,并发性能高。
    可在消费者中开启多线程,最常用的做法是一个channel对应一个消费者,每一个线程把持一个channel,多个线程复用connection的tcp连接,减少性能开销。
    当rabbitmq队列拥有多个消费者的时候,队列收到的消息将以轮询的分发方式发送给消费者。每条消息只会发送给订阅列表里的一个消费者,不会重复。
    这种方式非常适合扩展,而且是专门为并发程序设计的。
    如果某些消费者的任务比较繁重,那么可以设置basicQos限制信道上消费者能保持的最大未确认消息的数量,在达到上限时,rabbitmq不再向这个消费者发送任何消息。

特殊机制

  • 队列

    1、消费端ack与重回队列

    消费端进行消费的时候,如果由于业务异常我们可以进行日志的记录,然后进行补偿!(也可以加上最大努力次数的尝试)
    如果由于服务器宕机等严重问题,那我们就需要手动进行ack保证消费端的消费成功!
    消息重回队列
    重回队列就是为了对没有处理成功的消息,把消息重新投递给broker!实际应用中一般都不开启重回队列。

    2、TTL队列/消息

    TTL time to live 生存时间。

    支持消息的过期时间,在消息发送时可以指定。
    支持队列过期时间,在消息入队列开始计算时间,只要超过了队列的超时时间配置,那么消息就会自动的清除。

    3、死信队列

    死信队列:DLX,Dead-Letter-Exchange

    利用DLX,当消息在一个队列中变成死信(dead message,就是没有任何消费者消费)之后,他能被重新publish到另一个Exchange,这个Exchange就是DLX。
    消息变为死信的几种情况:
    1、消息被拒绝(basic.reject/basic.nack)同时requeue=false(不重回队列)
    2、TTL过期
    3、队列达到最大长度
    DLX也是一个正常的Exchange,和一般的Exchange没有任何的区别,他能在任何的队列上被指定,实际上就是设置某个队列的属性。
    当这个队列出现死信的时候,RabbitMQ就会自动将这条消息重新发布到Exchange上去,进而被路由到另一个队列。可以监听这个队列中的消息作相应的处理,这个特性可以弥补rabbitMQ以前支持的immediate参数的功能。
    死信队列的设置
    设置Exchange和Queue,然后进行绑定

    Exchange: dlx.exchange(自定义的名字)
    queue: dlx.queue(自定义的名字)
    routingkey: #(#表示任何routingkey出现死信都会被路由过来)
    然后正常的声明交换机、队列、绑定,只是我们在队列上加上一个参数:
    arguments.put(“x-dead-letter-exchange”,“dlx.exchange”);

常见问题

问题

  1. 如何保证消息不丢失
  2. 如何处理重复消息
  3. 如何保证消息的有序性
  4. 如何处理消息堆积
1、如何保证消息不丢失?

保证消息的可靠性需要三方配合,一,消息队列高可用,这是基础,二,消息安全送达,三,消息可靠消费

保证消息的成功发出
保证MQ节点节点的成功接收
发送端MQ节点(broker)收到消息确认应答
完善消息进行补偿机制

但是要注意消息可靠性增强了,性能就下降了,等待消息刷盘、多副本同步后返回都会影响性能。因此还是看业务,例如日志的传输可能丢那么一两条关系不大,因此没必要等消息刷盘再响应。

  • 生产消息

    生产者发送消息至Broker,需要处理Broker的响应,不论是同步还是异步发送消息,同步和异步回调都需要做好try-catch,妥善的处理响应,如果Broker返回写入失败等错误消息,需要重试发送。当多次发送失败需要作报警,日志记录等。
    这样就能保证在生产消息阶段消息不会丢失。

  • 存储消费

    存储消息阶段需要在消息刷盘之后再给生产者响应,假设消息写入缓存中就返回响应,那么机器突然断电这消息就没了,而生产者以为已经发送成功了。
    如果Broker是集群部署,有多副本机制,即消息不仅仅要写入当前Broker,还需要写入副本机中。那配置成至少写入两台机子后再给生产者响应。这样基本上就能保证存储的可靠了。

    需要控制响应的时机,单机情况下是消息刷盘后返回响应,集群多副本情况下,即发送至两个副本及以上的情况下再返回响应。

    • 发送消息设置消息落地
    • 服务高可用
  • 消费消息

    这里经常会有同学犯错,有些同学当消费者拿到消息之后直接存入内存队列中就直接返回给Broker消费成功,这是不对的。
    你需要考虑拿到消息放在内存之后消费者就宕机了怎么办。所以我们应该在消费者真正执行完业务逻辑之后,再发送给Broker消费成功,这才是真正的消费了。所以只要我们在消息业务逻辑处理完成之后再给Broker响应,那么消费阶段消息就不会丢失。

2、如何处理重复消息?
  • 消息重复是不可避免的

    假设我们发送消息,就管发,不管Broker的响应,那么我们发往Broker是不会重复的。
    但是一般情况我们是不允许这样的,这样消息就完全不可靠了,我们的基本需求是消息至少得发到Broker上,那就得等Broker的响应,那么就可能存在Broker已经写入了,当时响应由于网络原因生产者没有收到,然后生产者又重发了一次,此时消息就重复了。
    再看消费者消费的时候,假设我们消费者拿到消息消费了,业务逻辑已经走完了,事务提交了,此时需要更新Consumer offset了,然后这个消费者挂了,另一个消费者顶上,此时Consumer offset还没更新,于是又拿到刚才那条消息,业务又被执行了一遍。于是消息又重复了。
    可以看到正常业务而言消息重复是不可避免的,因此我们只能从另一个角度来解决重复消息的问题。

    • 幂等处理重复消息

      幂等是数学上的概念,我们就理解为同样的参数多次调用同一个接口和调用一次产生的结果是一致的。

      三个套路,真正应用到实际中还是得看具体业务细节

      • 前置条件判断
      • 数据库的约束例如唯一键
      • 记录关键的key,比如处理订单这种
3、如何保证消息的有序性?
  • 全局有序

    如果要保证消息的全局有序,首先只能由一个生产者往Topic发送消息,并且一个Topic内部只能有一个队列(分区)。消费者也必须是单线程消费这个队列。这样的消息就是全局有序的!不过一般情况下我们都不需要全局有序。

  • 部分有序

    因此绝大部分的有序需求是部分有序,部分有序我们就可以将Topic内部划分成我们需要的队列数,把消息通过特定的策略发往固定的队列中,然后每个队列对应一个单线程处理的消费者。这样即完成了部分有序的需求,又可以通过队列数量的并发来提高消息处理效率。

4、如何处理消息堆积?

消息的堆积往往是因为生产者的生产速度与消费者的消费速度不匹配。有可能是因为消息消费失败反复重试造成的,也有可能就是消费者消费能力弱,渐渐地消息就积压了。

  • 先定位消费慢的原因

    如果是因为本身消费能力较弱,我们可以优化下消费逻辑,比如之前是一条一条消息消费处理的,这次我们批量处理,比如数据库的插入,一条一条插和批量插效率是不一样的。

    假如逻辑我们已经都优化了,但还是慢,那就得考虑水平扩容了,增加Topic的队列数和消费者数量,注意队列数一定要增加,不然新增加的消费者是没东西消费的。一个Topic中,一个队列只会分配给一个消费者。
    当然你消费者内部是单线程还是多线程消费那看具体场景。不过要注意上面提高的消息丢失的问题,如果你是将接受到的消息写入内存队列之后,然后就返回响应给Broker,然后多线程向内存队列消费消息,假设此时消费者宕机了,内存队列里面还未消费的消息也就丢了。

应用场景

队列作用

  • 异步处理

    • 调用链路长、响应就慢了

      用户注册,欢迎短信,短信没必要这么的 “及时”

  • 服务解耦

    • 新流程接入

      对接很多服务,任何一个下游系统接口的变更可能都会影响到核心服务,但不是主流程,需接触耦合,(短信,邮件欢迎通知)不影响用户注册核心流程

  • 流量控制

    • 削峰填谷

      请求先放入消息队列中,后端服务尽自己最大能力去消息队列中消费请求。电子签约用印流程中有应用

场景

  • 消息重回队列,不建议使用,此消息消费端已处理完成,但是业务逻辑处理不成功,再次消费一般也不会成功,区别与消息重试。

  • 延时队列(死信队列+TTL),订单支付超时取消。

  • 事务消息,mq事务功能和本地消息表实现方式(以后分布式事物一起分享)
    mq事务的实现主要是对信道(Channel)的设置,主要的方法有三个:

    1. channel.txSelect()声明启动事务模式;
    2. channel.txComment()提交事务;
    3. channel.txRollback()回滚事务;
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername(config.UserName);
factory.setPassword(config.Password);
factory.setVirtualHost(config.VHost);
factory.setHost(config.Host);
factory.setPort(config.Port);	
Connection conn = factory.newConnection();
// 创建信道
Channel channel = conn.createChannel();
// 声明队列
channel.queueDeclare(_queueName, true, false, false, null);
String message = String.format("时间 => %s", new Date().getTime());
try {
	channel.txSelect(); // 声明事务
	// 发送消息
	channel.basicPublish("", _queueName, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes("UTF-8"));
	channel.txCommit(); // 提交事务
} catch (Exception e) {
	channel.txRollback();
} finally {
	channel.close();
	conn.close();
}

事务消息和正常消息有2个数量级的性能差异,使用发送者确认模式实现同样可以确保消息送达,不建议使用此种方式。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值