消息队列

262 阅读25分钟

消息队列(MQ):保存消息的一个容器,本质是队列。但是这个队列需要支持高吞吐、高并发、并且高可用
1676790379210.png

目前业界的消息队列对比如下:
1676789490434.png

中间件: 消息队列:面试必考项 什么是消息队列,什么是先进先出队列

1. 消息队列应用场景场景:

三大经典应用场景异步、削峰、解耦。

  • 异步
场景2:在高并发环境下,由于来不及同步处理,请求往往会发生堵塞,比如说,大量的insertupdate之类的请求同时到达MySQL,直接导致无数的行锁表锁,甚至最后请求会堆积过多,从而触发too many connections错误。通过使用消息队列,我们可以异步处理请求,从而缓解系统的压力。
	
  • 流量削峰: 流量高峰期的时候可能会给Redis、MySQL和服务器打挂,各自承受能力不同。把请求放到队列里面固定消费速度。

  • 应用解耦
    场景:电商系统, 线程池去做问题:耦合(一个流程涉及多个业务eg扣积分扣优惠卷扣库存发短信,要调用多个接口,每加一次业务要调用一个接口且重新发布系统),问题排查(流程之间多个业务相互影响)。 消息队列做:只用负责自己部分的业务,做完后给支付成功的消息告诉别的系统,别的系统订阅监听就可以了。 存在问题: + 数据不一致性,这个业务成功了,下一个失败了。 solve:分布式事务,给这个流程涉及的业务放在一个事务里面,要成功一起成功,要失败一起失败 + 可用性:MQ挂了 怎么保证高可用

  1. 系统崩溃 如果存储服务出现了问题,整个操作都会卡住。 所有的消息先发送到消息队列当中,由存储服务拉出。即使存储服务故障,消息也会顺利发送到消息队列里面,不会影响整个流程。
    1676788351113.png--解耦--> 1676788807299.png

  2. 服务处理能力有限 请求量过于庞大,而服务器承受能力有限最多只能同时处理10个请求时,怎么去承受庞大的请求量。
    由消息队列承受庞大请求。服务器只从消息队列中拉取承受范围内的请求数处理。
    1676788369420.png-削峰-> 1676788918397.png

  3. 链路耗时长尾 整个流程对于用户来说等待时间太长,
    只要成功写入消息队列就给用户返回下单成功的消息。异步做底层的事情
    1676788386812.png--异步--> 1676789072350.png

  4. 日志如何处理
    服务器坏掉导致本地日志(程序运行当中记录的信息)丢掉。 给日志先存到消息队列里面。 1676789168958.png

2. 消息队列-Kafka

分布式消息队列

吞吐量、可靠性、时效性 在这里插入图片描述

组件

  • Producer :消息生产者,就是向kafka broker发消息的客户端;
  • Consumer :消息消费者,从kafka broker取消息的客户端;
  • Broker :一台kafka服务器就是一个broker。一个集群由多个broker组成。一个broker可以容纳多个topic;
  • Topic :可以理解为一个队列(就是同一个业务的数据放在一个topic下),推送到不同的消费者组;
  • Partition:为了实现扩展性,一个非常大的topic可以分布到多个broker(即服务器)上,一个topic可以分为多个partition,每个partition是一个有序的队列。partition中的每条消息都会被分配一个有序的id(offset)。kafka只保证按一个partition中的顺序将消息发给consumer,不保证一个topic的整体(多个partition间)的顺序;
  • Consumer Group (CG):这是kafka用来实现一个topic消息的广播(发给所有的consumer)和单播(发给任意一个consumer)的手段。一个topic可以有多个CG。topic的消息会复制(不是真的复制,是概念上的)到所有的CG,但每个partition只会把消息发给该CG中的一个consumer。如果需要实现广播,只要每个consumer有一个独立的CG就可以了。要实现单播只要所有的consumer在同一个CG。用CG还可以将consumer进行自由的分组而不需要多次发送消息到不同的topic;
  • Offset:偏移量。

使用场景: 程序状态采集,QPS,查询写入耗时,判断程序是否健康

生产上使用: 基本概念: Topic:每一个不同的业务场景就是一个topic clus:来处理不同的业务数据 Producer:由Consumer消费处理

