文章目录
- 前言
- 一、MQ介绍
- 1. 背景
- 2. 解决思路
- 3. 解决方案
- 二、应用场景
- 三、常见的MQ产品
- 四、MQ选型总结
- 五、相关知识
- 1. AMQP
- 2. JMS
- 五、如何设计实现一个消息队列
- 1. 设计消息队列的思路
- 2. 实现队列基本功能
- 1. RPC通信协议
- 2. 高可用
- 3. 服务端承载消息堆积的能力
- 4. 存储子系统的选择
- 5. 消费关系解析
- 3. 队列高级特性设计
- 1. 可靠投递(最终一致性)
- 2. 事务
- 3. 性能相关
- 4. push/pull 推拉模型简要分析
前言
消息队列
传统应用系统出现同步耗时、系统耦合、高并发等痛点问题,支撑业务逐渐吃力。
通过引入MQ(Message Queue)消息队列机制,实现异步调用,系统解耦,流量削峰的方式,解决传统应用系统的痛点问题。
作为分布式系统中重要的组件,MQ本质是创建和维护应用程序间消息传输的通道。实现高性能,高可用,可伸缩和最终一致性架构。
一、MQ介绍
1. 背景
传统应用系统出现以下痛点问题,支撑业务逐渐吃力。
- 痛点1-同步耗时
有些复杂的业务系统,一次用户请求可能会同步调用N个系统的接口,需要等待所有的接口都返回了,才能真正的获取执行结果。
这种同步接口调用的方式总耗时比较长,非常影响用户的体验,特别是在网络不稳定的情况下,极容易出现接口超时问题。
- 痛点2-系统耦合
很多复杂的业务系统,一般都会拆分成多个子系统。以用户下单为例,请求会先通过订单系统,然后分别调用:支付系统、库存系统、积分系统 和 物流系统。
系统之间耦合性太高,如果调用的任何一个子系统出现异常,整个请求都会异常,对系统的稳定性非常不利。
- 痛点3-高并发
有时候为了吸引用户,会搞一些促销活动,比如秒杀。
如果用户操作突增,一时间所有的请求都到数据库,可能会导致数据库无法承受这么大的压力,响应变慢或者直接挂掉。
对于这种突然出现的请求峰值,无法保证系统的稳定性。
2. 解决思路
通过引入MQ(Message Queue)消息队列机制,实现异步调用,系统解耦,流量削峰的方式,解决传统应用系统的痛点问题。
- 方法1-异步调用
主业务执行结束后从属业务通过MQ,异步执行,减低业务的响应时间,提高用户体验。
2. 方法2-系统解耦
主业务完成以后,发送一条MQ,其余模块异步消费MQ消息,既可实现业务,又降低模块之间的耦合。
3. 方法3-流量削峰
高并发情况下,业务异步处理,提供高峰期业务处理能力,避免系统瘫痪。
3. 解决方案
MQ(Message Queue)是一种跨进程的通信机制,用于上下游传递消息。作为分布式系统中重要的组件,MQ本质是创建和维护应用程序间消息传输的通道,实现高性能,高可用,可伸缩和最终一致性架构。
MQ早已成为企业IT系统内部通信的核心手段。它具有低耦合、可靠投递、广播、流量控制、最终一致性等一系列功能,成为异步RPC的主要手段之一。
当今市面上有很多主流的MQ,如老牌的ActiveMQ、RabbitMQ,炙手可热的Kafka,阿里巴巴自主开发RocketMQ等。
二、应用场景
- 异步处理
不用MQ,那么我们的代码必然耦合在一起,下单成功后,依次要通过RPC远程调用这几个系统,然后同步等到他们的响应才能返回给用户是否成功的结果。假设每个系统耗时200ms,那么就得花费600ms。
2. 应用解耦
我购买车票成功后,会收到信息提醒,但是如果短信系统故障了,客户就有可能收到不短信了,这就是各个系统之间的耦合太高了,我们应该解耦。传统的做法如下:
我们在订单系统产生数据后,将订单这条数据发送给MQ,就返回成功,然后让短信、邮件等系统都订阅MQ,一旦发现MQ有消息,他们主动拉取消息,然后解析,进行业务处理。
这样一来,就算你短信系统挂了,丝毫不会影响其他系统,而且如果后来想加一个新的系统,你也不用改订单系统的代码了,你只要订阅我们的MQ提供的消息就行了。
应用解耦是消息队列 MQ 的主要特点,主要目的是减少请求响应时间和解耦。主要的使用场景就是将比较耗时而且不需要即时(同步)返回结果的操作作为消息放入消息队列。同时,由于使用了消息队列MQ,只要保证消息格式不变,消息的发送方和接收方并不需要彼此联系,也不需要受对方的影响,即解耦合。
- 流量削峰
流量削峰也是消息队列 MQ 的常用场景,一般在秒杀或团队抢购(高并发)活动中使用广泛。
以12306为例,假设平时可能买票的人不多,所以订单系统的QPS( 每秒查询率 )也不是很高,每秒也就处理1000个请求,但是一到节假日、春运期间可能抢票的人就非常多,并发量远远大于平时,这个时候,订单系统明显扛不住了。
为解决这些问题,可以设计高可用的MQ,让所有的请求都到MQ,缓存起来。这样一来高峰期的流量和数据都将积压在MQ中,流量高峰就被削弱了(削峰),然后我们的订单系统就避免了高并发的请求,它可以慢慢的从MQ中拉取自己能力范围内的消息就行处理。这样一来,高峰期积压的消息也终将被消费完,可以叫做填谷。
三、常见的MQ产品
目前业界有很多MQ产品,比较出名的有下面这些:
-
ActiveMQ
历史悠久的Apache开源项目。已经在很多产品中得到应用,实现了JMS1.1规范,可以和springjms轻松融合,实现了多种协议,支持持久化到数据库,对队列数较多的情况支持不好。 -
RabbitMQ
使用erlang语言开发,性能较好,适合于企业级的开发。但是不利于做二次开发和维护。 -
RocketMQ
阿里巴巴的MQ中间件,由java语言开发,性能非常好,能够撑住双十一的大流量,而且使用起来很简单。 -
Kafka
Kafka是Apache下的一个子项目,是一个高性能跨语言分布式Publish/Subscribe消息队列系统,相对于ActiveMQ是一个非常轻量级的消息系统,除了性能非常好之外,还是一个工作良好的分布式系统。 -
ZeroMQ
号称最快的消息队列系统,尤其针对大吞吐量的需求场景。扩展性好,开发比较灵活,采用C语言实现,实际上只是一个socket库的重新封装,如果做为消息队列使用,需要开发大量的代码。
ZeroMQ仅提供非持久性的队列,也就是说如果down机,数据将会丢失。
四、MQ选型总结
需要根据具体的应用场景和需求的多维度来决定:
性能:吞吐量、并发时效性
可靠性:可用性、集群支持、持久化、性能稳定性、安全性
可维护性:管理界面等运维能力
易用性:平台熟悉度
兼容可扩展性:易于扩展
服务支持:社区活跃度
-
ActiveMQ
作为老牌的消息队列,吞吐量比较低,也缺少大规模吞吐量场景的验证、社区活跃度也很低,数据持久化的支持一般,目前渐渐被淘汰,已经不是主流了,不太建议选择了。 -
RabbitMQ和RocketMQ
社区比较活跃,吞吐量比较高,支持AMQP,稳定性也比较好,如果你的场景是应用需要可靠性消息传递和较高的并发,那么这两者是比较好的选择。
要注意,rabbitMQ是使用Erlang语言开发的,而RocketMQ则使用Java语言开发,所以如果是需要深度研究掌握的话,要考虑团队中是否有Erlang工程师,如果不具备相关的人才储备的话,更建议选择RocketMQ。当然,如果只是小团队简单使用,则rabbitMQ是一个挺好的选择。 -
Kafka和Pulsar
如果是大数据领域的实时计算、日志采集等场景,那么这两者是比较好的选择。
Kafka经历了超大规模应用的验证,社区活跃度很高,性能也非常高,几乎是全世界这个领域的事实性的标准。
Pulsar作为新兴的分布式消息传递系统,可扩展性强、性能高、社区活跃度也很高,最重要的是支持存储和计算分离,这在云原生下是非常出色的一项能力,并且天然支持跨数据中心的容灾,目前的应用也越来越广泛,如果集群对于持久化要求高,数据级别是超大规模,对于机器成本敏感,且支持多数据中心容灾,则建议选择Pulsar。 -
RabbitMQ:当需要性能稳定、低延时、功能强大且易于管理的方案,建议使用 RabbitMQ。
-
RocketMQ:当需要低延迟和金融级别的稳定性,且吞吐量需求较大的消息队列,系统主要场景是处理在线业务,比如在交易系统中传递订单,那 RocketMQ 是最适合的方案。
-
Kafka:对消息吞吐量需求很大,且不在乎消息偶尔丢失的情况,像收集日志、监控信息或是前端埋点这类海量数据,或是应用场景大量使用了大数据、流计算相关的开源产品,那 Kafka 是最适合的消息队列。
比较项 | ActiveMQ | RabbitMQ | RocketMQ | Kafka | Pulsar |
---|---|---|---|---|---|
单机吞吐量 | 较低(万级) | 一般(万级) | 高(十万级) | 高(十万级) | 高(十万级) |
时效性 | ms级 | us级 | ms级 | ms级以内 | - |
可用性 | 高(主从架构) | 高(主从架构) | 非常高(分布式架构) | 非常高(分布式架构) | - |
持久化 | 支持(小) | 支持(小) | 支持(大) | 支持(大) | 支持(大) |
顺序消息 | 不支持 | 不支持 | 支持 | 支持 | 支持 |
性能稳定性 | 好 | 好 | 一般 | 较差 | 一般 |
集群支持 | 主备模式 | 镜像模式(复制) | 主备模式 | Leader-Slave每台既是master也是slave,集群可扩展性强 | 集群模式,broker无状态,易迁移,支持跨数据中心 |
消费模式 | P2P、Pub-Sub | direct、fanout、topic、Headers | 基于Topic和MessageTag的的Pub-Sub | 基于Topic的Pub-Sub | 基于Topic的Pub-Sub |
管理界面 | 一般 | 较好 | 一般 | 无 | 无 |
计算和存储分离 | 不支持 | 不支持 | 不支持 | 不支持 | 支持 |
AMQP支持 | 支持 | 支持 | 支持 | 不完全支持 | 不完全支持 |
开发语言 | Java | Erlang | Java | Java/Scala | Java |
维护者 | Apache | Spring | Apache(Alibaba) | Apache(Confluent) | Apache(StreamNative) |
Star数量 | 2.1K | 10.4K | 18.8K | 24.3K | 12.4K |
Contributor | 126 | 246 | 438 | 991 | 600 |
社区活跃度 | 低 | 高 | 较高 | 高 | 高 |
功能特性 | 成熟的产品,有较多的文档;各种协议支持较好 | 并发能力很强,性能很好,延时很低;管理界面丰富 | MQ功能比较完毕,扩展性佳 | 支持主要的MQ功能,在大数据领域应用广泛 | 可扩展性强、性能高、云原生下非常出色,支持存储和计算分离,跨数据中心的容灾 |
五、相关知识
1. AMQP
**高级消息队列协议(Advanced Message Queuing Protocol)**是一个网络协议。它支持符合要求的客户端应用(application)和消息中间件代理(messaging middleware broker)之间进行通信。主要特征是面向消息、队列、路由(包括点对点和发布/订阅)、可靠性、安全。AMQP是协议,类比HTTP。
2. JMS
Java消息服务(JavaMessage Service)应用程序接口,是一个Java平台中关于面向消息中间件(MOM)的 API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。JMS是API接口规范,类比JDBC。
五、如何设计实现一个消息队列
1. 设计消息队列的思路
- 一次RPC变成两次RPC
基于消息的系统模型,不一定需要broker(消息队列服务端)。
而之所以要设计一个消息队列,并且配备broker,无外乎要做两件事情:
-
消息的转储,在更合适的时间点投递,或者通过一系列手段辅助消息最终能送达消费机。
-
规范一种范式和通用的模式,以满足解耦、最终一致性、错峰等需求。
掰开了揉碎了看,最简单的消息队列可以做成一个消息转发器,把一次RPC做成两次RPC。发送者把消息投递到服务端(简称broker),服务端再将消息转发一手到接收端,就是这么简单。
- 整体设计思路如下
两次RPC加一次转储,外加消费确认的第三次RPC
(1)build一个整体的数据流,例如producer发送给broker,broker发送给consumer,consumer回复消费确认,broker删除/备份消息等。
(2)利用RPC将数据流串起来。考虑RPC的高可用性,尽量做到无状态,方便水平扩展。
(3)考虑如何承载消息堆积,在合适的时机投递消息。而处理堆积的最佳方式,就是存储,存储的选型需要综合考虑性能/可靠性和开发维护成本等诸多因素。
(4)而为了实现广播等功能,必须要维护消费关系,可以利用zk/config server等保存消费关系。
- 在完成了上述几个功能后,消息队列基本就实现了。然后我们可以考虑一些高级特性,如可靠投递,事务特性,性能优化等。
2. 实现队列基本功能
下面以设计消息队列时重点考虑的模块为主线,穿插灌输一些消息队列的特性实现方法,来具体分析设计实现一个消息队列时的方方面面。
1. RPC通信协议
所谓消息队列,无外乎两次RPC加一次转储,当然需要消费端最终做消费确认的情况是三次RPC。既然是RPC,需要考虑,负载均衡、服务发现、通信协议、序列化协议等等。
因为消息队列的RPC,和普通的RPC没有本质区别。利用公司现有的RPC框架:Thrift也好,Dubbo也好,或者是其他自定义的框架也好。
简单来讲,服务端提供两个RPC服务,一个用来接收消息,一个用来确认消息收到。并且做到不管哪个server收到消息和确认消息,结果一致即可。
2. 高可用
所有的高可用,是依赖于RPC和存储的高可用来做的。
而消息队列的高可用,只要保证broker接受消息和确认消息的接口是幂等的,并且consumer的几台机器处理消息是幂等的,这样就把消息队列的可用性,转交给RPC框架来处理了。
保证幂等最简单的方式莫过于共享存储。broker多机器共享一个DB或者一个分布式文件/kv系统,则处理消息自然是幂等的。另外failover可以依赖定时任务的补偿,这是消息队列本身天然就可以支持的功能。
对于不共享存储的队列,如Kafka使用分区加主备模式,就略微麻烦一些。需要保证每一个分区内的高可用性,也就是每一个分区至少要有一个主备且需要做数据的同步。
3. 服务端承载消息堆积的能力
为了满足我们错峰/流控/最终可达等一系列需求,把消息存储下来,然后选择时机投递就显得是顺理成章的了。
存储可以做成很多方式。比如存储在内存里,存储在分布式KV里,存储在磁盘里,存储在数据库里等等。但归结起来,主要有持久化和非持久化两种。
持久化的形式能更大程度地保证消息的可靠性(如断电等不可抗外力),并且理论上能承载更大限度的消息堆积(外存的空间远大于内存)。
但并不是每种消息都需要持久化存储。很多消息对于投递性能的要求大于可靠性的要求,且数量极大(如日志)。这时候,消息不落地直接暂存内存,尝试几次failover,最终投递出去。
4. 存储子系统的选择
理论上,从速度来看,文件系统 > 分布式KV(持久化) > 分布式文件系统 > 数据库,而可靠性却截然相反。
还是要从支持的业务场景出发作出最合理的选择,如果你们的消息队列是用来支持支付/交易等对可靠性要求非常高,但对性能和量的要求没有这么高,而且没有时间精力专门做文件存储系统的研究,DB是最好的选择。
分布式KV(如MongoDB,HBase)等,或者持久化的Redis,由于其编程接口较友好,性能也比较可观,如果在可靠性要求不是那么高的场景,也不失为一个不错的选择。
5. 消费关系解析
消息队列定义了一堆名词,如JMS 规范中的Topic/Queue,Kafka里面的Topic/Partition/ConsumerGroup,RabbitMQ里面的Exchange等等。
抛开现象看本质,无外乎是单播与广播的区别。所谓单播,就是点到点;而广播,是一点对多点。当然,对于互联网的大部分应用来说,组间广播、组内单播是最常见的情形。
至于广播关系的维护,一般由于消息队列本身都是集群,所以都维护在公共存储上,如config server、zookeeper等。维护广播关系所要做的事情基本是一致的:
-
发送关系的维护。
-
发送关系变更时的通知。
3. 队列高级特性设计
对于可靠投递(消息丢失与重复),事务,性能,不是每类消息队列都会兼顾到,所以要依照业务的需求,来仔细衡量各种特性实现的成本,利弊,最终做出最为合理的设计。
1. 可靠投递(最终一致性)
每当要发生不可靠的事情(RPC等)之前,先将消息落地,然后发送。当失败或者不知道成功失败(比如超时)时,消息状态是待发送,定时任务不停轮询所有待发送消息,最终一定可以送达。
-
具体来说:
- producer往broker发送消息之前,需要做一次落地。
- 请求到server后,server确保数据落地后再告诉客户端发送成功。
- 支持广播的消息队列需要对每个待发送的endpoint,持久化一个发送状态,直到所有endpoint状态都OK才可删除消息。
不是所有的系统都要求最终一致性或者可靠投递,比如一个论坛系统、一个招聘系统。一个重复的简历或话题被发布,可能比丢失了一个发布显得更让用户无法接受。不断重复一句话,任何基础组件要服务于业务场景。
- 消费确认
把消息的送达和消息的处理分开,这样才真正的实现了消息队列的本质-解耦。所以,允许消费者主动进行消费确认是必要的。当然,对于没有特殊逻辑的消息,默认Auto Ack也是可以的,但一定要允许消费方主动ack。
- 重复消息和顺序消息
重复消息是不可能100%避免的,除非可以允许丢失,所以绝对的顺序消息基本上是不能实现的,当然在METAQ/Kafka等pull模型的消息队列中,单线程生产/消费,排除消息丢失,也是一种顺序消息的解决方案。
一般来讲,一个主流消息队列的设计范式里,应该是不丢消息的前提下,尽量减少重复消息,不保证消息的投递顺序。
重复消息,主要是两个话题:
1. 如何鉴别消息重复,并幂等的处理重复消息。
2. 一个消息队列如何尽量减少重复消息的投递。
每一个消息应该有它的唯一身份MessageId,消息到来时能够进行比对就能完成重复的鉴定。
幂等的处理消息是一门艺术,因为种种原因重复消息或者错乱的消息还是来到了,说两种通用的解决方案版本号和状态机。
- 版本号
每次只接受比当前版本号大的消息。
参考TCP/IP协议,如果想让乱序的消息最后能够正确的被组织,那么就应该只接收比当前版本号大一的消息。并且在一个session周期内要一直保存各个消息的版本号。
比如应该的顺序是12,到来的顺序是21,则先把2存起来,待1到来后,再处理2,这样重复性和顺序性要求就都达到了。
- 状态机
使用版本号的最大问题是成本太高。
对发送方必须要求消息带业务版本号。
下游必须存储消息的版本号,对于要严格保证顺序的。
该怎么解决呢?业务方只需要自己维护一个状态机,定义各种状态的流转关系。
举例子说明,假设产品本身状态是下线,1是上线消息,2是下线消息,3是上线消息,正常情况下,消息应该的到来顺序是123,但实际情况下收到的消息状态变成了3123。
那么下游收到3消息的时候,判断状态机流转是下线->上线,可以接收消息。然后收到消息1,发现是上线->上线,拒绝接收,要求重发。然后收到消息2,状态是上线->下线,于是接收这个消息。
此时无论重发的消息1或者3到来,还是可以接收。另外的重发,在一定次数拒绝后停止重发,业务正确。
- 中间件对于重复消息的处理
上述通用的版本号/状态机/ID判重解决方案里,哪些是消息队列该做的、哪些是消息队列不该做业务方处理的呢?其实这里没有一个完全严格的定义,但回到出发点,保证不丢失消息的情况下尽量少重复消息,消费顺序不保证。
减少重复消息的关键步骤:
-
broker记录MessageId,直到投递成功后清除,重复的ID到来不做处理,这样只要发送者在清除周期内能够感知到消息投递成功,就基本不会在server端产生重复消息。
-
对于server投递到consumer的消息,由于不确定对端是在处理过程中还是消息发送丢失的情况下,有必要记录下投递的IP地址。决定重发之前询问这个IP,消息处理成功了吗?如果询问无果,再重发。
2. 事务
满足事务的一致性特征,则必须要么都不进行,要么都能成功。解决方案从大方向上有两种:
- 两阶段提交,分布式事务。
- 本地事务,本地落地,补偿发送。
分布式事务存在的最大问题是成本太高,两阶段提交协议,出现仲裁down机或者单点故障,几乎是一个无解的黑洞。对于交易密集型或者I/O密集型的应用,没有办法承受这么高的网络延迟,系统复杂性。并且成熟的分布式事务构建成本太高。
如何使用本地事务解决分布式事务的问题呢?以本地和业务在一个数据库实例中建表为例子,与其它业务操作在同一个事务里,将消息插入本地数据库。如果消息入库失败,则业务回滚;如果消息入库成功,事务提交,然后发送消息。为了最终一致性,只要消息没有发送成功,就一直靠定时任务重试。
本地事务做的,是业务落地和消息落地的事务,而不是业务落地和RPC成功的事务。如果是后者,是事务嵌套RPC,是大忌,会有长事务死锁等各种风险。
消息只要成功落地,很大程度上就没有丢失的风险(磁盘物理损坏除外)。而消息只要投递到服务端确认后本地才做删除,就完成了producer->broker的可靠投递,并且当消息存储异常时,业务也是可以回滚的。
对不同的业务场景做不同的选择。另外事务的使用应该尽量低成本、透明化,可以依托于现有的成熟框架,如Spring的声明式事务做扩展。业务方只需要使用@Transactional标签即可。
3. 性能相关
- 异步/同步
异步,归根结底你还是需要关心结果的,但可能不是当时的时间点关心,可以用轮询或者回调等方式处理结果;同步是需要当时关心的结果的。
RPC都是可以任意组合的:客户端同步对服务端异步,客户端异步对服务端异步,客户端同步对服务端同步,客户端异步对服务端同步。
对于客户端来说,同步与异步主要是拿到一个Result,还是Future(Listenable)的区别。实现方式可以是线程池,NIO或者其他事件机制。
服务端异步是需要RPC协议支持的。参考servlet 3.0规范,服务端可以吐一个future给客户端,并且在future done的时候通知客户端。
同步能够保证结果,异步能够保证效率,要合理的结合才是最好的方式。
- 批量
生产者消费者模型。
最大的痛点是:消费者到底应该何时进行消费。消费动作都是事件驱动的。主要事件包括:
攒够了一定数量。
到达了一定时间。
队列里有新的数据到来。
为什么网络请求小包合并成大包会提高性能?主要原因有两个:
减少无谓的请求头,如果你每个请求只有几字节,而头却有几十字节,无疑效率非常低下。
减少回复的ack包个数。把请求合并后,ack包数量必然减少,确认和重发的成本就会降低。
4. push/pull 推拉模型简要分析
简要分析下 push 推和 pull 拉模型各自存在的利弊。
- 慢消费
慢消费是push模型最大的致命伤,消费者的速度比发送者的速度慢很多,势必造成消息在broker的堆积。最致命的是broker给consumer推送一堆consumer无法处理的消息,consumer不是reject就是error,然后来回踢皮球。
反观pull模式,consumer可以按需消费,无需记录每一个要发送消息的状态,只需要维护所有消息的队列和偏移量就可以了。所以对于建立索引等慢消费,消息量有限且到来的速度不均匀的情况,pull模式比较合适。
- 消息延迟与忙等
这是pull模式最大的短板。pull模的主动权在消费方,而有没有消息到来的决定权不在消费方。
业界较成熟的做法是从短时间开始(不会对broker有太大负担),然后指数级增长等待。比如开始等5ms,然后10ms,然后20ms,然后40ms……直到有消息到来,然后再回到5ms。
在阿里的RocketMQ里,有一种优化的做法-长轮询,来平衡推拉模型各自的缺点。基本思路是,消费者如果尝试拉取失败,不是直接return,而是把连接挂在那里wait,服务端如果有新的消息到来,把连接notify起来,这也是不错的思路。但海量的长连接block对系统的开销还是不容小觑的,还是要合理的评估时间间隔,给wait加一个时间上限更稳妥。
- 顺序消息
对于本身堆积能力和慢消费就是瓶颈的push模式的消息队列,实现分区下的顺序消息,成本太高了,尤其是必须每个消息消费确认后才能发下一条消息。
反观pull模式,如果想做到分区顺序消息,就相对容易很多:
producer对应partition,并且单线程。
consumer对应partition,消费确认(或批量确认),继续消费即可。
所以对于日志这种全局有序,但允许出现小误差的场景,pull模式非常合适。如果不想看到通篇乱套的日志。
需要顺序消息的场景还是比较有限的而且成本太高,请慎重考虑。
本文的引用仅限自我学习如有侵权,请联系作者删除。
参考知识
消息队列设计