写在开始 :
本文合计2万多字, 500多行, 阅读可能需要花费一点时间;
主要包括消息队列和 常用MQ(比如RabbitMQ, RocketMQ 和 Kafka)的部分高频面题, 可供复习参考使用
日常应用场景: 异步发送(验证码、短信、邮件==),MySQL和Redis、ES之间的数据同步、分布式事务、削峰填谷等等
发送⽅确认机制:信道需要设置为 confirm 模式,则所有在信道上发布的消息都会分配⼀个唯⼀ ID。⼀旦消息被投递到queue(可持久化的消息需要写⼊磁盘),信道会发送⼀个确认给⽣产者(包含消息唯⼀ ID)。如果 RabbitMQ 发⽣内部错误从⽽导致消息丢失,会发送⼀条 nack(未确认)消息给⽣产者。所有被发送的消息都将被 confirm(即 ack) 或者被nack⼀次。但是没有对消息被 confirm 的快慢做任何保证,并且同⼀条消息不会既被 confirm⼜被nack 发送⽅确认模式是异步的,⽣产者应⽤程序在等待确认的同时,可以继续发送消息。当确认消息到达⽣产者,⽣产者的回调⽅法会被触发。
ConfirmCallback接⼝:只确认是否正确到达 Exchange 中,成功到达则回调
ReturnCallback接⼝:消息失败返回时回调
接收⽅确认机制:消费者在声明队列时,可以指定noAck参数,当noAck=false时,RabbitMQ会等待消费者显式发回ack信号后才从内存(或者磁盘,持久化消息)中移去消息。否则,消息被消费后会被⽴即删除。
消费者接收每⼀条消息后都必须进⾏确认(消息接收和消息确认是两个不同操作)。只有消费者确认了消息,RabbitMQ 才能安全地把消息从队列中删除。
RabbitMQ不会为未ack的消息设置超时时间,它判断此消息是否需要重新投递给消费者的唯⼀依据是消费该消息的消费者连接是否已经断开。这么设计的原因是RabbitMQ允许消费者消费⼀条消息的时间可以很⻓。保证数据的最终⼀致性;
如果消费者返回ack之前断开了链接,RabbitMQ 会重新分发给下⼀个订阅的消费者。(可能存在消息重复消费的隐患,需要去重)
延迟队列 : 进入队列的消息会被延迟消费的队列
场景 : 超时订单 限时优惠 定时发布。。。
延迟队列 = 死信交换机 + TTL(生存时间)
消息超时未消费就会变成死信(死信的其他情况:拒绝被消费,队列满了)
延迟队列插件实现延迟队列 DelayExchange
1. 消息被消费⽅否定确认,使⽤ channel.basicNack 或 channel.basicReject ,并且此时
requeue 属性被设置为 false 。
2. 消息在队列的存活时间超过设置的TTL时间。
3. 消息队列的消息数量已经超过最⼤队列⻓度。
那么该消息将成为“死信”。“死信”消息会被RabbitMQ进⾏特殊处理,如果配置了死信队列信息,那么
该消息将会被丢进死信队列中,如果没有配置,则该消息将会被丢弃
为每个需要使⽤死信的业务队列配置⼀个死信交换机,这⾥同⼀个项⽬的死信交换机可以共⽤⼀个,然
后为每个业务队列分配⼀个单独的路由key,死信队列只不过是绑定在死信交换机上的队列,死信交换
机也不是什么特殊的交换机,只不过是⽤来接受死信的交换机,所以可以为任何类型【Direct、
Fanout、Topic】
TTL:⼀条消息或者该队列中的所有消息的最⼤存活时间
如果⼀条消息设置了TTL属性或者进⼊了设置TTL属性的队列,那么这条消息如果在TTL设置的时间内
没有被消费,则会成为“死信”。如果同时配置了队列的TTL和消息的TTL,那么较⼩的那个值将会被使
⽤。
只需要消费者⼀直消费死信队列⾥的消息
生产环境下,使用集群保证高可用性
普通集群、镜像集群、仲裁队列
生产环境下,采用的镜像模式搭建的集群,共有3个节点
镜像队列结构是一主多从(从就是镜像),所有操作都是主节点完成,然后同步给镜像节点;
主宕机后,镜像节点会替代成新的主(如果主从同步前,主机已经宕机,可能出现数据丢失)
采用仲裁队列, 也是主从模式,支持主从数据同步,基于 Raft 协议,强一致.并且使用也简单,不需要额外配置,在声明队列的时候只要指定这个是仲裁队列即可
镜像queue有master节点和slave节点。master和slave是针对⼀个queue⽽⾔的,⽽不是⼀个node作为
所有queue的master,其它node作为slave。⼀个queue第⼀次创建的node为它的master节点,其它
node为slave节点。
⽆论客户端的请求打到master还是slave最终数据都是从master节点获取。当请求打到master节点时,
master节点直接将消息返回给client,同时master节点会通过GM(Guaranteed Multicast)协议将
queue的最新状态⼴播到slave节点。GM保证了⼴播消息的原⼦性,即要么都更新要么都不更新。
当请求打到slave节点时,slave节点需要将请求先重定向到master节点,master节点将将消息返回给
client,同时master节点会通过GM协议将queue的最新状态⼴播到slave节点。
如果有新节点加⼊,RabbitMQ不会同步之前的历史数据,新节点只会复制该节点加⼊到集群之后新增
的消息。
投递消息到队列,消费者从队列中获取消息并消费。多个消费者可以订阅同⼀个队列,这时队列中的消
息会被平均分摊(轮询)给多个消费者进⾏消费,⽽不是每个消费者都收到所有的消息进⾏消费。(注意:RabbitMQ不⽀持队列层⾯的⼴播消费,如果需要⼴播消费,可以采⽤⼀个交换器通过路由Key绑定多个
队列,由多个消费者来订阅这些队列的⽅式。
路由不到,或返回给⽣产者,或直接丢弃,或做其它处理。
这个消息的路由规则。这个路由Key需要与交换器类型和绑定键(BindingKey)联合使⽤才能最终⽣效。
在交换器类型和绑定键固定的情况下,⽣产者可以在发送消息给交换器时通过指定RoutingKey来决定消
息流向哪⾥。
可以指定如何正确的路由到队列了。
交换器和队列实际上是多对多关系。就像关系数据库中的两张表。他们通过BindingKey做关联(多对多
关系表)。在投递消息时,可以通过Exchange和RoutingKey(对应BindingKey)就可以找到相对应的队
列。
客户端紧接着可以创建⼀个AMQP 信道(Channel) ,每个信道都会被指派⼀个唯⼀的D。RabbitMQ 处
理的每条AMQP 指令都是通过信道完成的。信道就像电缆⾥的光纤束。⼀条电缆内含有许多光纤束,允
许所有的连接通过多条光线束进⾏传输和接收。
通过对信道的设置实现
Q ; 发送消息失败?
A : 设置异步发送, 发送失败使用回调进行记录或重发
// 同步发送
RecordMetadata recordMetadata = kafkaProducer.send(record).get();
// 异步发送
kafkaProducer.send(record,new Callback(){
@Override
public void onCompletion(RecordMetadata recordMetadata,Exception e){
if(e != null){System.out.print("消息发送失败日志记录");}
long offset = recordMetadata.offset();
int partition = recordMetadata.partition();
String topic = recordMetadata.topic();
}
});
消息重试 : 失败重试,参数配置,可设置重试次数
// 设置重试次数
prop.put(ProducerConfig.RETRIES_CONFIG,10);
Q : 消息在 Brocker 存储中丢失
发送确认机制 acks , 选择 all,让所有副本参与保存数据后确认
消费者从 Brocker 接收消息丢失
场景:
即时消息中的单对单聊天和群聊,保证发送方消息发送顺序和接收方的顺序一致
充值转账两个渠道在同一时间进行余额变更,短信通知必须有序
原因 :
一个 topic 数据可能存储在不同分区,每个分区都有一个按照顺序的存储的偏移量,如果消费者关联了多个分区不能保证顺序性
解决方案:
发送消息时候指定分区号
发送消息按照相同业务设置相同的 key
Kafka 默认存储和消费消息,不能保证顺序性,因为一个 topic 数据肯存在不同的分区,每个分区都有一个
按照顺序存储的偏移量,如果消费者关联多个分区不能保证顺序性;
如果有这样需求,可以把消息存储同一个分区下,
第一个是发送消息时候指定分区号,第二个是发送消息按照相同业务设置相同 key ,因为默认情况分区是
通过 key 的 hashcode 值来选择分区, hash 值如果一样,分区肯定也一样
集群模式
一个Kafka集群由多个broker实例组成,即使某一台宕机,也不耽误其他 broker 继续对外提供服务
分区备份机制(复制机制)
一个 topic 有多个分区,每个分区有多个副本,一个leader,其余是 follower, 副本存储在不同的 broker
所有分区副本内容相同,如果 leader 故障,自动将其中一个 follower 提升为 leader,保证系统容错性高可用
解释一下复制机制的 ISR
ISR (in-sync replica) 需要同步复制保存的 follower
分区副本分为两类,一个是 ISR ,和 leader 副本同步保存数据,另一个普通副本, 异步同步数据,当 leader挂掉,优先从 ISR 副本列表选取一个作为 leader;
文件存储机制(存储结构)
Kafka 里面 topic 数据存储在分区上,分区如果文件过大会分段存储 segment
每个分段都在磁盘以索引 (.index) 和日志文件 (.log) 形式存储
分段好处是,第一能减少单个文件内容大小,查找数据方便,第二方便 Kafka 进行日志清理
日志清理策略有两个:
根据消息保留时间,当消息保存时间超过指定时间,触发清理,默认168h (7天)
根据 topic 存储的数据大小,当 topic 所占日志文件大小超过阈值,开始删除最久的消息(默认关闭)
这两个策略都是通过 Kafka 的 broker 的配置文件进行设置
总而言之 : 多方协同结果,包括宏观架构,分布式储存, ISR数据同步,以及高效利用磁盘,操作系统特性等等,主要有以上六方面;
Kafka 是⼀种⾼吞吐量、分布式、基于发布/订阅的消息系统,最初由 LinkedIn 公司开发,使⽤Scala
语⾔编写,⽬前是 Apache 的开源项⽬。
broker:Kafka 服务器,负责消息存储和转发
topic:消息类别, Kafka 按照 topic 来分类消息
partition:topic 的分区,⼀个 topic 可以包含多个 partition,
topic 消息保存在各个partition 上
offset:消息在⽇志中的位置,可以理解是消息在 partition 上的偏移
量,也是代表该消息的唯⼀序号
Producer:消息⽣产者
Consumer:消息消费者
Consumer Group:消费者分组,每个 Consumer 必须属于⼀个 group
Zookeeper:保存着集群 broker、 topic、 partition等 meta 数据;另外,还负责 broker 故障
发现, partition leader 选举,负载均衡等功能
Kafka的⽣产者采⽤的是异步发送消息机制,当发送⼀条消息时,消息并没有发送到Broker⽽是缓存起
来,然后直接向业务返回成功,当缓存的消息达到⼀定数量时再批量发送给Broker。这种做法减少了⽹
络io,从⽽提⾼了消息发送的吞吐量,但是如果消息⽣产者宕机,会导致消息丢失,业务出错,所以理
论上kafka利⽤此机制提⾼了性能却降低了可靠性。
**缓冲和削峰 **:上游数据时有突发流量,下游可能扛不住,或者下游没有⾜够多的机器来保证冗余,
kafka在中间可以起到⼀个缓冲的作⽤,把消息暂存在kafka中,下游服务就可以按照⾃⼰的节奏进⾏慢
慢处理。
解耦和扩展性 :项⽬开始的时候,并不能确定具体需求。消息队列可以作为⼀个接⼝层,解耦重要的业
务流程。只需要遵守约定,针对数据编程即可获取扩展能⼒。
冗余 :可以采⽤⼀对多的⽅式,⼀个⽣产者发布消息,可以被多个订阅topic的服务消费到,供多个毫⽆关联的业务使⽤。
健壮性 :消息队列可以堆积请求,所以消费端业务即使短时间死掉,也不会影响主要业务的正常进⾏。
**异步通信 **:很多时候,⽤户不想也不需要⽴即处理消息。消息队列提供了异步处理机制,允许⽤户把⼀个消息放⼊队列,但并不⽴即处理它。想向队列中放⼊多少消息就放多少,然后在需要的时候再去处理它们。
ISR:In-Sync Replicas 副本同步队列
AR:Assigned Replicas 所有副本ISR是由leader维护,follower从leader同步数据有⼀些延迟(包括延
迟时间replica.lag.time.max.ms和延迟条数replica.lag.max.messages两个维度, 当前最新的版本0.10.x
中只⽀持replica.lag.time.max.ms这个维度),任意⼀个超过阈值都会把follower剔除出ISR, 存⼊
OSR(Outof-Sync Replicas)列表,新加⼊的follower也会先存放在OSR中。AR=ISR+OSR。
Q : Kafka的消费者如何消费数据
消费者每次消费数据的时候,消费者都会记录消费的物理偏移量( offset)的位置等到下次消费时,他
会接着上次位置继续消费
Q : Kafka消费者负载均衡策略
⼀个消费者组中的⼀个分⽚对应⼀个消费者成员,他能保证每个消费者成员都能访问,如果组中成员太
多会有空闲的成员
Q : kafaka⽣产数据时数据的分组策略
⽣产者决定数据产⽣到集群的哪个 partition 中每⼀条消息都是以( key, value)格式 Key是由⽣产者
发送数据传⼊所以⽣产者( key)决定了数据产⽣到集群的哪个 partition
Q : Kafka中是怎么体现消息顺序性的?
kafka每个partition中的消息在写⼊时都是有序的,消费时,每个partition只能被每⼀个group中的⼀个
消费者消费,保证了消费时也是有序的。整个topic不保证有序。如果为了保证topic整个有序,那么将
partition调整为1.
Kafka并没有使⽤JDK⾃带的Timer或者DelayQueue来实现延迟的功能,⽽是基于时间轮⾃定义了⼀个
⽤于实现延迟功能的定时器(SystemTimer)。JDK的Timer和DelayQueue插⼊和删除操作的平均时间
复杂度为O(nlog(n)),并不能满⾜Kafka的⾼性能要求,⽽基于时间轮可以将插⼊和删除操作的时间复杂
度都降为O(1)。时间轮的应⽤并⾮Kafka独有,其应⽤场景还有很多,在Netty、Akka、Quartz、
Zookeeper等组件中都存在时间轮的踪影。底层使⽤数组实现,数组中的每个元素可以存放⼀个
TimerTaskList对象。TimerTaskList是⼀个环形双向链表,在其中的链表项TimerTaskEntry中封装了
真正的定时任务TimerTask.Kafka中到底是怎么推进时间的呢?Kafka中的定时器借助了JDK中的
DelayQueue来协助推进时间轮。具体做法是对于每个使⽤到的TimerTaskList都会加⼊到DelayQueue
中。Kafka中的TimingWheel专⻔⽤来执⾏插⼊和删除TimerTaskEntry的操作,⽽DelayQueue专⻔负
责时间推进的任务。再试想⼀下,DelayQueue中的第⼀个超时任务列表的expiration为200ms,第⼆个
超时任务为840ms,这⾥获取DelayQueue的队头只需要O(1)的时间复杂度。如果采⽤每秒定时推进,
那么获取到第⼀个超时的任务列表时执⾏的200次推进中有199次属于“空推进”,⽽获取到第⼆个超时
任务时有需要执⾏639次“空推进”,这样会⽆故空耗机器的性能资源,这⾥采⽤DelayQueue来辅助以少
量空间换时间,从⽽做到了“精准推进”。Kafka中的定时器真可谓是“知⼈善⽤”,⽤TimingWheel做最
擅⻓的任务添加和删除操作,⽽⽤DelayQueue做最擅⻓的时间推进⼯作,相辅相成。
a. ⽣产者订单系统先发送⼀条half消息到Broker,half消息对消费者⽽⾔是不可⻅的
b. 再创建订单,根据创建订单成功与否,向Broker发送commit或rollback
c. 并且⽣产者订单系统还可以提供Broker回调接⼝,当Broker发现⼀段时间half消息没有收到任
何操作命令,则会主动调此接⼝来查询订单是否创建成功
d. ⼀旦half消息commit了,消费者库存系统就会来消费,如果消费成功,则消息销毁,分布式事务成功结束
e. 如果消费失败,则根据重试策略进⾏重试,最后还失败则进⼊死信队列,等待进⼀步处理
根据CAP理论,同时最多只能满⾜两个点,⽽zookeeper满⾜的是CP,也就是说zookeeper并不能保证
服务的可⽤性,zookeeper在进⾏选举的时候,整个选举的时间太⻓,期间整个集群都处于不可⽤的状
态,⽽这对于⼀个注册中⼼来说肯定是不能接受的,作为服务发现来说就应该是为可⽤性⽽设计。
基于性能的考虑,NameServer本身的实现⾮常轻量,⽽且可以通过增加机器的⽅式⽔平扩展,增加集
群的抗压能⼒,⽽zookeeper的写是不可扩展的,⽽zookeeper要解决这个问题只能通过划分领域,划
分多个zookeeper集群来解决,⾸先操作起来太复杂,其次这样还是⼜违反了CAP中的A的设计,导致
服务之间是不连通的。
持久化的机制来带的问题,ZooKeeper 的 ZAB 协议对每⼀个写请求,会在每个 ZooKeeper 节点上保
持写⼀个事务⽇志,同时再加上定期的将内存数据镜像(Snapshot)到磁盘来保证数据的⼀致性和持久
性,⽽对于⼀个简单的服务发现的场景来说,这其实没有太⼤的必要,这个实现⽅案太重了。⽽且本身
存储的数据应该是⾼度定制化的。
消息发送应该弱依赖注册中⼼,⽽RocketMQ的设计理念也正是基于此,⽣产者在第⼀次发送消息的时
候从NameServer获取到Broker地址后缓存到本地,如果NameServer整个集群不可⽤,短时间内对于⽣
产者和消费者并不会产⽣太⼤影响。
RocketMQ由NameServer注册中⼼集群、Producer⽣产者集群、Consumer消费者集群和若⼲
Broker(RocketMQ进程)组成,它的架构原理是这样的:
Broker在启动的时候去向所有的NameServer注册,并保持⻓连接,每30s发送⼀次⼼跳
Producer在发送消息时候从NameServer获取Broker服务器地址,根据负载均衡算法选择⼀台服务器来发送消息
Conusmer消费消息的时候同样从NameServer获取Broker地址,然后主动拉取消息来消费
因为使⽤了顺序存储、Page Cache和异步刷盘。我们在写⼊commitlog的时候是顺序写⼊的,这样⽐
随机写⼊的性能就会提⾼很多,写⼊commitlog的时候并不是直接写⼊磁盘,⽽是先写⼊操作系统的
PageCache,最后由操作系统异步将缓存中的数据刷到磁盘
A : 消息可靠传输代表了两层意思,既不能多也不能少。
零拷⻉: kafka和RocketMQ都是通过零拷⻉技术来优化⽂件读写。
传统⽂件复制⽅式: 需要对⽂件在内存中进⾏四次拷⻉。
零拷⻉: 有两种⽅式, mmap和transfile,Java当中对零拷⻉进⾏了封装, Mmap⽅式通过
MappedByteBuffer对象进⾏操作,⽽transfile通过FileChannel来进⾏操作。Mmap 适合⽐较⼩的⽂件,通常⽂件⼤⼩不要超过1.5G ~2G 之间。Transfile没有⽂件⼤⼩限制。RocketMQ当中使⽤Mmap⽅式来对他的⽂件进⾏读写。
在kafka当中,他的index⽇志⽂件也是通过mmap的⽅式来读写的。在其他⽇志⽂件当中,并没有使⽤零拷⻉的⽅式。Kafka使⽤transfile⽅式将硬盘数据加载到⽹卡。
大体思路 :
- 从整体到细节,从业务场景到技术实现。
- 以现有产品为基础。
MQ作⽤、项⽬⼤概的样⼦。
类别 | Kafka | RocketMQ | RabbitMQ |
---|---|---|---|
单机吞吐量 | 17w/s | 11w/s | 2.6w/s(消息持久化) |
开发语言 | Scala/java | java | Erlang |
维护者 | Apache | Alibaba | Mozilla/Spring |
订阅形式 | 基于topic,按照t进行正则匹配 | ||
发布订阅模式 | 基于topic/messageTag,按照消息类型,属性进行正则匹配,发布订阅 | 提供四种,direct,topic,Headers | |
fanout(就是广播模式) | |||
持久化 | 支持大量堆积 | 支持大量堆积 | 支持少量堆积 |
顺序消息 | 支持 | 支持 | 不支持 |
集群方式 | Leader-Slave,无状态集群,每台服务器既Master也Slave | 多对M-S模式,开源版本需手动切换S变Master | 支持简单集群,复制模式,高级集群模式支持不好 |
性能稳定性 | 较差 | 一般 | 好 |
综上所述 :
Kafka:
优点: 吞吐量⾮常⼤,性能⾮常好,集群⾼可⽤。
缺点:会丢数据,功能⽐较单⼀。
使⽤场景:⽇志分析、⼤数据采集
RabbitMQ:
优点: 消息可靠性⾼,功能全⾯。
缺点:吞吐量⽐较低,消息积累会严重影响性能。erlang语⾔不好定制。
使⽤场景:⼩规模场景。
RocketMQ:
优点:⾼吞吐、⾼性能、⾼可⽤,功能⾮常全⾯。
缺点:开源版功能不如云上商业版。官⽅⽂档和周边⽣态还不够成熟。客户端只⽀持java。
使⽤场景:⼏乎是全场景。
码字不易,麻烦大家小手一点 , 点赞或关注 , 感谢大家的支持!!