消费者组:消费精度独立, partition:是Topic的分区的概念,多个分区,不同的分区消息可以并发处理,来提高Topic大的吞吐能力,

offset: 每个partition内部都会存储不同的消息。对于每条消息都会有一个唯一的offset, 保证消息在内部的一个顺序性

副本: 每个分区有多个partition,每个partition有不同的副本 多个副本分布在集群中的不同机器上,容灾作业,副本有不同的角色 Leader对外写入读取(生产消费), Follower不断的从leader拉取数据保证跟Leader一样的状态 In-Sync Replicas是Kafka的一个特性。如果Follower和leader之间的差距大于了某个值,就会被踢出去In-Sync Replicas,也就是再同步中的副本 差距通过时间判断,只允许有10s的差距 作用:如果Leader所在副本机器宕机,再ISR中选择一个副本重新成为Leader继续为生产者和消费者服务 高可用保证

数据复制整个副本分布图: 4个Broker代表了Kafka集群当中的节点,所有的节点组成了这样的一个集群 每个分区三副本状态 有一个Broker扮演了Controller的角色,负责对集群中的所有副本和Broker进行分配,整个状态和分区位置的分配,都是由Controller计算出来,

架构图: 跟Controller配合

一条消息的自述: Kalfka优点: Producer: 批量发送:降低IO次数,提高吞吐 数据压缩:推荐ZSTD压缩算法,压缩率和计算效率 降低带宽和流量 Broker:

  • 顺序写:写得更快
  • 消息索引:寻找消息机制
  • 零拷贝:内核态与用户态的数据拷贝的过程当中会存在什么问题,通过什么样的操作解决 Broker如何存储压缩后的数据

副本会以日志的形式写到磁盘上,日志不可能一直不断写 消息有过期机制,需要释放空间。副本会切分为不同的日志段,有序。对于一个LogEsement存储4个文件。第一个:真正的日志文件储存真实消息数据 第二个:消息偏移量索引文件,offset和数据在真实文件当中具体位置映射,可以通过offset和该文件找到数据具有位置 第三个:通过时间戳索引 整个文件命名 segment当中的第一个消息offset作为具体命名

磁盘结构: 怎么找到位置写入 时间最长的消耗在寻道也就是磁头的移动上,需要避免

顺序写:只会末尾添加 如何读:

Broker拿到消息后需要干嘛 索引查询: offset索引机制 文件索引采取稀疏索引来进行索引构建

时间缩影机制怎么找 在offset上一层加了二级索引,通过时间戳找到offset,然后进行offset索引机制

Broker找到数据后。发送给consumer流程中有什么优化? 传统数据拷贝:系统调用只能在内核态,从磁盘读数据,先读到内核空间,在拷贝到应用空间,在拷贝到SocketBuffer,再拷贝到网卡内存,用网卡发送给对端的消费者进程 很多数据的内存拷贝,开销很大 零拷贝技术: 不需要经过内存空间,降低了三次的应用拷贝技术 读写都用了零拷贝 总结:顺序写,消息索引,零拷贝

COnsumer端: Rebalance机制 partition和consumer的分配问题:

  • 手动分配 是什么 优点:快
    缺点:consumer3挂掉了,那他对应的partition就中断了、不能够自动容灾 consumer能力不够,再加机器consumer4,没听明白
  • 自动分配 Broker集群当中,对于不同的consumer Group来讲都会选取一台Broker当Coordinator(协调者),帮助某一个Group里面的consumer进行自动分配partition过程,rebalence。 可以解决上面两个缺点,无论是机器断掉还是增加,都会被感知到,让consumer稳定的消费状态

底层整个rebalance是如何发生的, consumer 1. 找到协调者是谁,找到负载最低的Broker发请求,Broker回复给consumer它的协调者是谁,当所有consumer收到的消息达到一致时, 2. 发送第二次请求,将consumer加入到协调中 3. 协调者收到消息,从consumer中选一台当leader,由它计算整个分配策略

为什么要在consumer中做分配:业务有自己的业务特性,希望某一些分片特定的分配到某一个consumer上,给业务提供接口实现自己分配方式。

  1. 发送第三次请求,让协调者同步整个集群的分配方案,Leader发自己的方案,其他consumer发请求,协调者收到方案和请求后,会给方案告诉每一个consumer。 完成了一整个rebalance

