1.kafka的读写效率快的原因()
(1) 利用 Partition 实现并行处理
Kafka 是一个 Pub-Sub 的消息系统,无论是发布还是订阅,都要指定 Topic;
Topic 只是一个逻辑的概念。每个 Topic 都包含一个或多个 Partition,不同 Partition 可位于不同节点。
一方面,由于不同 Partition 可位于不同机器,因此可以充分利用集群优势,实现机器间的并行处理。
另一方面,由于 Partition 在物理上对应一个文件夹,即使多个 Partition 位于同一个节点,也可通过配置让同一节点上的不同 Partition 置于不同的磁盘上,从而实现磁盘间的并行处理,充分发挥多磁盘的优势。
(2) 顺序写磁盘
Kafka 中每个分区是一个有序的,不可变的消息序列,新的消息不断追加到 partition 的末尾,这个就是顺序写。
Kafka的删除并不是"读-写"这种形式,而是将Partition分为多个Segment,每个Segment对应一个物理文件,通过删除整个文件的方式去删除Partition 内的数据。这种方式清除旧数据的方式,也避免了对文件的随机写操作。
(3) 充分利用 Page Cache
[1] I/O Scheduler 会将连续的小块写组装成大块的物理写从而提高性能。
[2] I/O Scheduler 会尝试将一些写操作重新按顺序排好,从而减少磁盘头的移动时间。
[3] 充分利用所有空闲内存(非 JVM 内存)。如果使用应用层 Cache(即 JVM 堆内存),会增加 GC 负担。
[4] 读操作可直接在 Page Cache 内进行。如果消费和生产速度相当,甚至不需要通过物理磁盘(直接通过 Page Cache)交换数据。
[5] 如果进程重启,JVM 内的 Cache 会失效,但 Page Cache 仍然可用。
(4) 零拷贝技术
技术指在计算机执行操作时,CPU 不需要先将数据从一个内存区域复制到另一个内存区域,从而可以减少上下文切换以及 CPU 的拷贝时间。
[1] 网络数据持久化到磁盘 (Producer 到 Broker)
传统模式下:数据从网络传输到文件需要 4 次数据拷贝、4 次上下文切换和两次系统调用。
① 首先通过 DMA copy 将网络数据拷贝到内核态 Socket Buffer。
② 然后应用程序将内核态 Buffer 数据读入用户态(CPU copy)
③ 接着用户程序将用户态 Buffer 再拷贝到内核态(CPU copy)
④ 最后通过 DMA copy 将数据拷贝到磁盘文件
kafka MMap:
使用 mmap 的目的是将内核中读缓冲区(read buffer)的地址与用户空间的缓冲区(user buffer)进行映射。从而实现内核缓冲区与应用程序内存的共享,省去了将数据从内核读缓冲区(read buffer)拷贝到用户缓冲区(user buffer)的过程。它的工作原理是直接利用操作系统的 Page 来实现文件到物理内存的直接映射。完成映射之后你对物理内存的操作会被同步到硬盘上。
使用这种方式可以获取很大的 I/O 提升,省去了用户空间到内核空间复制的开销。
[2] 磁盘文件通过网络发送(Broker 到 Consumer)
传统模式:
① 首先通过系统调用将文件数据读入到内核态 Buffer(DMA 拷贝)
② 然后应用程序将内 存态 Buffer 数据读入到用户态 Buffer(CPU 拷贝)
③ 接着用户程序通过 Socket 发送数据时将用户态 Buffer 数据拷贝到内核态 Buffer(CPU 拷贝)
④ 最后通过 DMA 拷贝将数据拷贝到 NIC Buffer
kafka:
Kafka 在这里采用的方案是通过 NIO 的 transferTo/transferFrom 调用操作系统的 sendfile 实现零拷贝。总共发生 2 次内核数据拷贝、2 次上下文切换和一次系统调用,消除了 CPU 数据拷贝
(5) 批处理
在很多情况下,系统的瓶颈不是 CPU 或磁盘,而是网络IO。
因此,除了操作系统提供的低级批处理之外,Kafka 的客户端和 broker 还会在通过网络发送数据之前,在一个批处理中累积多条记录 (包括读和写)。记录的批处理分摊了网络往返的开销,使用了更大的数据包从而提高了带宽利用率。
(6) 数据压缩
Producer 可将数据压缩后发送给 broker,从而减少网络传输代价,目前支持的压缩算法有:Snappy、Gzip、LZ4。数据压缩一般都是和批处理配套使用来作为优化手段的。
2.redis数据主从同步
(1)全量同步:
需要在主库上进行一次bgsave将当前内存的数据全部快照到磁盘文件中,然后在将快照文件的内容全部传送到从节点。从节点将快照文件接收完毕后,立即执行一次全量加载,加载之前先要将当前内存的数据清空。
(2)部分同步:
只同步从服务器中没有的数据,涉及到赋值偏移量和赋值积压缓冲区。
(3)命令传播:
用于在master的数据库状态被修改时,将导致变更的命令传播给slave,从而让slave的数据库状态与master保持一致。
(4)复制挤压缓冲区
[1] 它是master维护的一个固定长度的先进先出的内存队列(FIFO),默认为1MB。当队列长度超过时,最先进入的元素被弹出。队列的生存时间默认为3600秒。如果master不在有与之相连接的slave,并且该状态持续时间超过了队列生成时间,master就会释放该队列,等到有需要的时候在创建。
[2] 如果从服务器与master失联后重连成功,从服务器通过PSYNC命令将自己的赋值偏移量发送给master,master通过偏移量来决定采用全量同步还是增量同步。如果从服务器的复制偏移量之后的数据仍然存在于缓冲区中,则采用部分同步,否则采用全量同步
[3] 3.0之后增加了WAIT命令,提供两个参数,第一个参数时从库的数量N,第二个参数时时间t,以毫秒为单位。它标识等待wait指令之前的所有写操作同步到N个从库,最多等待时间t,如果t=0则表示无限等待所有操作完成
(5) 主从同步策略
刚链接时进行全量同步,全量同步结束后进行增量同步。当有需要是slave可触发全量同步,但通常只会触发增量同步,只有增量同步失败才会尝试全量同步。
3.redis持久化
(1) RDB
[1] Redis是一个单进程的服务,通过fork产生子进程,父进程继续处理Client请求,子进程负责将快照写入临时文件中,完成后用临时文件替换原有的快照文件
[2] rdbSave函数负责将内存中的数据库数据以RDB格式保存到磁盘中,如果RDB文件已存在,那么新文件将替换原有文件。Rdbload用于将RDB文件中的数据重新载入到内存中
[3] SAVE和BGSAVE命令的区别
① SAVE直接调用rdbSave,阻塞Redis主进程,直到保存完成为止。
② BGSAVE则fork出一个子进程,子进程负责调用rdbSave,并在保存完成之后向主进程发送信号,通知保存已完成。
③ save配置
a)save s number
s:时间单位秒 number:数据修改次数
例:save 100 10
含义:当客户端在100秒内对数据库中数据,进行了10次修改,则自动执行BGSAVE命令
[4] 优点
① RDB文件紧凑,全量备份,非常适合用于进行备份和容灾处理恢复。
② 生成RDB文件时,redis柱进行会调用fork()产生一个紫禁城来处理所有保存工作,柱进行不需要进行任何磁盘IO操作。
③ RDB在恢复大数据集时的速度比AOF的速度快。
[5] 缺点
RDB快照是一次全量备份,存储的是内存数据的二进制序列化形式数据,粗出紧凑。当进行快照持久化时,会开启一个字进行负责快照的持久化过程,但当子进程在持久化过程中不会保存主进程的变动,可能会导致丢失数据。
(2) AOF
[1] 容写入缓冲区,子进程写完退出,父进程接收退出消息后,将缓冲区AOF写入临时文件,覆盖原文件
[2] AOF协议文本,将所有对数据库进行写操作命令记录到文件中,以此达到记录数据库状态的目的
[3] AOF生成文件过程
① 命令传播:Redis将执行完的命令、参数等信息发送到AOF程序中
② 缓存追加:根据接收到的命令数据,将命令转换为网络通讯协议的格式,然后将协议内容追加到服务器的AOF缓存中
③ 文件写入:AOF缓存中的内容被写入AOF文件末尾,如果社零的AOF保存条件被满足的话,调用fsync函数或fdatasync函数,将写入的内容真正保存到磁盘
[4] sync频率配置:
通过appendfsync参数进行控制,有三个值
① AOF_FSYNC_NO 不保存
② AOF_FSYNC_EVERYSEC 每秒保存一次(默认)
③ AOF_FSYNC_ALWAYS 每执行一个命令保存一次
[5] AOF文件加载过程
① 创建一个不带网络连接的伪客户端(fake client)
② 读取AOF所保存的文件,并根据内容还原出命令及参数等信息
③ 根据命令使用伪客户端进行执行
[6] 优点
① AOF可以更好的保护数据不丢失,一般AOF会间隔1s,通过一个后台程序执行一次fsync操作,所以最多智慧丢失1s的数据。
② AOF日志文件没有任何磁盘寻址开销,写入性能搞,文件不容易破损
③ AOF日志文件及时过大的时候,出现后台重写操作也不会影响客户端。
④ AOF日志文件可以通过修改错误操作的日志文件内容的方式回复所有数据。
[7] 缺点
① 对于同一份数据来说,AOF日志文件通常比RDB数据快照文件更大。
② AOF开启后,支持的写QPS会比RDB支持的写QPS低。
③ AOF通过日志回复数据,不一定会与预期完全一致
(3) RDB与AOF的区别
[1] RDB使用快照生成文件,崩溃恢复时在通过rdbloade将文件加载到内存中
[2] AOF使用缓冲区记录所有已执行命令,将命令写入文件,崩溃恢复时通过创建一个伪客户端读取文件并执行
4.redis 键过期处理
(1) Redis所有的数据结构都可以设置过期时间,时间到了会自动删除。Redis会将每个设置了过期时间的key放入一个独立的字典中,以后会定时遍历这个字典来删除到期的key(redis采用惰性删除和定期删除结合的策略,以达到合理使用cpu时间和避免浪费内存空间之间的平衡)
(2) 惰性删除:仅在每次收到对key的请求时才检查是否过期,如果已过期则删除key
(3) 定期删除:每隔一段时间对key字典进行检查,删除过期的key
(4) 从库的过期策略:从库对key的过期处理是被动的,会在AOF文件中添加一条del指令,同步到所有从库中,根据命令执行来删除过期key
5.redis 键踢出策略:
当redis内存超出物理内存限制时,内存的数据会开始和磁盘产生频繁的交换(swap),交换会让redis的性能急剧下降。所以我们是不允许redis出现交换行为的。为了限制最大使用内存,redis提供了配置参数maxmemory来限制内存超出期望大小时的处理策略。
(1) Noeviction:不踢出,当内存使用达到阈值的时候,所有引起申请内存的命令会报错(默认策略)
(2) volatile-lru:在设置了过期时间的键空间中,优先移除最近未使用的key,如果键不存在则报错
(3) volatile-ttl:在设置了过期时间的键空间中,优先移除过期时间最近的key,如果键不存在则报错
(4) volatile-random:在设置了过期时间的建空间中,随机移除个key
(5) allkeys-lru:在主键空间中,优先移除最近未使用的key
(6) allkeys-random:在主键空间中,随机移除某个key。踢出时机:每个命令执行之前(redis.c/processCommand)
6.redis 最久未使用踢出策略:LRU算法
(1) 新数据插入到链表头部
(2) 每次访问命中缓存,则将数据移动到链表头部
(3) 链表满时,移除链表尾部元素
7.redis 超时策略LRU-K
(1) 数据第一次被访问时,加入到访问历史列表
(2) 如果数据数据在访问历史列表后没有达到K次访问,则按照规则淘汰(FIFO,LRU)
(3) 当访问历史队列中的数据访问次数达到K次后,将数据索引从历史队列转移到缓存队列中,并缓存此数据,缓存队列重新按照时间排序
(4) 需要淘汰数据时,淘汰缓存队列中排在末尾的数据
8.redis哨兵
(1) 每个哨兵每秒想它所知的master、slave及其他哨兵发送ping命令,检查存活状态
(2) 如果一个实例距离最后一次有效回复ping命令时间超过预设值(down-after-milliseconds),则这个实例会被哨兵标记为主观下线
(3) 如果一个master被标记为主观下线,则正在监视这个master的所有哨兵要每秒确认一次master的确进入了主观下线状态
(4) 当有足够的哨兵在制定的时间范围内确认master的确进入了主观下线状态,则master会被标记为客观下线
(5) 一般情况下,每个哨兵会以每10秒一次的频率想它所制定的所有master、slave发送info命令
(6) 当master被哨兵标记为客观下线时,info命令的频率改为每秒一次
(7) 若没有足够数量的哨兵同意master已经下线,则master的客观下线状态会被移除
(8) 如果有足够数量的哨兵同意master已下线,则进行投票选举master,当被投slave的票数达到?n/2+1时,成为新的master
9.缓存穿透
查询一个一定不存在的数据。由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。
解决方案:
(1)布隆过滤
[1] 概念
对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃。
[2] 实现方式
将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
[3] 基本原理及要点:位数组+k个独立hash函数。
将hash函数对应的值的位数组置1,查找时如果发现所有hash函数对应位都是1说明存在,很明显这个过程并不保证查找的结果是100%正确的。同时也不支持删除一个已经插入的关键字,因为该关键字对应的位会牵动到其他的关键字。所以一个简单的改进就是counting Bloom filter,用一个counter数组代替位数组,就可以支持删除了。添加时增加计数器,删除时减少计数器。
(2) 缓存空对象
10.缓存雪崩
如果缓存集中在一段时间内失效,发生大量的缓存穿透,所有的查询都落在数据库上,造成了缓存雪崩。
解决方案:
(1) 加随机过期时间
为所有并发查询的的缓存时间添加一个额外时间随机数在1~15分钟使缓存尽量不会再同一时间失效
(2) 加锁排队
在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去loaddb,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果。
(3) 数据预热
可以通过缓存reload机制,预先去更新缓存,再即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间, 让缓存失效的时间点尽量均匀
(4) 二级缓存或双缓存
A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期。
(5) 接口限流:计数器、时间窗口、令牌等算法实现
11.CMS和G1区别
(1) CMS执行过程 是一种以获取最短回收停顿时间为目标的收集器
[1] 初始标记:标记GC Root可直接连接的对象,运行期间会停止其他用户操作
[2] 并发标记:在初始标记的基础上继续向下追溯标记,以所有已标记对象为根节点继续搜索向下标记。
[3] 并发预清理:,虚拟机查找在执行并发标记阶段新进入老年代的对象(可能会有一些对象从新生代晋升到老年代, 或者有一些对象被分配到老年代)。通过重新扫描,减少下一个阶段”重新标记”的工作,因为下一个阶段会Stop The World。
[4] 重新标记 :停止其他用户操作,收集器线程扫描在CMS堆中剩余的对象。扫描从”根对象”开始向下追溯,并处理对象关联。
[5] 并发清理 :清理垃圾对象,这个阶段收集器线程和应用程序线程并发执行。
[6] 并发重置:清理CMS栈。
(2) G1运行机制 有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。并且停顿时间可预测。
[1] 初始标记:仅标记GC Roots能直接到达的对象,并且修改TAMS的值,让下一阶段用户程序并发运行时能在正确可用的Region中创建新对象。
[2] 根区域扫描:从GC Roots开始对对已标记的引用扫描,并标记对老年代的引用。
[3] 并发标记:在整个堆中查找可访问的对象并标记。
[4] 最终标记:多线程修正在并发期间因用户程序执行而导致标记产生变化的标记。切将对象的变化记录在Remembered Set Logs里,把这里的数据合并到Remembered Set中。
[5] 筛选回收:对每个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来指定回收计划。
12. 设计模式,会的每种一个demo编写。
(1) 单例模式
[1] 懒汉式
public class Test {
// 如果下面增加 synchroinzed 代码块,此属性需要增加 volatile 修饰符,禁止指令重排序,避免线程不安全
private static Test test;
// 重点在于私有化构造方法,防止外部调用
private Test() {}
public static Test getTest() {
// 存在问题,单个if判断没有锁,在初始化并发访问时会返回多个不同对象, 线程不安全
// 解决方案,在下面if中添加 synchronized 代码块 然后在判断一次test == null才进行对象创建
// 此解决方案也有另一个名字叫做双检索
if (test == null) {
test = new Test();
}
return test;
}
}
[2] 饿汉式
public class Test {
// 类加载时进行初始化,由于是静态成员对象所以一直会保存在内存中,无法被GC回收
private static Test test = new Test();
// 重点在于私有化构造方法,防止外部调用
private Test() {}
public static Test getTest() {
return test;
}
}
[3] 枚举类,java本身支持的单例对象不会被多次加载
[4] 双检索
public class Test {
// 类加载时进行初始化,由于是静态成员对象所以一直会保存在内存中,无法被GC回收
// volatile 保证多线程内存可见性
private static volatile Test test;
// 重点在于私有化构造方法,防止外部调用
private Test() {}
public static Test getTest() {
if (test == null) {
synchronized(Test.class) {
test = = new Test();
}
}
return test;
}
}
(2) 代理模式
[1] 静态代理
// 接口类
public interface Test {
void request();
}
// 实现类
public class ConcreteSubject implements Test {
@Override
public void request() {
//业务处理逻辑
}
}
// 代理类
public class Proxy implements Test {
/**
* 需要代理的类
*/
private Test test = null;
/**
* 默认自己
*/
public Proxy() {
this.test = new Proxy();
}
public Proxy(Test subject) {
this.test = subject;
}
/**
* 实现接口方法
*/
@Override
public void request() {
this.before();
this.subject.request();
this.after();
}
/**
* 调用原方法前处理逻辑
*/
private void before() {}
/**
* 调用原方法后处理逻辑
*/
private void after() {}
}
// 调用方
public class Client {
public static void main(String[] args) {
Test subject = new ConcreteSubject();
Proxy proxy = new Proxy(subject);
proxy.request();
}
}
[2] 动态代理
// 接口类
public interface Test {
void request();
}
// 实现类
public class ConcreteSubject implements Test {
@Override
public void request() {
//业务处理逻辑
}
}
// 代理类
public class ProxyHandler implements InvocationHandler {
/**
* 目标对象
*/
private Object target;
/**
* 绑定关系,也就是关联到哪个接口(与具体的实现类绑定)的哪些方法将被调用时,执行invoke方法。
*
* @param target 绑定具体的代理实例
* @return 动态代理类实例
*/
public Object newProxyInstance(Object target) {
this.target = target;
/*
该方法用于为指定类装载器、一组接口及调用处理器生成动态代理类实例。
第一个参数指定产生代理对象的类加载器,需要将其指定为和目标对象同一个类加载器。
第二个参数要实现和目标对象一样的接口,所以只需要拿到目标对象的实现接口。
第三个参数表明这些被拦截的方法在被拦截时需要执行哪个InvocationHandler的invoke方法
根据传入的目标返回一个代理对象
*/
Object result = Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(), this);
return result;
}
/**
* 关联的这个实现类的方法被调用时将被执行。InvocationHandler接口的方法。
*
* @param proxy 代理
* @param method 原对象被调用的方法
* @param args 方法的参数
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//TODO 原对象方法调用前添加的预处理逻辑
Object ret = null;
try {
//调用目标方法
ret = method.invoke(target, args);
} catch (Exception e) {
log.error("调用{}.{}发生异常", target.getClass().getName(), method.getName(), e);
throw e;
}
//TODO 原对象方法调用后添加的后处理逻辑
return ret;
}
}
// 调用方
@Slf4j
public class Client {
public static void main(String[] args) {
log.info("开始");
ProxyHandler handler = new ProxyHandler();
Test subject = (Test) handler.newProxyInstance(new ConcreteSubject());
subject.request();
log.info("结束");
}
}
[3] 责任链模式
/**
* 全局异常处理
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 业务异常
*/
@ExceptionHandler(BusinessException.class)
public ResponseBase handleBusinessException(BusinessException ex) {
log.error(ex.getMessage(), ex);
return new ResponseBase(ex.getErrorMsg());
}
/**
* 404NotFound
*/
@ExceptionHandler(NoHandlerFoundException.class)
public ResponseBase handleNoHandlerFoundException(NoHandlerFoundException ex) {
log.error(ex.getMessage(), ex);
return new ResponseBase(ResponseEnum.NOT_FOUND_404);
}
}
// 未完待续。。。。。。
13.Lucene
简单来说Lucene主要作用为文件检索,可通过配置分词器及其配置对已有词汇和新词汇进行词语定义,它的实现原理类似于数据库,通过索引库保存原有数据位置进行字典式的查询操作。
感兴趣的同学可以下载两本电子书,自学成才:
相关的虽然面试问的不多,但是其他的一些检索相关框架都是基于它进行升级改版的,对于以后学习、设计其他框架很有帮助的说。
14.ES
(1)master选举机制
[1] 选举发起条件
① 选举由master-eligible节点发起,当满足一下条件时,触发master选举,并且该master-eligible节点的当前状态不是master。
② 该master-eligible节点通过ZenDiscovery模块的ping操作询问其已知的集群其他节点,没有任何节点连接到master。
③ 包括本节点在内,当前已有超过minimum_master_nodes个节点没有连接到master。
[2] 选举过程
① 通知所有已知节点发起对master选举
② 每个节点对自己所有已知节点进行排序,排序条件为clusterStateVersion 从大到小,数值越大代表版本号越新该值相同时进行第二次排序,取节点的id
③ 根据ID排序 从小到大, ID为第一次启动时生成的随机字符串。
④ 所有节点取排序第一位的节点并提交给master-eligible节点
[3] 选举结束
① 假设当前节点为Node_A, Node_A选Node_B当Master;
② Node_A 向 Node_B 发起join请求(投票)
③ 如果Node_B选举的不是自己则会拒绝此次join,然后Node_A重新发起选举。
④ 如果此时Node_B已经是master,那么Node_B将Node_A加入集群中。
⑤ 如果Node_B还不是master,则Node_A进入等待结果状态。
⑥ 如果Node_B活的半数以上的投票时,则B成为master,然后将自己的master node 置为自己,并对所有已知节点进行通知.
( 如果 Node_A选自己为Master,那么A则什么也不做,等待其他节点对其发起join操作。)
(2)脑裂
① 概述:
发起选举后产生了1个master节点,同时之前离线的master节点重新介入进来,导致集群不能保证集群状态一致性的情况。
② 脑裂产生场景:
当第一轮选举过后,当选的node节点一直没有成为master,其余节点重新发起投票,此时选取了另一个node节点作为masert,最终两个节点都成为了master,并想集群发布消息进行commit操作。
③ 选举如何保证不产生脑裂
通过选举周期term,每个master产生后都将分配一个选举周期,最终最大的为最新的master。
(3)倒排索引
在搜索引擎中每个文件都对应一个文件ID,文件内容被表示为一系列关键词的集合(实际上在搜索引擎索引库中,关键词也已经转换为关键词ID)。例如“文档1”经过分词,提取了20个关键词,每个关键词都会记录它在文档中的出现次数和出现位置。
得到正向索引的结构如下:
“文档1”的ID > 单词1:出现次数,出现位置列表;单词2:出现次数,出现位置列表;…………。
当用户在主页上搜索关键词 key 时,假设只存在正向索引(forward index),那么就需要扫描索引库中的所有文档,找出所有包含关键词 key 的文档,再根据打分模型进行打分,排出名次后呈现给用户。因为互联网上收录在搜索引擎中的文档的数目是个天文数字,这样的索引结构根本无法满足实时返回排名结果的要求。
注意这个打分模型,是可以自己设置调节的,在倒排索引当中,也有相应的打分模型,打分模型的打分决定了这些文档的排列先后顺序 ,也就是在那个链表中的先后位置,
所以,搜索引擎会将正向索引重新构建为倒排索引,即把文件ID对应到关键词的映射转换为关键词到文件ID的映射,每个关键词都对应着一系列的文件,这些文件中都出现这个关键词。
得到倒排索引的结构如下:
“关键词1”:“文档1”的ID,“文档2”的ID,…………。
从词的关键字,去找文档。
倒排索引(Inverted Index):倒排索引是实现“单词-文档矩阵”的一种具体存储形式,通过倒排索引,可以根据单词快速获取包含这个单词的文档列表。倒排索引主要由两个部分组成:“单词词典”和“倒排文件”。
单词词典(Lexicon):搜索引擎的通常索引单位是单词,单词词典是由文档集合中出现过的所有单词构成的字符串集合,单词词典内每条索引项记载单词本身的一些信息以及指向“倒排列表”的指针。
倒排列表(PostingList):倒排列表记载了出现过某个单词的所有文档的文档列表及单词在该文档中出现的位置信息,每条记录称为一个倒排项(Posting)。根据倒排列表,即可获知哪些文档包含某个单词。
倒排文件(Inverted File):所有单词的倒排列表往往顺序地存储在磁盘的某个文件里,这个文件即被称之为倒排文件,倒排文件是存储倒排索引的物理文件。
问题:ES 倒排索引,内部的链表以什么排序
回答: 根据打分模型的打分进行排序。每条记录有一个score 参数,带有很多小数的double类型,可以看到具体的打分,影响这个打分的有单词在这个文档中出现的位置,出现的频率次数等众多参数。
15.Quartz 分布式定时原理
16.线上如何压力测试
17.如何影子表 隔离 压力数据
18.故障演练如何做
19.Mysql事务隔离级别
20.手写有序数组二分查找
21.分布式锁的实现
22.2pc,xa协议
23.mysql是如何做crash-safe的
24.ThreadLocal的使用场景
25.redis数据结构
26.juc下面公平非公平锁的实现区别?
27.spring循环依赖
28.各种分布式算法场景等
1.分布式一致性算法
2.投票选举算法
29.限流算法
30.熔断处理
31.降级处理
有几个涉及到公司项目相关的问题及设计方案就不写了