一、八股文
1.一次es查询发生的事情
答:master接收到查询请求,根据负载均衡策略,并结合路由字段将查询请求转发到指定分片上,如果未指定路由字段则转发到所有索引分片(主分片和副本分片择一转发),分片上执行查询语句,从倒排索引上找到相应的document id, 然后所有分片将查询结果返回到master节点上做合并操作,拿到最终结果,最后根据文档id“回表”获取到全部文档。
注意:倒排索引上可以设置是否包含该字段值,类似于覆盖索引,这时候要判断是从倒排索引上取值更快,还是回表查询更快,这个地方涉及到一个考点:I/O次数,回表查询是一次I/O,从多个索引上取值也是多次I/O,哪种方案更快取决于I/O的次数,I/O次数少的方案更快。
2.tair如何实现的分布式锁,中间有哪些问题?
2.1 实现逻辑:
答:
实现方式:利用SETNX原子指令,利用可以唯一确定一条记录的业务key组装成唯一key,利用SETNX向唯一key上赋值,如果能赋值成功则认为获取锁成功。
2.2 如果获取到锁的线程宕掉了,没释放掉锁怎么办?
答:对tair缓存的唯一key设置超时时间,比如10秒,如果超过10秒线程还未释放锁,则自动释放。
2.3 设置超时时间,如果线程执行慢了,超过了超时时间怎么办?
答:利用看门狗机制,额外创建一个观察线程,如果执行线程快到超时时间仍未执行完毕,则主动延长超时时间。
2.4 如何释放锁?
答:tair.del(key),删除缓存的key。
2.5 目前有A、B两个线程,如何防止B线程误释放掉A线程加的锁?
答:SETNX给唯一key赋值时,value设置为当前线程id,释放锁时检验当前value是否为当前线程id,如果不是则抛异常,如果是则删除key。check和删除这两个操作需要保证在一个事务内,实现事务可以使用lua脚本的方式,将check和删除操作写入同一个lua脚本中, tair执行lua脚本是原子性的,从而保证事务。
2.6 tair是否会有单点故障?
答:会有单点问题,要解决单点问题可以使用REDLOCK或者REDISSION这种基于redis的分布式锁实现框架,他们利用集群的方式解决单点问题,但上面两个框架依然没办法完全保证锁的互斥性,极端情况下还是会出现不一致问题(Redlock在主从切换的瞬间获取到节点的锁),如果要求绝对一致性可以选择zookeeper或者数据库事务来保证。
2.7 除了自己实现分布式锁, 现在外界有现成的分布式锁框架吗?
答:有的,如上面提到的redlock和redission
2.8 分布式锁唤醒等待线程会造成羊群效应,如何解决?
答:维护一个有序队列,每次只唤醒队列的前3个线程。
3. redis用过吗?
3.1 redis有哪些数据结构?
答:redis有五种基本的数据结构:string、list、hash、set、zset。
3.2 zset底层是用什么数据结构实现的,插入和查询的时间复杂度是多少?
答:节点数量和每个节点占用空间较小时,zset底层是用ziplist实现的,当数据量较大时zset底层是通过跳表和散列表实现的,散列表底层是数组,key是member,value是score。按score插入和查找的时间复杂度都是o(logn)。
3.3 redis里的string底层是怎么实现的?
答:SDS数组结构,底层为数据加一些基本属性:
struct sdshdr{ int len;//记录buf数组中已使用字节的数量 int free; //记录 buf 数组中未使用字节的数量 char buf[];//字符数组,用于保存字符串 }
3.4 redis为什么快呢?
答:
- redis是基于内存的,支持随机读写,读写复杂度为O(1),
- redis是单线程的,没有多线程之间的切换和资源的同步、锁的竞争等耗时场景。
- redis支持的数据结构简洁,专门定制的高效的数据结构,例如压缩列表。
- 基于I/O多路复用,同步非阻塞I/O。
- 底层定制VM,零拷贝技术(MMap)减少操作系统内核态与用户态的转换。
3.5 零拷贝技术有哪些实现呢?
答:
- DMA+文件句柄拷贝(kafka)
- MMAP
3.6 redis实现一个滑动窗口可以使用什么样的数据结构?
答:
zset,利用score排序,score递增来实现窗口的滑动。
3.7 redis set结构是如何扩容的?
答:渐进式rehash,新增数据在新数组,删改查在老数组,然后根据服务器空闲状态,批量的进行数据迁移。
4.数据库知识
4.1MySQL数据库锁有哪些呢?
答:
有乐观锁和悲观锁两大类:乐观锁有mvcc,悲观锁有全局锁、表锁、行锁、间隙锁、意向锁等。另外还有排他锁和共享锁这种概念上的分类。
4.2 数据库事务有哪些特性?MySQL是通过什么方式来实现这些特性的呢?
答:事务有四大特性:原子性、隔离性、持久性、一致性。MySQL利用undo log 实现原子性,利用redo log实现持久性和一致性,利用mvcc实现隔离性。
4.3 数据库事务有哪些隔离级别?
答:有四个隔离级别,读未提交、读已提交、可重复读、串行。读未提交会有脏读问题,读已提交不能重复读,可重复读级别有幻读问题。
4.4 mvcc能完全解决幻读吗?
答:只靠mvcc是不能完全解决幻读的,还必须配合数据库的记录锁和间隙锁,才能完全实现可重复度。
如何加间隙锁? SELECT FOR UPDATE
如何加记录锁(共享锁模式)? SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE
4.5 mvcc是怎么实现的呢?
答:在记录上增加两个版本号字段,查找记录时利用版本号来筛选记录,从而实现快照读, 具体实现细节是:在聚簇索引上增加隐藏字段:当前事务id、指向undo log的指针,在undo log中记录版本链,利用自己事务id到版本链中查找符合自己版本要求的数据,从而实现读视图。
4.6 MySQL行锁是如何实现的呢?
通过给索引上的索引项加锁来实现的
4.7 什么时候会出现表锁,什么时候出现行锁?
答:更新数据时未使用索引筛选数据,或者表小,更新的数据占表大部分这时候会用到表锁。如果更新的是特定的记录,比如id = 5,这个时候会用到行锁。
4.8 上面提到redo log,MySQL是如何保证redo log和bin log的一致性?
答:通过分布式事务来保证的,因为redo log是存储引擎层记录的日志,bin log是数据库服务层记录的日志,这两种日志分别属于不同的功能模块,要保证不能功能模块之间数据的一致性只能通过分布式事务,MySQL利用两段式提交的方式来实现分布式事务,先写redo log生成事务标记Xid并设置状态为prepare,写入binlog,进行事务提交,将Xid对应的redo log状态设置为commit。如果中间binlog写入失败会去更新redo log状态用于数据恢复。
4.9 MySQL常用的存储引擎有哪些?
答:有innodb、myisam、memory、CSV等
4.10 innodb底层的数据结构是什么?
答:是B+ 树,B+ 树是一种多路查找树,同一层节点有序,兄弟叶子节点之间有双向链表,支持范围查找。非叶子存储的索引数据不存储实际数据,叶子节点存储实际数据地址。B+ 树和B树的区别是:B+树所有查询必须走到叶子节点,B树走到非叶子就能返回,B+树利用兄弟节点的双向链表支持高效的范围查询,B+树非叶子不存储数据所以一页内存可以放更多节点,减少磁盘IO。
4.11 什么是聚簇索引?
答:在同一个结构中保存索引值和数据行,并且相邻键值紧凑的存储在一起的索引结构。
4.12 MySQL写入一条数据的过程
Mysql专栏(二)Innodb数据写入过程_lvqinglou的博客-CSDN博客_mysql数据写入流程
答:
4.13 UUID适合做MySQL的主键id吗?为什么
答:UUID不适合做主键id,因为:
- UUID要占128位,占用太多存储空间,相比于long型字段,一页内存只能放下50%的数据,这就导致两倍的磁盘I/O,查询性能低。
- UUID是无序的,这数据写入时需要随机寻址,随机寻址对于磁盘来说是性能最低的,另外无序也会导致索引树的结构剧烈变动,不断的调整节点位置,也会导致多次I/O从而降低性能。
- 信息不安全:基于MAC地址生成UUID的算法可能会造成MAC地址泄露。
4.14 MySQL如何开启死锁检测? 底层实现原理是什么?线上如何排查死锁?
答:
innodb_deadlock_detect
=on 这个语句可以开启死锁检测,死锁检测的原理是构建一个资源的等待图,然后深度优先搜索该图,如果图中存在环则任务存在死锁。线上排查死锁的方法:select * from information_schema.innodb_trx lock waits和 show innodb status语句可以打印出当前事务的执行状态,通过分析这些信息可以得出死锁卡在哪一点上。
4.15 MySQL SELECT语句使用的两个字段都有索引,存储引擎如何选择使用哪一个呢?
答:查询优化器根据需要扫描的行数,是否使用临时表,是否排序等因素来选择具体使用的索引。优化器统计扫描行数的方法,统计索引基数,利用采样查几个页然后用平均值推算出总体数据量,选择扫描行数少的索引。如果order by 后的字段是索引,那么会选择这个索引,因为查询出的数据直接是有序的,不需要额外排序了。另外就是查询过程是否使用到临时表等场景。
4.16 MySQL 深翻页对性能有什么影响?如何解决?
答:深翻页会降低数据库性能,因为数据库会把前面所有的数据拉出来,然后再丢弃前n- 1页数据,最终返回第n页的数据,这个过程涉及到多次索引页的I/O,所以性能低。解决深翻页有多种方式:
- 每次查询记录上一次查询结果的最大id,然后利用这个id来筛选数据,减少页数。
- 单独建一张表,这种表里存储数据页跟id的关系,分页查询时先查关系表拿到id范围,然后再查数据
延迟关联inner join,利用覆盖索引来缩小数据范围。
4.17 MySQL binlog有哪几种格式?
MySQL的binlog有有几种录入格式?分别有什么区别?_疯狂的毛毛虫哟的博客-CSDN博客
答:MySQL binlog有三种模式:statement、row、mixed,mixed是前两种的混合。statement记录是sql语句本身,row保存哪条记录被修改,记录单元为每一行的改动(两个event:Table_map和Delete_rows)。 statement模式会有数据不一致的风险,delete from t where a>=4 and u_time<='2021-11-15' limit 1例如这个语句如果主库和从库在索引字段选择上出现差异,删除的数据就会不一致。row能解决数据不一致的问题,但是保存的信息太多,日志量太大耗费IO。所以出现了mixed模式,MySQL会自动判断如果statement会造成不一致时再使用row模式。
4.18 分库分表有什么坏处?
答:
- 分库分表后需要分布式事务来支撑以前单库单表事务的场景。
- 只能使用分表键查询,不再支持范围查询、join等联表查询,多表数据之间排序操作失效。
- 不能再使用表自增id做幂等,需要自己生成全局唯一id。
- 后期扩容麻烦。
5. Java基础知识
5.1 解释下什么是双亲委托模型?
答:类在加载时,系统会判断当前类是否已加载,如果已加载则直接返回,如果未加载则尝试加载,在加载时,当前系统会把类加载任务委托给其父类loadClass方法进行处理,整个过程是一个递归处理的过程,最终都会交给顶层BootstrapClassLoader 加载器来加载类,之前父类无法加载相应类时才交给下一层子类来加载。
5.2 为什么要用这种方式来加载类呢?
答:利用这种方式保证Java核心类正确加载,防止用户私自篡改Java核心类。
5.3 有办法打破这种委托加载模型吗?
答:有的,
- 自己定义一个类加载器,重写里面的loadClass()方法。
- 在用户线程里面调用java.lang.Thread类的setContext-ClassLoader()方法,利用线程上下文类加载器来加载类。
- OSGi自定义了类加载器,当需要动态地更换一个模块的时候,就把模块连通这个模块的类加载器一起替换,从而实现了热替换
5.4 Java反射是什么含义?有哪些应用场景?
答:在运行时,任何一个类都能知道自身所有的属性和方法,将相应的字节码映射为可执行的方法和可访问的属性。应用场景有:开发通用框架、动态代理、自定义注解。
5.5 currenthashmap底层实现
答:JDK 1.7版本currenthashmap是通过分段的数组+链表实现的,整个数组分为16个段,段继承自Retrantlock,实现多线程之间修改的互斥,利用分段锁提高并发度。JDK1.8 修改了currenthashmap的底层实现,使用node数组+链表,并结合Synchronized 和CAS来保证线程安全。
5.6 CAS是什么?
答:CAS是 compare and set的缩写,是一种无锁算法,可以在不加锁的情况下实现线程间同步,不会使线程阻塞。CAS涉及到三个操作,需要操作的内存V,要比较的值A,要设置的值B,当且仅当V处的值等于A时,才将V处设置为新值B,失败后通过自旋来重试。
5.7 CAS有什么问题呢?
答:
- ABA问题。
- 自旋消耗CPU资源。
- 只能保证单个变量操作的原子性,无法保证多个变量操作的原子性。
5.8 CAS底层是如何实现的,如何保证指令的原子性?
答:有两种方式:锁总线和锁内存
5.9 synchronized和lock的区别
答:
- 使用方法上,synchronized锁的是对象,lock可以锁语句,synchronized无需手动释放做,lock需要手动释放锁,通常在finally语句里调用releas()方法,另外lock还支持多种类业务场景的锁:比如读写锁、公平锁、非公平锁等等。
- 底层实现上,synchronized是Java关键字,利用Java对象头里面的mark down相关字段来实现加锁和释放锁,支持无锁、偏向锁、轻量级锁、重型锁等类型,重型锁是利用操作系统的mutex锁来实现线程阻塞的。lock是API层面的锁,底层是对AQS(抽象队列同步器)的实现,利用额外的字段(state)、双向链表和CAS实现线程之间的同步。
- 是否支持中断:synchronized不支持中断,lock支持中断。
5.10 Java虚拟机内存分区
答:分为堆区、栈区、方法区(元数据区)、本地方法栈、程序计数器。
5.11 有哪些区会产生内存溢出的情况呢?
答:堆区、栈区、方法区、本地方法栈都有可能出现内存溢出的情况,堆区new 的对象太多放不下会出现OutOfMemoryError,栈区深度过深会出现StackOverflowError,如果栈在动态拓展(自动扩大栈的深度)时无法申请到更多地址,则抛出OutOfMemoryError。本地方法栈与栈区类似。方法区如果使用动态代理在运行时产生了太多类,也会出现OutOfMemoryError。
5.12 垃圾回收机制。
答:JVM垃圾回收总体上是分代回收的机制,在新生代,将内存分为Eden区和Survivor区,survivor区又分为from和to区,每次新生代垃圾回收时会将Eden区存活的对象拷贝到from区,然后把Eden区全部回收,在拷贝过程中如果出现from区放不下的大对象,该对象直接晋升到老年代,另外在新对象第一次分配内存空间时,如果对象大小超过一定阈值也会直接在老年代分配空间,这个阈值是根据新生代垃圾回收过程中晋升的大对象得出的值 。
5.13 你们项目中使用了哪个垃圾回收器?
答:使用了CMS。
5.14 能说下CMS的执行过程吗?其中哪几个环节是stop the world的?
答:CMS总体上可以分为四个步骤
- 初始标记:只标记GC ROOT直接关联的对象,是STW。
- 并发标记:与用户线程并行,标记引用链能到达的对象。
- 重新标记:修订并发标记过程发生变更的对象是 STW。
- 并发收集:与用户线程并行,清除垃圾对象。
5.15 CMS有什么优点,又有什么缺点呢?
答:优点是能利用多核处理器的优势减小停顿时间。缺点是采用标记清除的垃圾回收算法,会产生内存碎片。 后来出现的G1垃圾回收器采用标记整理算法,可以解决内存碎片问题,G1是 Garbage First的缩写。
5.16 JVM参数如何设置?
答:
- 首先是要控制内存的大小,每一个区域都要设置上限,来避免溢出,比如元数据区。
- 通常堆区设计为整个机器内存的2/3。
- 选择合适的垃圾回收算法,CMS、G1等等。
- 参数调整,比如根据自身业务场景调整年轻代和老年代的比例。
- 依据系统容量、访问延迟、吞吐量等进行专项优化, 比如服务是高并发的,对 STW 的时间敏感就要有针对性的优化。
- 记录详细的 GC 日志,来找到这个瓶颈点,借用 GCeasy 这样的日志分析工具,定位问题。
6.spring相关知识
6.1 spring IOC注入类的步骤,以及bean的生命周期
答:
- bean实例化
- 注入对象属性(XML文件中、注解中配置的相关属性值)
- BeanPostProcessor前置处理
- Initialization初始化
- BeanPostProcessor后置处理
- 注册回调方法
- bean使用
- bean销毁
6.2 spring 是如何解决循环依赖的?
答:spring利用三级缓存机制来解决循环依赖问题,bean完成实例化之后提前暴露于三级缓存,完成简单属性赋值的bean升级放入二级缓存,完成创建的bean放入一级缓存。
6.3 spring AOP是如何实现的?
答:AOP是基于动态代理的,有两种实现方式:JDK的proxy和基于asm框架字节流的Cglib。如果要代理的对象实现了代理的接口,这时候就会用proxy去创建一个代理对象,如果没有实现代理的接口则会使用Cglib创建一个代理类的子类来作为代理。
6.4 spring是如何实现事务的?
答:spring事务是依靠AOP实现的,生成代理对象之后执行如下步骤:
- 解析各个方法上配置的属性,根据属性判断是否开启事务。
- 如果开启事务,这时获取数据库链接,关闭自动提交功能,开启事务。
- 处理过程中如果需要回滚,则进入completeTransactionAfterThrowing方法,调用doRollBack实现回滚。
- 完成处理后通过commitTransactionAfterReturing调用doCommit方法实现事务的提交。
- 事务执行完毕之后清除cleanupTransationInfo等事务信息。
6.5 spring如何动态加载类?
答:直接拿取spring的ClassLoader来加载需要的类或者 Instrumentation方法。
7.多线程了解吗?
7.1 Java定义一个线程池有哪些参数?
答:有如下参数,
- 核心线程数
- 最大线程数
- 线程工厂(用于给新创建的线程起一个有意义的名字)
- 等待队列(有基于数组的有界队列,有基于链表的无界队列还有优先级队列)
- 拒绝策略(线程池和等待队列都满之后如果处理?直接抛异常、直接丢弃、丢失队列中中最近元素、利用当前线程执行任务等等策略)
7.2 线程池处理流程
答:
判断核心线程是否有空闲线程,如果有空闲线程则直接执行任务,否则进入下一流程。
判断等待队列是否已满,如果未满则将请求放入等待队列中,如果已满则进入下一流程。
线程池判断当前线程数是否已达到最大线程数,如果未达到则申请一个新线程执行任务,如果已达到最大线程数则根据拒绝策略来操作请求。
7.3 为什么先把请求放队列,而不是直接创建新线程呢?
答:线程池创建新线程都需要获取一个全局锁,这个全局锁是系统性能的瓶颈,所以线程池需要尽可能避免全局锁。
7.4 为什么不鼓励使用FixedThreadPool?
答:FixedThreadPool的等待队列是基于链表的无界队列,无界队列不限制等待的请求个数,会造成内存溢出。
7.5 线程有哪些状态?
答:线程有6种状态:new、runable、waiting、time_waiting、blocked、terminat。
7.6 synchronize和lock获取锁失败后,线程的状态各自是什么?
答:synchronize获取锁失败后,当前线程的状态是blocked,lock获取锁失败后线程状态是waiting,lock获取锁失败后会调用LockSupport.park()方法让线程进入waiting状态,释放cpu资源。
7.7 线程池预热调用哪个方法呢?
答:调用线程池的prestartAllCoreThreads()方法会提前创建并启动所有基本线程(core size)。
8.工作中用到es了吗?
8.1 es插入的数据为什么1秒钟之后才可见?
答:新写入的数据会先写入内存buffer,buffer内数据每秒刷新(refresh)到文件缓存中(cache),刷新后形成段,这时段才会打开并开放查询,所以es数据1秒之后才可见。
8.2 es索引可以设置为包含文档 字段,如何选择使用倒排索引上的文档值还是直接用文档id反查?
答:是否直接使用倒排索引上的值取决于要查询的文档列的数目,字段个数较多查询倒排索引涉及到的I/O跟回表查询的I/O还多,那不如直接使用文档id反查拿出整个文档。
8.3 能描述下es查询的过程吗?
答:分为两种情况使用文档id查询和其他字段查询,利用文档id查询时,请求会先发往master节点,然后master节点根据负载均衡策略路由到相应的分片(主分片和副本分片中的一个),然后从磁盘上获取文档记录返回给master节点。当用其他字段查询时,如果设置了路由字段,则将请求转发到指定分片,如果未指定路由字段则转发到所有分片(主分片和副本分片选择其中一个),各个分片执行查询获取到相应的文档id返回到master节点做合并和排序操作,选择出需要的结果集之后再用文档id反查出所有文档记录返回给客户端。
9.kafka用过吗?
9.1 能简单介绍下kafka功能模块的划分吗?
答:kafka主要包括:producer、broker、consumer三个部分。producer用于消息的发送,broker用来存储消息,consumer则提供消息的消费管理。
9.2 为什么kafka性能高呢?
答:kafka在每个功能模块都做了性能优化,
- producer侧通过消息压碎、批量发送减少消息发送时间。
- broker侧,通过分区并行处理消息,磁盘顺序读写、零拷贝。
- consumer侧,消费组的方式进行高效的消息管理。
9.3 kafka如何保证消息不丢失的?
答:
- producer侧通过ack机制,每一条消息发送到broker侧后返回一个ack。
- broker侧利用副本分区的方式来保证数据不丢失,broker侧还会维护一个ISR列表,利用高水位的方式控制副本分区和主分区之间数据的差异。
- consumer侧在zookeeper 上保存消息的offset,当消息真正消费完成后在提交offset,通过zookeeper来保证消费的消息的一致性。
9.4 kafka 文件结构
kafka消息存储机制和原理_像精灵一样思考的博客-CSDN博客_卡夫卡消息队列原理,保存消息方式是什么?
答:kafka共有三种文件:日志文件、索引文件和日期索引文件,kafka采用分片和索引机制来提升文件管理效率,kafka会按partition维度生成文件,每个partition又会生成多个segement,文件名是用第一行记录的offset来命名的。
9.5 kakfa 副本如何同步日志
答:leader分片和各个副本分片都各自维护一个水位,记录当前已存储的日志的offset,当leader分片有最新offset产生时会通知副本分片,副本分片利用自身维护的offset到leader分片上来拉取要同步的日志内容,写入到副本分片上的日志文件中,当没有最新offset产生时,副本分片的同步线程阻塞。
10.限流有了解吗?
10.1 常见的限流策略有哪些?
限流算法-常见的4种限流算法_billgates_wanbin的博客-CSDN博客_限流算法
答:常见的限流策略如下:
- 计数限流法:保存一个计数器,处理了一个请求,计数器加一,一个请求处理完毕之后计数器减一。每次请求来的时候看看计数器的值,如果超过阈值直接拒绝。优点是实现简单,缺点是流量可能瞬间出现,击垮系统(比如阈值定义 1000,流量在1秒内全部占满)。
固定窗口限流算法:先维护一个计数器,将单位时间段当做一个窗口,计数器记录这个窗口接收请求的次数。一段时间内(不超过时间窗口)系统服务不可用,窗口切换时可能会产生两倍于阈值流量的请求。
滑动窗口限流:
滑动窗口限流解决固定窗口临界值的问题,可以保证在任意时间窗口内都不会超过阈值。相对于固定窗口,滑动窗口除了需要引入计数器之外还需要记录时间窗口内每个请求到达的时间点,因此对内存的占用会比较多。无法解决短时间之内集中流量的突击。
漏桶算法:往漏桶中以任意速率流入水,以固定的速率流出水。当水超过桶的容量时,会被溢出,也就是被丢弃。因为桶容量是不变的,保证了整体的速率,类似于消息队列。
令牌桶算法:有一个令牌管理员,根据限流大小,定速往令牌桶里放令牌。如果令牌数量满了,超过令牌桶容量的限制,那就丢弃。系统在接受到一个用户请求时,都会先去令牌桶要一个令牌。如果拿到令牌,那么就处理这个请求的业务逻辑。拿不到令牌,就直接拒绝这个请求