5.rebalance完成后, consumer在一定的间隔内给协调者发送一个心跳。如果某个consumer故障了,未在一定时间内发送心跳,协调者就会认为该consumer挂掉了,会重新在走一遍rebalance过程

kalfa缺点:
数据拷

3. 消息队列存在问题、缺点:

系统复杂性提升了。插入一个中间件后需要去考虑这个中间件使用过程中的问题:

  • 重复消费: 场景举例1:支付成功发送消息,下游多个业务订阅如优惠卷、积分、库存等。某个下游业务发生错误,要求重发支付成功消息。消息队列的重试机制。此时问题:别的服务也在监听也会收到这个消息导致库存减两次之类的情况 导致重试情况普遍,比如:网络抖动、开发人员代码bug、数据问题等 解决方法:幂等 在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。通俗了讲就是你同样的参数调用我这个接口,调用多少次结果都是一个。 怎么保证幂等:分场景考虑强校验还是弱检验 强校验:通过订单号+业务场景唯一标识去查流水表表,有这条流水则return掉,无就执行后面的逻辑。加钱接口和加流水接口放在一个事务里面。 弱校验:id+场景唯一标识作为RedisKey,放到缓存里面失效时间看场景。
  • 消息丢失:
  • 消息的顺序消费: 顺序消息分为全局顺序消息与部分顺序消息,全局顺序是指某个Topic下的所有消息都要保证顺序;部分顺序消息只要保证每一组消息被顺序消费即可。如果想要实现全局顺序消息,那么只能使用一个队列,以及单个生产者,这是会严重影响性能。我们常说的顺序消息通常是只的部分顺序消息。 场景举例:一个订单产生了三条消息分别是订单创建、订单付款、订单完成。消费时要按照这个顺序消费才能有意义,但是同时订单之间是可以并行消费的。RocketMQ可以严格的保证消息(部分)有序。我们不用管不同的订单ID的消息之间的总体消费顺序,只需要保证同样订单ID的消息能按照订单创建、订单付款、订单完成这个顺序消费就可以了。 顺序消费实际上有两个核心点,一个是生产者有序存储,另一个是消费者有序消费。 解决:一个Topic下有多个队列,为了保证发送有序,RocketMQ提供了MessageQueueSelector队列选择机制,他有三种实现:

4.RabbitMQ

使用场景:快速轻量易上手;支持多种语言适配性好;消息堆积的支持并不好、当大量消息积压的时候,会导致 RabbitMQ 的性能急剧下降。每秒钟可以处理几万到十几万条消息。 有四大核心概念:生产者、交换机、队列、消费者。

交换机: 其中交换机是 RabbitMQ 非常重要的一个部件,一方面它接收来自生产者的消息,另一方面它将消息推送到队列中。交换机必须确切知道如何处理它接收到的消息,是将这些消息推送到特定队列还是推送到多个队列,亦或者是把消息丢弃,这个得有交换机类型决定。 1.direct Exchange(直接交换机):匹配路由键,只有完全匹配消息才会被转发 2.Fanout Excange(扇出交换机):将消息发送至所有的队列 3.Topic Exchange(主题交换机):将路由按模式匹配,此时队列需要绑定要一个模式上。符号“#”匹配一个或多个词,符号“匹配不多不少一个词。因此“abc.#”能够匹配到“abc.def.ghi”,但是“abc.” 只会匹配到“abc.def”。 4.Header Exchange:在绑定Exchange和Queue的时候指定一组键值对,header为键,根据请求消息中携带的header进行路由

限流机制: 持久化机制: TTL:

RabbitMQ的介绍_rabbitmq 5.1介绍_Littewood的博客-CSDN博客 点对点模式、订阅/发布模式(通过交换机实现)。

怎么解决消息堆积

消息堆积通常是由于消费者处理消息的速度跟不上生产者产生消息的速度所导致的。长时间的消息堆积可能导致RabbitMQ运行缓慢或内存和磁盘空间被耗尽。

  1. 增加消费者,提高消费速度
  2. 优化消息处理速度:优化消费者代码使处理消息更高效
  3. 为消息设置生存时间TTL,长度限制
  4. 定期清理消息
  5. 监控和报警:RabbitMQ进程的总内存使用情况和RabbitMQ的数据存储目录的磁盘使用情况,通过RabbitNQ自带插件或者第三方插件

