为什么需要消息队列
1、削峰
高并发情况下,服务器通过集群模式(即多机部署),可以抗住几万的并发,但数据库能承受的并发量是有限的,若服务器将所有请求直接打向数据库,会直接把数据库打垮。如图所示:
2、异步
对于实时性不是很高的业务,例如给用户发送短信、邮件通知,以及下单后的创建订单、削减库存等操作都可以放到消息队列里去。因为相对于核心订单流程来说,短信、邮件晚一些发送,对用户来说影响不是很大。同时还可以提升整个链路的响应时间。
1、解耦
多个服务通过消息队列关联在一起,相互之间不会产生耦合,一个服务挂了不影响其他服务。后续的可维护性、扩展性都大大提升。如:订单系统在创建了订单之后需要通知其他的所有系统,若通过rpc调用就把订单系统和其余的系统强耦合在了一起。若采用消息队列,就可以将所有服务关联在一起,订单服务只用往消息队列中发送消息,而其他服务则根据自己的消费能力依次消费消息即可。
主流的消息队列选型
1、Kafka
Kafka是LinkedIn开源的分布式发布-订阅消息系统,属于apache的开源项目,基于pull模式来消费消息。
优点:拥有很高的吞吐量,单机能够抗下十几w的并发,而且写入的性能也很高,能够达到毫秒级别。
缺点:高并发情况下可能会出现消息的丢失。
应用场景:用于大量数据的日志消息的收集,允许丢失一两条消息。
2、RabbitMQ
RabbitMQ是使用Erlang语言开发的开源消息队列系统,基于AMQP协议来实现。
优点:保证消息不丢失,可靠性高,且写入延迟可达到微秒级;
缺点:吞吐量只能达到几万;
应用场景:不允许消息丢失,且不会出现高并发的业务需求。
3、RocketMQ
RocketMQ是一个纯Java、分布式、队列模型的开源消息中间件,前身是MetaQ,是阿里参考Kafka特点研发的一个队列模型的消息中间件,后开源给apache基金会成为了apache的顶级开源项目,具有高性能、高可靠、高实时、分布式特点。
优点:与Kafka一样,拥有很高的吞吐量,单机能够抗下十几w的并发,比Kafka小一些,但能保证消息不丢失,可靠性高;
应用场景:广泛运用于订单、交易、计算、消息推送、binlog分发等场景。
三种MQ的吞吐性能对比:
RocketMQ架构组成
如图所示,RocketMQ的架构主要包括四个部分:
1、NameServer (名称服务器)
topic路由注册中心,为Producer和Consumer提供路由信息。
NameServer充当路由消息的提供者,支持Broker的动态注册与发现。生产者或消费者能够通过NameServer查找各Topic对应的Broker IP列表。NameServer通常以集群方式部署,各实例间相互不进行信息通讯。NameServer有以下功能:
①Broker管理:接受Broker的注册信息并保存下来作为路由信息的基本数据;
②心跳检测:检测Broker是否还存活;
③路由信息管理:每个NameServer实例保存一份关于Broker集群的整个路由信息。Producer在发送消息前会根据Topic到NameServer获取Broker的路由信息,而Consumer也会定时获取路由信息,从而对指定Topic进行消息的消费。
④无状态:当某个NameServer实例因某种原因下线,Broker会向其他实例同步路由信息,Producer和Consumer依旧可以获取Broker的路由信息。
2、Broker(代理服务器)
消息中转角色,负责存储消息、转发消息。
Broker负责接收从生产者发送来的消息并存储,同时为消费者的拉取请求作准备。Broker有以下特点:
①分Group:Broker以group分开,每个group有一个master,对应若干个slave;
②主从复制,读写分离:master负责写入数据,并同步到slave,消费时可从master或slave读取数据;
3、Producer(生产者)
负责生产消息,生产者负责把业务系统产生的消息发送到Broker服务器。
Producer由用户进行分布式部署,消息由Producer通过多种负载均衡模式发送到Broker集群,发送低延时,支持快速失败。RocketMq提供了三种发送消息的模式:
①同步发送:消息发送方发送出数据后在收到接收方的发回响应之后才发送下一个数据包。一般用于重要通知消息,例如重要通知邮件、营销短信。
②异步发送:发送方发送出数据后不等接收方发回的响应,接着发送下一个数据包。一般用于可能链路耗时较长而对响应时间敏感的业务场景,例如用户视频上传后通知启动转码服务。
③单向发送:只负责发送消息而不等待服务器回应且没有回调函数触发。一般用于某些耗时非常短但对可靠性要求并不高的场景,例如日志收集。
④顺序发送:一类消息消费要按照一定的顺序,发送时也要保证其是有序的。如:一个订单创建了三条消息,订单创建、订单付款、订单完成,必须按照这个顺序发送到同一个队列中,并从该队列中按序消费才有意义。
4、Consumer(消费者)
负责异步消费消息,消费者负责从Broker服务器拉取消息并将其提供给应用程序。
Consumer也由用户部署,支持PUSH和PULL两种消费模式,支持集群消费和广播消息,提供实时的消息订阅机制。两种消费模式如下:
Pull Consumer(主动消费型):主动从消息服务器拉取信息,只要批量拉取到消息,用户应用就会启动消费过程,所以 Pull 称为主动消费型。
Push Consumer(被动消费型):封装了消息的拉取、消费进度和其他的内部维护工作,将消息到达时执行的回调接口留给用户应用程序来实现。从实现上看还是从消息服务器中拉取消息,不同于 Pull 的是 Push 首先要注册消费监听器,当监听器处触发后才开始消费消息。
集群消费
RocketMQ默认情况下是集群消费,即同一个Topic中的每条消息只能被一个消费组中的一个消费者实例消费。消息消费成功后,需要返回结果给Broker,告知其消费成功。若消息消费失败,Broker会通过消息重试机制,重新投递消息。在RocketMQ 达到最大重试消费次数之后,就将该消息投递至死信队列。然后关注死信队列,并对该死信消息业务做人工的补偿操作。
广播消费
广播消息可以被消费组中的每一个消费者消费。RocketMQ中提供广播消费的接口函数。
消息领域模型
Message: 要传输的信息。
Topic: 消息的主题,Topic可看作消息存储在Broker上的目标地址。一条消息必须有一个Topic,一个Topic可以有多个生产者向其发送消息,也可以被多个消费者消费消息。
Consumer Group: 消费者组,订阅了相同Topic的多个Consumer实例组成一个消费者组。
Queue: 是Topic在一个Broker上的分片等分成指定份数之后其中的一份,即一个Topic会对应多个Queue,是负载均衡过程中资源分配的基本单位。
Offset: 在RocketMQ 中,所有消息队列都是持久化,长度无限的数据结构,所谓长度无限是指队列中的每个存储单元都是定长,访问其中的存储单元使用Offset 来访问,Offset 为 java long 类型,64 位,理论上在 100年内不会溢出,所以认为是长度无限。
Broker,Topic,Queue之间的关系,如下图所示:
RocketMQ工作流程
1、启动NameServer,等待Broker、Producer、Consumer连接;
2、启动Broker,跟所有NameServer实例保持长连接,每30秒发送一次心跳包,当120s没有发送心跳包时,NameServer认为该Broker不可用了。心跳包中包含当前Broker路由信息及存储的所有Topic信息,并形成映射关系。
3、创建Topic,并指定存储在哪些Broker中;
4、Producer启动,先跟NameServer集群中一个建立长连接,并从NameServer上获取当前发送的Topic在哪些Broker上,然后与Topic所在的Broker建立长连接,向Broker中的队列轮询发送消息;
5、Consumer启动,先跟NameServer集群中一个建立长连接,获取订阅的Topic在哪些Broker上,然后与Topic所在的Broker建立长连接,从Broker中的队列消费消息;
RocketMQ面试常见问题
1、RocketMQ优缺点
优点:
①单机吞吐量大,达到十万级别;
②采用分布式架构,可用性高、扩展性好;
③消息可靠性高,保证消息0丢失;
④容量大,支持10亿级别消息堆积,且不影响性能;
⑤稳定性好,高并发业务场景已经过阿里双11的多次考验;
缺点:
1、社区活跃度不高
2、消息重复消费
若出现消息重复的情况,会导致重复消费。如:网络原因闪断,ACK返回失败等故障,确认信息没有传送到消息队列,导致消息队列不知道该消息已经被消费了,再次将该消息分发给所有的消费者。假设下单场景下,用户支付成功后,发送消息给消息队列,活动系统、积分系统、库存系统等消费消息。出现消费重复时,会导致库存扣减两次、优惠券发送两次、积分增加两次的错误。如下图所示:
为了避免消息的重复消费,就需要在消费业务端保持幂等性。
幂等性: 用户使用相同操作进行重复执行,获得的结果是相同的。
保证幂等性策略:保证每条消息都有业务唯一标识的key(如:唯一流水号),对应数据表中也是唯一键,在数据表中查看数据,若已存在就直接return,否则就执行业务逻辑。
2、顺序消息
顺序消费就是要保证消费者对同个业务场景下几个不同操作的消息消费顺序是与发送时顺序一致的,从而避免出现错误。如:下单时会先发送创建订单的消息,等订单支付成功后会发送修改订单的消息,这两个操作的消费顺序也必须保持一致,否则就会出现数据错误。如下图所示:
一个Topic下有多个队列,为了保证发送有序,RocketMQ提供了MessageQueueSelector队列选择机制,其中一种是哈希取模法。
发送消息时,增加订单、修改订单对应同一个订单号,对订单号进行哈希取模,就可以发送到同一个队列中。
RocketMQ的Topic内的队列机制,可以保证存储满足FIFO(先进先出),剩下的只需要消费者顺序消费即可。
消费消息时,一个队列Queue只能被一个消费者消费,对订单号Hash取模能保证从同一个队列取消息,而队列是先进先出的,就能保证顺序消费。
为什么一个队列不能被多个消费者消费?因为RocketMQ支持的顺序消费,是在单个队列中消息是有顺序性的,如多个消费者同时消费同一个队列,就很难保证消息的顺序性。
3、死信队列
RocketMQ在集群模式下,消息失败后,Broker会通过消息重试机制,重新投递消息,在超过一定次数后(默认16次)还未成功,就会将消息放到死信队列中。那么,这种消费失败的消息成为死信消息,存储死信消息的特殊队列就被称为死信队列(Dead-Letter Queue)。死信队列有默认的Topic,可以被订阅和消费,且默认为3天过期。
应用场景:秒杀系统中,消费MQ消息,进行创建订单、削减库存操作时,若出现错误且重试后无效,会导致消息消费不成功,就会将其放到死信队列中,然后人工进行补偿操作,保证消息处理成功并存储到数据库中。
4、分布式事务
本文参考:保送阿里的RocketMQ知识点。