ActiveMQ | RabbitMQ | RocketMQ | Kafka | |
---|---|---|---|---|
单机吞吐量 | 比 RabbitMQ低 | 2.6w/s( 消息做持 久化) | 11.6w/s | 17.3w/s |
开发语言 | Java | Erlang | Java | Scala/Java |
主要维护者 | Apache | Mozilla/Spring | Alibaba | Apache |
成熟度 | 成熟 | 成熟 | 开源版本不够成熟 | 比较成熟 |
订阅形式 | 点对点 (p2p)、广 播(发布订阅) | 提供了4 种: direct、topic、headers、fanout。fanout就是广播模式 | 基于topic/messageTag以及按照消息类型、属性进行正则匹配的发布订阅模式 | 基于topic以及按照topic进行正则匹配的发布订阅模式 |
持久化 | 支持少量堆积 | 支持少量堆积 | 支持大量堆积 | 支持大量堆积 |
顺序消息 | 不支持 | 不支持 | 支持 | 支持 |
性能稳定性 | 好 | 好 | 一般 | 较差 |
集群方式 | 支持简单集群模 式,比如’主备’,对高级集群模式支持不好。 | 支持简单集群,'复 制’模式,对高级集群模式支持不 好。 | 常用多对’MasterSlave’模 式,开源版本需手动切换Slave变成Master | 天然的‘LeaderSlave’无状态集群,每台服务器既是Master也是Slave |
管理界面 | 一般 | 较好 | 一般 | 无 |
RabbitMQ:延时低,微妙级延时,社区活跃度高,bug修复及时,而且提供了很友善的后台界面;用Erlang 语言开发,只熟悉 Java 的无法阅读源码和自行修复 bug。
RocketMQ:阿里维护的消息中间件,可以达到十万级的吞吐量,支持分布式事务。
Kafka:分布式的中间件,最大优点是其吞吐量高,一般运用于大数据系统的实时运算和日志采集的场景,功能简单,可靠性高,扩展性高;缺点是可能导致重复消费。
中小型公司,技术实力较为一般,技术挑战不是特别高,用RabbitMQ是不错的选择;大型公司,基础架构研发实力较强,用RocketMQ是很好的选择。如果是大数据领域的实时计算、日志采集等场景,用Kafka是业内标准的,社区活跃度很高。
RabbitMQ是一个由Erlang语言开发的AMQP的开源实现。
AMQP :Advanced Message Queue,高级消息队列协议。它是应用层协议的一个开放标准,为面向消息的中间件设计,基于此协议的客户端与消息中间件可传递消息,并不受产品、开发语言等条件的限制。
由Exchange、Queue、RoutingKey三个才能决定一个从Exchange到Queue的 唯一的线路。
RabbitMQ官网提供了七种队列模型,分别是:简单队列、工作队列、发布订阅、路由模式、主题模式、RPC模式、发布者确认模式。
RabbitMQ整体上是一个生产者与消费者模型,主要负责接收、存储和转发消息。
RabbitMQ的整体模型架构:
Producer:生产消息的一方。Consumer:消费消息的一方。
消息一般由2部分组成:消息头(或者说是标签Label)和消息体。消息体也可以称为payLoad。消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。生产者把消息交由RabbitMQ后,RabbitMQ会根据消息头把消息发送给感兴趣的Consumer。
在RabbitMQ中,消息并不是直接被投递到Queue(消息队列)中的,中间还必须经过Exchange这一层,Exchange会把消息分配到对应的Queue(消息队列) 中。
Exchange用来接收生产者发送的消息并将这些消息路由给服务器中的队列中,如果路由不到,或许会返回给 Producer(生产者) ,或许会被直接丢弃掉 。这里可以将RabbitMQ中的交换器看作一个简单的实体。
RabbitMQ的Exchange有4种类型,不同的类型对应着不同的路由策略:direct(默认)、fanout、topic和headers,不同类型的Exchange转发消息的策略有所区别。
生产者将消息发给交换器的时候,一般会指定一个RoutingKey(路由键),用来指定这个消息的路由规则,而这个RoutingKey需要与交换器类型和绑定键(BindingKey)联合使用才能最终生效。
RabbitMQ中通过Binding(绑定)将Exchange与Queue(消息队列)关联起来,在绑定的时候一般会指定一个BindingKey(绑定建)。这样RabbitMQ就知道如何正确将消息路由到队列了。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。Exchange和Queue的绑定可以是多对多的关系。
Binding(绑定) 示意图:
生产者将消息发送给交换器时,需要一个RoutingKey,当BindingKey和RoutingKey相匹配时,消息会被路由到对应的队列中。在绑定多个队列到同一个交换器的时候,这些绑定允许使用相同的BindingKey。BindingKey并不是在所有的情况下都生效,它依赖于交换器类型,比如fanout类型的交换器就会无视,而是将消息路由到所有绑定到该交换器的队列中。
Queue用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。
RabbitMQ中消息只能存储在队列中,这一点和Kafka这种消息中间件相反。Kafka将消息存储在topic(主题) 这个逻辑层面,而相对应的队列逻辑只是topic实际存储文件中的位移标识。 RabbitMQ的生产者生产消息并最终投递到队列中,消费者可以从队列中获取消息并消费。
多个消费者可以订阅同一个队列,这时队列中的消息会被平均分摊(Round-Robin,即轮询)给多个消费者进行处理,而不是每个消费者都收到所有的消息并处理,这样避免的消息被重复消费。
RabbitMQ 不支持队列层面的广播消费,如果有广播消费的需求,需要在其上进行二次开发,这样会很麻烦,不建议这样做。
对于RabbitMQ来说,一个RabbitMQ Broker可以简单地看作一个RabbitMQ服务节点,或者RabbitMQ服务实例。大多数情况下也可以将一个RabbitMQ Broker看作一台RabbitMQ服务器。
生产者将消息存入RabbitMQ Broker,以及消费者从Broker中消费数据的整个流程。
RabbitMQ常用的Exchange Type有fanout、direct、topic、headers这四种。
RoutingKey 为一个点号“.”分隔的字符串(被点号“.”分隔开的每一段独立的字符串称为一个单词),如 “com.rabbitmq.client”、“java.util.concurrent”、“com.hidden.client”;
BindingKey 和 RoutingKey 一样也是点号“.”分隔的字符串;
BindingKey 中可以存在两种特殊字符串“”和“#”,用于做模糊匹配,其中“”用于匹配一个单词,“#”用于匹配多个单词(可以是零个)。
路由键为 “com.rabbitmq.client” 的消息会同时路由到 Queuel 和 Queue2;
路由键为 “com.hidden.client” 的消息只会路由到 Queue2 中;
路由键为 “com.hidden.demo” 的消息只会路由到 Queue2 中;
路由键为 “java.rabbitmq.demo” 的消息只会路由到Queuel中;
路由键为 “java.util.concurrent” 的消息将会被丢弃或者返回给生产者(需要设置 mandatory 参数),因为它没有匹配任何路由键。
可以认为是无限制,因为限制取决于机器器的内存,但是消息过多会导致处理效率的下降。
RabbitMQ使用发送方确认模式,确保消息正确地发送到RabbitMQ。
发送方确认模式:将信道设置成confirm模式(发送方确认模式),则所有在信道上发布的消息都会被指派一个唯一的ID。一旦消息被投递到目的队列后,或者消息被写入磁盘后(可持久化的消息),信道会发送一个确认给生产者(包含消息唯一ID)。如果RabbitMQ发生内部错误从而导致消息丢失,会发送一条nack(not acknowledged,未确认)消息。
发送确认模式是异步的,生产者应用程序在等待确认的同时,可以继续发送消息。当确认消息到达生产者应用程序,生产者应用程序的回调方法就会被触发来处理确认消息。
接收方消息确认机制:消费者接收每一条消息后都必须进行确认(消息接收和消息确认是两个不同操作)。只有消费者确认了消息,RabbitMQ才能安全地把消息从队列中删除。
这里并没有用到超时机制,RabbitMQ仅通过Consumer的连接中断来确认是否需要重新发送消息。也就是说,只要连接不中断,RabbitMQ给了Consumer足够长的时间来处理理消息。
特殊情况:
- 如果消费者接收到消息,在确认之前断开了连接或取消订阅,RabbitMQ会认为消息没有被分发,然后重新分发给下个订阅的消费者。(可能存在消息重复消费的隐患,需要根据bizId去重)
- 如果消费者接收到消息却没有确认消息,连接也未断开,则RabbitMQ认为该消费者繁忙,将不会给该消费者分发更多的消息。
在消息生产时,MQ内部针对每条生产者发送的消息生成一个inner-msg-id,作为去重和幂等的依据(消息投递失败并重传),避免重复的消息进入队列;
在消息消费时,要求消息体中必须要有一个bizId(对于同一业务全局唯一,如支付ID、订单ID、帖子ID等)作为去重和幂等的依据,避免同一条消息被重复消费。
由于TCP连接的创建和销毁开销较大,且并发数受系统资源限制,会造成性能瓶颈。RabbitMQ使用信道的方式来传输数据。信道是建立在真实的TCP连接内的虚拟连接,且每条TCP连接上的信道数量量没有限制。
- RabbitMQ采用类似NIO(Non-blocking I/O)做法,选择TCP连接复用,不仅可以减少性能开销,同时也便于管理。
- 每个线程把持一个信道,所以信道复用了Connection的TCP连接。同时RabbitMQ可以确保每个线程的私密性,就像拥有独立的连接一样。
从概念上来说,消息路路由必须有三部分:交换器、路由、绑定。生产者把消息发布到交换器器上;绑定决定了消息如何从交换器路由到特定的队列;消息最终到达队列,并被消费者接收。
1、消息发布到交换器时,消息将拥有一个路由键(routing key),在消息创建时设定。
2、通过队列路由键,可以把队列绑定到交换器器上。
3、消息到达交换器后,RabbitMQ会将消息的路路由键与队列列的路路由键进行匹配(针对不同的交换器有不同的路由规则)。
4、如果能够匹配到队列列,则消息会投递到相应队列列中;如果不能匹配到任何队列列,消息将进入 “黑洞”。
消息提供方 >>> 路由 >>> 一至多个队列。
消息发布到交换器时,消息将拥有一个路由键(routing key),在消息创建时设定。
通过队列路由键,可以把队列绑定到交换器上。
消息到达交换器后,RabbitMQ 会将消息的路由键与队列的路由键进行匹配(针对不同的交换器有不同的路由规则);
常用的交换器主要分为一下三种:
fanout:如果交换器收到消息,将会广播到所有绑定的队列上
direct:如果路由键完全匹配,消息就被投递到相应的队列
topic:可以使来自不同源头的消息能够到达同一个队列。 使用topic交换器时,可以使用通配符。
消息持久化的前提是:将交换器/队列的durable属性设置为true,表示交换器/队列是持久交换器/队列,在服务器崩溃或重启之后不需要重新创建交换器/队列(交换器/队列会自动创建)。
如果消息想要从Rabbit崩溃中恢复,那么消息必须:
1、在消息发布前,通过把它的 “投递模式” 选项设置为2(持久)来把消息标记成持久化。
2、将消息发送到持久交换器。
3、消息到达持久队列。
RabbitMQ确保持久性消息能从服务器重启中恢复的方式是,将它们写入磁盘上的一个持久化日志文件,当发布一条持久性消息到持久交换器上时,Rabbit会在消息提交到日志文件后才发送响应(如果消息路由到了非持久队列,它会自动从持久化日志中移除)。一旦消费者从持久队列中消费了一条持久化消息,RabbitMQ会在持久化日志中把这条消息标记为等待垃圾收集。如果持久化消息在被消费之前RabbitMQ重启,那么Rabbit会自动重建交换器和队列(以及绑定),并重播持久化日志文件中的消息到合适的队列或者交换器上。
顺序会错乱的场景:RabbitMQ:一个queue,多个consumer。
解决:
常用的交换器:
1、direct:如果路由键完全匹配,消息就被投递到相应的队列。
2、fanout:如果交换器收到消息,将会播到所有绑定的队列上。
3、topic:可以使来自不同源头的消息能够到达同一个队列。 使用topic交换器时,可以使用通配符,比如:“*” 匹配特定位置的任意文本, “.” 把路由键分为了几个部分,“#” 匹配所有规则等。特别注意:发往topic交换器的消息不能随意的设置选择键(routing_key),必须是由"."隔开的一系列的标识符组成。
Broker:简单来说就是消息队列列服务器器实体。
Exchange:消息交换机,它指定消息按什什么规则,路路由到哪个队列。
Queue:消息队列列载体,每个消息都会被投入到一个或多个队列列。
Binding:绑定,它的作用就是把exchange和queue按照路路由规则绑定起来。
Routing Key:路路由关键字,exchange根据这个关键字进行消息投递。
vhost:虚拟主机,一个broker里可以开设多个vhost,用作不同用户的权限分离。
producer:消息生产者,就是投递消息的程序。
consumer:消息消费者,就是接受消息的程序。
channel:消息通道,在客户端的每个连接里,可建立多个channel,每个channel代表一个会话任务。
数据的丢失问题,可能出现在生产者、MQ、消费者中。
消息积压处理办法:临时紧急扩容。
先修复consumer的问题,确保其恢复消费速度,然后将现有cnosumer都停掉。新建一个topic,partition是原来的 10 倍,临时建立好原先10倍的queue数量。然后写一个临时的分发数据的consumer程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的10倍数量的queue。
接着临时征用10倍的机器来部署consumer,每一批consumer消费一个临时queue的数据。这种做法相当于是临时将queue资源和consumer资源扩大10倍,以正常的10倍速度来消费数据。等快速消费完积压数据之后,得恢复原先部署的架构,重新用原先的consumer 机器来消费消息。
MQ中消息失效:假设你用的是RabbitMQ,RabbtiMQ是可以设置过期时间的,也就是TTL。如果消息在queue中积压超过一定的时间就会被RabbitMQ给清理掉,这个数据就没了。那这就是第二个坑了。这就不是说数据会大量积压在mq里,而是大量的数据会直接搞丢。我们可以采取一个方案,就是批量重导,数据大量积压的时候,我们当时就直接丢弃数据了,然后等过了高峰期以后,就开始写程序,将丢失的那批数据,写个临时程序,一点一点的查出来,然后重新灌入mq里面去,把丢的数据给他补回来。比如1 万个订单积压在mq里面,没有处理,其中1000个订单都丢了,你只能手动写程序把那1000个订单给查出来,手动发到mq里去再补一次。