生产者端批处理

优点:

  • 减少单个消息发送时的网络开销
  • 提高吞吐量
  • 减少与RabbitMQ交互次数

缺点:

  • 延迟增加
  • 消费端具有批处理能力:处理能力足够、消费逻辑本来只处理一个,现在批处理复杂性增加
  • 消息大小可能太大

组件、组成部分:

image.png

  • Message:消息是不具名的,它由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。
  • Publisher:消息的生产者,也是一个向交换器发布消息的客户端应用程序,
  • Consumer:消息的消费者。表示一个从消息队列中取得消息的客户端应用程序。
  • Virtual host:虚拟主机。出于多租户和安全因素设计的,把AMQP(Advanced Message Queuing Protoco,一个消息服务代理的规范,RabbitMQ是AMQP的实现)的基本组件划分到一个虚拟的分组中,类似于网络中的namespace概念。当多个不同的用户使用同一个RabbitMQ server提供的服务时,可以划分出多个vhost,每个用户在自己的vhost创建exchange/queue等。
  • Broker:接收和分发消息的应用,RabbitMQ Server就是Message Broker。
  • Connection:publisher/consumer和broker之间的TCP连接。断开连接的操作只会在client端进行,Broker不会断开连接,除非出现网络故障或broker服务出现问题。
  • Channel:TCP内部逻辑连接,轻量级的Connection,应用程序开启多线程则TCP内部多个channel,channel之间相互隔离。

如果每一次访问RabbitMQ都建立一个Connection,在消息量大的时候建立TCP Connection的开销将是巨大的,效率也较低。Channel是在connection内部建立的逻辑连接,如果应用程序支持多线程,通常每个thread创建单独的channel进行通讯,AMQP method包含了channel id帮助客户端和message broker识别channel,所以channel之间是完全隔离的。Channel作为轻量级的Connection极大减少了操作系统建立TCP connection的开销。

  • Exchange: message到达broker的第一站,根据分发规则,匹配查询表中的routing key,分发消息到queue中去。常用的类型有:
    • direct (point-to-point):点对点模式,可以精确地将消息发送给某一队列。
    • fanout (multicast):广播模式,发送时每一个队列都能接收到该消息。
    • topic (publish-subscribe) 模糊匹配的模式(发布/订阅),可以使用#.news(#匹配0个或1个单词)或*.news(*匹配任意单词)。
  • Queue:消息最终被送到这里等待consumer取走。一个message可以被同时拷贝到多个queue中。
  • Binding:exchange和queue之间的虚拟连接,binding中可以包含routing key。Binding信息被保存到exchange中的查询表中,用于message的分发依据。 在这里插入图片描述

推拉模式

在rabbitmq中有两种消息处理消费的模式,一种是推模式/订阅模式/投递模式(也叫push模式),消费者调用channel.basicConsume方法订阅队列后,由RabbitMQ主动将消息推送给订阅队列的消费者;另一种是拉模式/检索模式(也叫pull模式),需要消费者调用channel.basicGet方法,主动从指定队列中拉取消息。

  • 推:
    1. 自动立刻不断投递 推模式接收消息是最有效的一种消息处理方式,在投递模式期间,当消息到达RabbitMQ时,RabbitMQ会自动地、不断地投递消息给匹配的消费者,而不需要消费端手动来拉取,当然投递消息的个数还是会受到channel.basicQos的限制。直到取消队列订阅。
    2. 缓冲区
      推模式将消息提前推送给消费者,消费者必须设置一个缓冲区缓存这些消息。优点是消费者总是有一堆在内存中待处理的消息,所以当真正去消费消息时效率很高。缺点就是缓冲区可能会溢出。
    3. 实时性 由于推模式是信息到达RabbitMQ后,就会立即被投递给匹配的消费者,所以实时性非常好,消费者能及时得到最新的消息。

拉模式(pull) 1:如果只想从队列中获取单条消息而不是持续订阅,则可以使用channel.basicGet方法来进行消费消息。 2:拉模式在消费者需要时才去消息中间件拉取消息,网络开销会增加消息延迟,降低系统吞吐量,实时性差。 3:不能在循环中使用拉模式,严重影响性能

