面试八股-各种中间件
1.消息队列
1.1.应用场景
- 异步处理 减少请求的等待时间,将请求放入队列待处理,提高总体性能
- 应用解耦 将消息存入队列,下游服务订阅并消费
- 流量削峰 在高峰期应对消息过多导致崩溃的情况,MQ积压请求,系统每次从MQ拉取能力范围内的数据
1.2.示例
- 电商系统
- 日志收集系统
- JMS
1.3.消息模型
-
队列模型:消费者之间是竞争关系,即每条消息只能被一个消费者消费
-
发布订阅模型:将消息发送至一个topic中,所有订阅该topic的服务消费(同时也是kafka的原理)
1.4.消费方式
- 同步 收到消息之前阻塞
- 异步 消息到达之后,自动调用onMessage方法
1.5.各种消息队列实现的比较
Kafka 1.1.0 | RabbitMQ 3.6.10 | RocketMQ 4.2.0 | |
---|---|---|---|
优先队列 | 不支持 | 支持,优先级0-10之间 | 支持,维护不同的优先级队列,根据message的优先级发送到对应的队列中 |
延迟队列 | 不支持 | 支持 | 支持 |
死信队列 | 不支持 | 支持 | 支持 |
重试队列 | 不支持 | 支持 | 支持 |
消费模式 | pull | pull+push | pull+长轮询 |
广播消费 | 支持,Kafka对于广播消息的支持更加正统 | 支持 | 支持 |
批量消息 | 支持,生产者异步发送 | 支持 | 支持,生产者同步发送 |
消息回溯 | 支持Kafka支持offset和timestamp两种维度进行消息回溯 | 不支持,一旦被确认消费就会被删除 | 支持 |
消息堆积 | 支持,海量消息堆积,堆积能力和磁盘大小挂钩 | 支持,但是内存达到阈值时,性能会受到影响 | 支持海量消息堆积 |
持久化方式 | 消息队列,segment方式 | 支持 | 消息队列 |
消息追踪 | 不支持。消息追踪可以通过外部系统来支持,但是支持粒度没有内置的细腻 | 支持 | 支持 |
消息过滤 | 客户端级别的支持,可用过kafka stream进行消息过滤 | 不支持,但是二次封装一下也比较简单 | 支持,可通过message tag、属性进行过滤 |
多租户 | 不支持 | 支持 | 不支持 |
多协议支持 | 只支持定义协议,目前几个主流版本间存在兼容性问题 | AMQS | JMS,OpenMessaging |
跨语言支持 | 当前版本Java编写,支持多种语言客户端 | 采用Erlang编写,支持多种语言客户端 | Java C++ Go |
流量控制 | 支持client和user级别,通过主动设置可将流控作用于生产者或消费者 | 流量控制基于credit-base算法,是内部被动触发的保护机制,用于生产者层面 | RocketMQ提供了针对不同维度的流量控制 |
消息顺序性 | 支持普通的顺序消息,即对于单个分区的消息发送和消费是有序的,但是不保证不重复 | 顺序性条件比较苛刻,需要单线程发送,单线程消费并且不采用延迟队列、优先级队列等一些高级功能,某种意义上来说不支持顺序性 | 支持普通的顺序消息和严格的顺序消息 |
幂等性 | 支持单个producer单个分区的会话幂等性 | 不支持 | 不支持,不解决消息的重复问题 |
事务性消息 | 支持 | 支持 | 支持 |
性能 | 最高 | 高 | 高 |
高可用和容错 | 使用partition的副本机制和isr选举机制保证高可用 | 普通集群非高可用,可用镜像模式和主备集群 | 通过broker的master和slave实现高可用 |
定时消息 | 不支持 | 支持 | 支持 |
负载均衡 | 客户端消费者负载均衡,需要一个broker作为corrdinator | 默认是轮询 | 客户端负载均衡,支持平均和轮询分配 |
刷盘策略 | 默认是异步刷盘 | 默认是内存存储 | 默认同步刷盘 |
1.6.各种问题的解法
1.6.1.防止消息丢失
分类
- 生产消息:消息发送至broker的时,需要处理broker响应,如果写入失败进行重传。
- 存储消息:如果消息发送至broker缓存中还未刷盘便挂掉,则会出现信息丢失,此时需要做broker的备份。
- 消费消息:如果消息发送至消费者缓存就宕机,则会出现信息丢失,正确做法是消费者处理完业务逻辑之后在给broker响应。
实现
- RabbitMQ
- 生产者丢消息:生产者发送之前丢失。生产者发送数据前开启事务发送消息,失败则回滚,成功则提交,这种同步方式可能会造成吞吐量下降。开启confirm模式,每条消息都会有唯一id,写入成功后有对应的消息回复,如果超时也可以重发,这种异步机制是一般会采用的。
- RabbitMQ自己丢消息:RabbitMQ没有持久化,重启数据丢失。需要设置消息持久化到磁盘,首先创建queue的时候将其设置为持久化,可以保证RabbitMQ持久化queue的元数据。发送消息时将消息的deliveryMode设置为2,这样消息就会被设为持久化方式,必须同时开启这两个才行。和生产者confirm机制配合使用。
- 消息端丢消息:消费者刚读到数据就挂掉。关闭RabbitMQ的自动ACK,让消费者手动ack。
- Kafka
- 生产者丢消息:发送过程中丢失数据。关闭自动提交offset,生产者处理完毕之后手动提交offset。
- Kfaka自己丢消息:
- 给topic 设置replication.factor 参数大于1,要求每个partition至少有两个副本
- 在kafka服务端设置min.isync.replicas参数大于1,要求一个leader感知到至少有一个follower在跟自己保持正常联系同步数据
- 在生产端设置acks=all,要求每条数据必须写入所有replica副本之后,才能认为是成功了
- 在生产端设置retries=MAX,表示一旦写入失败,就不停重试
- 消费端丢消息:自己自动提交了offset还没来的急处理消息。设置ack=all后就不会丢失数据。
1.6.2.消息幂等处理(不重复消费)
- 天然幂等的操作
- 查询操作
- 删除操作
- 机制
- 唯一索引
- token机制
- 锁
- 悲观锁和乐观锁
- 分布式锁
- select+insert+并发不高的后台系统
1.6.3.消息按序处理
分类
-
全局有序:只能由一个生产者往一个队列中发消息,并且消费者单线程消费。
-
部分有序:大部分需求是部分有序。即将Topic内部划分为所需队列数,把消息用过特定策略发往固定队列中,每个队列对应一个单线程处理的消费者。即完成了部分有序的需求,又通过增加队列数量来提高并发性和处理效率。
实现
- RabbitMQ:
- 原因:集群部署MQ,不同消费者执行速度不同导致顺序错乱
- 解决:每个消费者消费固定的queue,同一个订单号的消息发送到同一个queue中
- Kafka:
- 原因:一个topic下同一个partition中消息肯定有序,生产者写时指定key,同一个key肯定发到同一个partition中。但是最终消费者使用多线程,每个线程速度不一致
- 解决:处理线程前面增加一个内存队列,单个线程只负责其中一个队列的消息
- RocketMQ:
- 原因:一个Topic可以指定多个MessageQueue,同一个订单号的binlog进入了不同的MessageQueue,进而导致一个订单的binlog被不同机器的消费者处理
- 解决:想办法让同一个订单的binlog进入同一个MessageQueue中
1.6.4.处理堆积消息
-
消费者消费慢,优化消费逻辑
-
扩容,增加队列数和消费者数。一定要增加队列数,因为一个Topic中一个队列只会分配给一个消费者
1.6.5.MQ失败消息的处理
- 核心业务队列
- 死信队列 失败消息存储,由一个专门程序负责检测目标消费者是否正常,正常的话从该队列中取出消息进行消费
1.7.Kafka速度快的原因
- 顺序写入 每个partition其实都是一个文件,受到消息后Kafka把数据插入到文件末尾,消费者有offset用于保存读到了第几条数据。
- Memory Mapped Files 使用内存映射文件,直接利用操作系统的Page来实现文件到物理内存的直接映射。操作系统会在适时进行同步硬盘操作,分为同步和异步两种模式,前者等待flush之后再返回,后者写入mmap之后直接返回。
2.搜索引擎
2.1.搜索方式
- 顺序搜索法
- 索引搜索法 全文搜索使用的
2.2.特点
- 相关度排序
- 文本关键字高亮显示
- 摘要截取
- 只关注文本,不考虑语义
- 搜索效果更加精确
2.3.使用场景
- 替换数据库模糊查询,数据库左模糊会让索引失效
- 全文检索式搜索引擎寄出
- 只对指定领域的网站进行索引和搜索,即垂直搜索
- 可以在word、pdf等各种各样的数据格式中检索内容
- 输入法等
2.4.倒排索引
2.4.1.创建索引
- 分词
- 语言处理(单词缩减为词根等)
- stemming 词干提取,用缩减方式
- lemmatization 词形还原,用转换方式
- 索引组件创建字典
- 文档倒排链表
2.4.2.使用索引
- 词法分析识别单词和关键字
- 语法分析生成语法树
- 语言处理(和索引一致)
- 根据文档和查询语句相关性排序
2.4.3.计算工具
权重计算公式 余弦相似度
Lucene 全文搜索工具包 依赖于Java 不适合集群环境
ES:全文搜索服务器 基于Lucene Rest HTTP调用,对集群支持
分词器 找几个常用分词器
-
standard(过滤标点符号
-
simple (过滤数字和标点符号)
-
whitespace (不过滤,按照空格分隔)
-
stop (过滤停顿单词及标点符号,例如is are等等)
-
keyword (视为一个整体不进行任何处理)
-
path hierarchy tokenizer (路径层次分词器)
-
中文分词 analysis-ik
3.Kafka
3.1.特点
-
一个分布式流式处理平台,也可以当作消息队列使用。
-
性能强,且非常可靠,在大数据领域生态兼容性好。
-
采用的是 发布-订阅模型,当一个生产者生产消息后,其他订阅该消息的消费者都可以消费完整的消息。
3.2.架构
- Kafka-cluster:一个Kafka集群中包含多个broker
- Broker:可以看作是一个独立的Kafka服务器,每个broker上有多个topic
- Topic:主题,生产者将消息发送到特定的主题,消费者从订阅的主题消息,每个topic有多个分区,来提高负载
- Partition:分区,每个topic有多个分区,并且这些分区有多个副本,可以分布在不同的broker上,防止数据丢失
3.3.多副本机制
每个分区都有多个副本,发送的消息先存入leader,然后同步到其他副本。
- 多副本保证了数据安全性,防止一个broker宕机造成的数据丢失
- 多分区的目的是提高并发能力,即负载均衡,分区分布在不同的broker上
3.4.和RocketMQ
RocketMQ的出现是因为kafka不支持在交易、订单、充值等场景下许多特性,于是阿里编写了RocketMQ,定位于非日志的可靠消息传输;而kafka诞生的时候定位是日志传输。
3.5.zookeeper管理Kafka
- broker注册:每个broker在启动时,都会到zookeeper上注册
- topic注册:kafka中topic会被分为几个分区并分布在broker上,这些分区信息和与broker的对应关系也都会由zookeeper维护
- 负载均衡:zookeeper根据分区数量和消费者来负载均衡
3.6.消息相关问题解法
3.6.1.消息有序性
一个topic中可以有多个分区,消费者发送消息时在每个分区中使用的是先入先出的方法。能保证每个分区中消息的有序性。但没办法保证各分区之间的有序性。
- 一个topic只对应一个分区
- 发送消息时指定分区
3.6.2.保证消息不丢失
1)生产者:发送时要有返回,即使用callback得知消息是否发送成功
2)消费者:当提交了偏移量后还没有消费,挂掉,那就会造成丢失。因此必须在真正消费后提交偏移量。这时候可能会出现重复消费,比如消费后偏移量还没提交就挂掉。
3)Kafka:当分区副本中的leader挂掉后,如果消息还没有同步到其他副本上,会造成消息丢失,因此要设置一定有几个副本更新完消息后才算发送成功。
3.6.3保证不重复消费-幂等处理
通过业务来保证
4.Zookeeper
是一个分布式服务管理服务,用来管理分布式服务中的一些问题,是很多分布式技术和框架的重要组件,比如hadoop,dubbo。
4.1.用途
- 集群管理:分布式系统中会有节点的加入和退出,其他机器需要感知变化,并作出相应的调整和决策。
- 命名服务:通过命名服务,分布式系统可以根据特定的名字来获取资源或服务器。
- 负载均衡
- 分布式锁:对于分布式系统,多个进程直线的活动之间会需要进行协调,zookeeper来处理。
4.2.运行模式
- 单机模式
- 集群模式
- 伪集群模式 (各机器在同一台机器上)
4.3.角色
- Leader 进行投票的发起和决议,更新系统状态
- Follower 接受客户端请求,并给客户端返回结果,参与投票。(写操作只有leader可以做,follower得到要转发给leader,读操作谁都可以做)
- Observer 不参与投票,可以提高可伸缩性(增加节点提高负载能力)。等于增加数个不参与投票的节点
4.4.节点的四种状态
- leading
- following
- observing
- looking 没有leader时,寻找leader的状态
4.5.Zab协议(原子广播协议)
4.5.1.限制
其定义了事务请求的方式
- 所有事务请求必须由一个全局唯一的服务器来协调处理,即leader
- leader负责将一个客户端请求,转化为一个proposal,并广播到其他节点
- leader要保证超过半数节点得到正确响应后,leader才会向所有follower服务器发送commit消息进行proposal提交
4.5.2.消息广播
正常接收事务的状态。消息广播流程如下
- 客户端发起一个写请求,leader将请求转化成一个proposal,并分配一个全局zxid
- leader为每个follower服务器分配一个单独的队列,然后将广播的proposal放在队列中,并根据先入先出的规则进行消息发送(使用队列保证异步级耦合)
- follower收到proposal后,首先将其以事务的方式写入本地磁盘,写入成功后返回ack
- leader收到超半数ack后,即认为消息发送成功,可以发送commit消息
- leader和follower将事务提交
4.5.3.崩溃恢复
条件:
- leader崩溃
- 服务器启动
- 不存在半数节点和leader通信
主要阶段:
- 选举阶段
- 发现阶段
- 同步阶段
选举阶段
选举机制必须满足两个条件
- 新选择的leader不能包含未提交的proposal
- 新选择的leader必须包含最大的事务id(避免leader事务回退)
具体流程
每个服务器有两个id,myid(自己id)和zxid(事务id)
- 初始投票,每个服务器都给自己投一票
- 同步投票结果,每个服务器将各自投票结果同步给其他服务器
- 检查投票有效性
- 处理投票,先检查zxid,较大的服务器优先作为leader,zxid相同时,myid较大的作为leader
- 统计投票结果,超半数节点得到相同的结果时才算成功
- 更改服务器状态
发现阶段 发现leader
-
从所有节点发现最新的zxid,防止因为出现意外生成两个leader
-
zxid前32位为epch,epoch最大的是leader
-
leader接受所有follower发来的epoch,并选择最新的,发送给follower,并在上面加1作为自己的epoch值
同步阶段 同步数据
leader接收所有follower发送的zxid,并选择最大的更新到自己的事务日志,并将事务日志同步给所有follower,超半数同步完成则结束崩溃恢复
4.6.处理丢弃的事务 - 依靠zxid
每个新事务都有一个递增的唯一zxid
如果一个节点数据同步时发现自身最大zxid要大于leader上的最大zxid,则进行回退。
4.7.zookeeper的事件通知-watcher
客户端可以像节点注册watcher,节点发生变化时,通知客户端作出相应的处理