推模式和拉模式都是消费端主动去和消息中间件建立连接(轮询也好,长连接也罢),然后将消息拉回消费端。因而个人认为,推拉模式的本质差异是:消费频率和消息状态的保存位置,负载均衡实现端等的不同

批处理

消息大小越小越能降低延迟,提高消息速率;而批处理将小消息合并成大消息进行处理,能够增大吞吐量,减少网络通信;

缺点:

  • 批处理让应用程序变得复杂,必须先积累消息然后才能发送,有时候需要花费较长事件来等待响应。适合非实时性场景。
  • 为了保证当前批量操作一致性,在个别失败的情况下会引发批量操作重试
  • 在数据库层面,批量操作在大事务,会导致锁的竞争,适用于可以接收一定丢失的不太重要的数据存储

另外一个需要提及的点是,RabbitMQ 中涉及到批处理的地方有两处:

Consumer 侧 Acknowledging Multiple Deliveries at Once;需要业务自己实现相应逻辑

Producer 侧使用 Publisher confirm 机制时,若 broker 设置 basic.ack 中 multiple 域为 true,Producer侧的批处理只需要理解rabbitMQ实现的批处理逻辑

消息堆积一种场景:消费速度过慢,导致消息堆积,eg没消费一条队列消息会伴随一条插入。 批处理优化,将每条插入优化为批量插入

高并发系统

由于大部分场景都是读多写少,更关注系统的查询性能,可以通过数据库的分布式改造,各类缓存原理和使用技巧

高并发写场景:秒杀系统 应对措施:

  • 缓存查询热点数据。
  • 静态化能被静态化的数据,比如图片和视频数据,命中CDN节点缓存,减少Web服务器的查询量和带宽负担,Web服务器比如Nginx可以直接访问分布式缓存节点,避免请求到达tomcat服务器。
  • 限流策略:短时间同一用户同IP同设备重复请求丢弃
  • 消息队列削峰填谷

消息队列削峰填谷:

怎么理解削峰填谷 可能会碰到的问题:

  • 消息堆积造成请求延迟处理,适合非实时性的场景,监控堆积量,超过一定量时增加队列处理机数量,提升消息处理能力。 消费者速度追不上生产者速度 系统某部分出现性能问题,来不及处理上游发的消息导致消息积压。 性能优化主要在生产者和消费者的业务逻辑,MQ本身处理能力远大于业务系统。 增加发送性能:批处理增加消息大小,增加并发 增加消费性能:增加队列数量。分配策略。 关于消费者端先将消息放到内存队列中并发消费提升消费性能,会导致问题:机器宕机,批量消息丢失。 消费者角度:采取推模式进行消费,而不是一条一条的进行消费 采取Fanout模式的交换机,利用ttl特性对消息进行时间的设置,使其进入死信队列中去,无需对消息进行确认

    原因之一:消费线程触发死锁或卡在等待某些资源

  • 重复消息: 怎么解决: rabbitmq实践怎么做 golang里面怎么做

  • 消息接收顺序

  • 消息丢失失败重试

消费端限流

并发参数:concurrency和prefetch,实现消费端MQ并发处理消息

Prefetch:(默认值250) 每个customer会在MQ预取一些消息放入内存的LinkedBlockingQueue中进行消费,这个值越高,消息传递的越快,但非顺序处理消息的风险更高。如果ack模式为none,则忽略。如有必要,将增加此值以匹配txSize或messagePerAck。从2.0开始默认为250;设置为1将还原为以前的行为。

concurrency:(默认为1) 消费者开启几个线程去消费queue。如果在Listener配置了exclusive参数,即确定此容器中的单个customer是否具有对队列的独占访问权限。如果为true,则容器的并发性必须为1。

死信队列

简述:使用两个队列,一个队列接收消息不消费,等待指定时间后消息死亡,再由该队列绑定的死信exchange再次将其路由到另一个队列提供业务消费。 死信指:当消息被reject或者nack否认接收,,在队列里超过了该消息设置的过期时间(TTL),队列超出长度限制后再接收到的消息。

当一个消息再队列里变为死信时,它会被重新publish到另一个exchange交换机上,这个exchange就为DLX。因此我们只需要在声明正常的业务队列时添加一个可选的"x-dead-letter-exchange"参数,值为死信交换机,死信就会被rabbitmq重新publish到配置的这个交换机上,我们接着监听这个交换机就可以了。 在这里插入图片描述

注意点: 当我往死信队列中发送两条不同过期时间的消息时,如果先发送的消息A的过期时间大于后发送的消息B的过期时间时,由于消息的顺序消费,消息B过期后并不会立即重新publish到死信交换机,而是会等到消息A过期后一起被消费

golang使用代码见:blog.csdn.net/Tester_mull…

ACK机制

为了保证消息从队列可靠的达到消费者,RabbitMQ 提供了消息确认机制。消费者在订阅队列时,可以指定 autoAck 参数,当 autoAck 参数等于 false 时,RabbitMQ 会等待消费者显式地回复确认信号后才从内存(或者磁盘)中移除消息(实际上是先打上删除标记,之后在删除)。当 autoAck 参数等于 true 时,RabbitMQ 会自动把发送出去的消息置为确认,然后从内存(或者磁盘)中删除,而不管消费者是否真正地消费到了这些消息。

用消息确认机制后,只要设置 autoAck 参数为 false,消费者就有足够的时间处理消息(任务),不用担心处理消息过程中消费者进程挂掉后消息丢失的问题,因为 RabbitMQ 会一直等待持有消息直到消费者显式调用 Basic.Ack 命令为止。

当autoAck 参数为 false 时,对于 RabbitMQ 服务器端而言,队列中的消息分成了两部分:一部分是等待投递给消费者的消息;一部分是已经投递给消费者,但是还没有收到消费者确认信号的消息。如果 RabbitMQ 服务器端一直没有收到消费者的确认信号,并且消费此消息的消费者已经断开连接,则服务器端会安排该消息重新进入队列,等待投递给下一个消费者(也可能还是原来的那个消费者)。

RabbitMQ 不会为未确认的消息设置过期时间,它判断此消息是否需要重新投递给消费者的唯一依据是消费该消息连接是否已经断开,这个设置的原因是 RabbitMQ允许消费者消费一条消息的时间可以很久很久。

生产者消息发送确认: confirmCallback回调接口:只确认是否正确到达 Exchange 中。 ReturnCallback:在交换器路由不到队列时触发回调 消费者消息接收确认: 消费者确认发生在监听队列的消费者处理业务失败,如:发生了异常,不符合要求的数据等,这些场景我们就需要手动处理,比如重新发送或者丢弃。

消息丢失处理机制: 通过事务方式,比较消耗性能用的不多。 confirm确认机制 代码见:studygolang.com/articles/29…

重试机制

在消费中如果发生异常了,rabbitmq会使用补偿机制,如果没消费成功,会一直重复发送,直接消费成功为止。 开启消费者重试机制,最大重试次数,重试间距:retry:enabled:

// 来源https://blog.csdn.net/weixin_43086579/article/details/84801394
// 注册监听
producer.channel.NotifyClose(producer.notifyClose)
producer.channel.NotifyPublish(producer.notifyConfirm)

// 通过time.NewTicker实现超时重发
  ticker := time.NewTicker(resendDelay)
        select {
        case confirm := <-producer.notifyConfirm:
            if confirm.Ack {
                producer.logger.Println("Push confirmed!")
                return nil
            }
        case <- ticker.C:

与kalfa核心区别

  1. 推拉模式 kafka是通过一个提交日志记录的方式来存储消息记录,采用拉模式,而RabbitMQ则采用队列的方式,属于推模式
  2. 多订阅实现 kalfa 是通过提交日志记录的方式,消息的状态在消费端维护,可以所有消费端对应同一个partition实现,无需建立多个partition rabbitmq通过队列方式实现,当需要实现多订阅时,就在一个topic下建立多个队列,队列之间的消息完全一样
  3. 负载均衡实现 当kalfa需要实现负载均衡时,需要在一个topic下建立多个topic,partition和消费端是多对1的关系,消息通过负载均衡分配到不同的partition。在消息保存进partition的时候负载,多个partiton的目的是为了负载均衡,kafka提交日志的方式不需要考虑多订阅,但需要考虑负载均衡; rabbitmq只需要由每个队列来实现负载均衡,队列和消费者是一对一的关系。在消息消费的时候负载,多个队列的目的是为了实现多订阅 事务。rabbitMQ的方式需要考虑多订阅。相比之下,rabbitMQ进行负载均衡时比kafka更容易。