3.1 二面如何自我介绍

自我介绍

  • 二面一般是你未来的leader

要点

  • 简短有力3-4分钟较好
  • 内容要有结构性:经历简介、项目经历,技术总结
  • 凸显能力:技术基础能力扎实,喜欢探究原理

    3.2 Java语言基础

    3.2.1 Java基础数据结构

image.png

TreeSet与比较大小方法有关,LinkedList的顺序是跟保存顺序有关。

3.2.2 ArrayList和LinkedList区别

ArrayList是使用数组进行实现,使用一块固定大小的、连续的内存区域,查找非常快。插入和删除需要移动元素,效率较低。
LinkedList通过链表去查询,相对比查找速度不如ArrayList,插入和删除只需要做指针指向改变。

查找来说数组,查找时间复杂度O(1),插入最坏时间复杂度为O(n)
链表进行查找,查找的最坏时间复杂度为O(n),插入的时间复杂度为O(1)

3.2.3 Hashmap的内存结构,ConcurrentHashMap的加锁力度

JDK1.7 HashMap 数组+链表,并且采用头插法
image.png

JDK1.8 链表节点个数大于等于8,会变成红黑树,小于6则会转化为链表,为什么节点是8,红黑树至少有三层,查找效率高于链表,低于三层和链表效率差不都,反而还要维护红黑树。

HashMap是线程不安全的,JDK1.8之前,ConcurrentHashMap是使用segment的锁,比如说有六个槽位,会以三个槽位为一组做一个segment锁,操作任何一个槽位都会获取这个锁,JDK1.8之后, 采用槽位做管理,node做管理,判断当前要操作的槽位加了锁,就等待所释放之后在操作,不同槽位不会产生影响,把加锁粒度控制到最小。

因为HashMap插入是无序的,LinkedHashMap可以保证有序,原理就是用一个链表记录插入顺序,

image.png
treeMap本质上是一个红黑树结构,可以进行排序,内部是树形结构。

HashSet内部就是一个HashMap,HashMap的add方法,如果map中存在这个key,如果存在则返回false,不存在则作为key put到map里。

LinkedHashSet也是包装了HashMap。

3.3 多线程

3.3.1 线程池的核心参数及结构

ThreadPoolExecutor:

  • coreSize:核心线程数
  • maxSize:最大线程数
  • queueSize:等待队列数
  • keepAliveTime:空闲线程多久被销毁
  • handler:拒绝策略,丢弃/丢 弃抛异常/调用线程处理/丢弃最前面的任务然后尝试重新执行

启动线程池,创建之初就会创建核心数的线程,也叫最小线程数,及时队列是空的,也会保持这个线程数量,当任务数量小于核心线程数,核心线程数完全可以处理这些任务,但是,如果超过核心线程数的任务进来,就会优先放入到等待线程数,如果等待队列也满了,则会去创建线程直到最大线程数,达到最大线程数队列也满了就会执行拒绝策略,可以选择去丢弃或者抛异常,或者由调用线程处理。大于核心线程数的线程数,空闲多久被销毁keepAliveTime。

3.3.2 synchronized和ReentrantLock

synchronized和ReentrantLock,synchronized是关键字。
ReentrantLock等待可以中断,synchronized则不可以。
ReentrantLock支持公平锁。
ReentrantLock可以更细粒度的控制对象。

3.3.3 三种锁

重量级锁
轻量级锁
偏向锁

synchronized早期的原理是重量级锁,性能低下,会频繁切换用户态和内核态,在最新的jdk版本,它的性能,几乎和ReentrantLock差不多,主要就是引入了以上三种锁。

偏向锁,它是有偏向性的,当锁对象被第一个线程竞争的时候,会用CAS比较是否有其他线程设置了偏向锁,没有的话,则会设置上线程号,如果另一个线程来请求偏向锁,发现偏向锁已经被其他线程所占用,就会升级为轻量级锁,轻量级锁和重量级锁的区别就是不会频繁切换到内核态,而是选择自旋操作,不断地去CAS操作,去获取轻量级锁,自旋锁是轻量级,不过自旋锁会大量消耗CPU资源,会给自旋设置周期,会进行一个自动的控制,可以自动进行调整,如果之前自旋锁竞争的条件是非常苛刻的,则会减少自旋的次数,改为重量级锁,如果很快就能获得,就会增加自旋的次数,如果达到自旋次数会升级为重量级锁。

3.3.4 CopyOnWrite容器

读快照
写副本并替换

CopyOnWrite是一个写时复制的功能,我们会在容器内部创建一个写副本,并且写完副本之后,进行数据替换,如果写的时候读到的是旧的数据,相当于写快照,实现在写的时候不影响读。

面试经常问的一个问题,如果并发发生了两次写操作,会创建两个副本,如果发生了并发写,写操作会加锁的,同一时间,只有一个写操作,需要等到上一次写操作结束,在进行写操作,读操作是并行的,写操作是串行的。

3.3.5 volatile关键字

多线程可见
防止指令重排序
单例书写中会用volatile,线程所读取的一个变量是处于内存当中的,而做内存的写操作是会把内存当中的数据发送到CPU的指令寄存器上,在指令寄存器上做写操作,写完之后再同步到对应的内存当中,那么每个线程读取变量的栈地址,内存的区域是不同的,因此变量不设置volatile关键字,每个线程看到的内存区域属于自己栈空间的一个副本,没有办法做到一个写操作,另一个线程立即可见的状态,因此多线程变量的修改是要加volatile关键字的,同时volatile能够防止出现指令重排,来确保读操作在代码维度必定发生在写操作之后,来做流程控制。

3.4 Java IO

BIO:阻塞IO,会持续阻塞住,同步阻塞式IO
NIO:同步非阻塞IO
AIO:异步非阻塞IO

BIO,recv会一直被阻塞住,直到有消息。
image.png

NIO,引入了select机制,select可以去管理server socket句柄,平时情况下,对应的select操作会被阻塞住,会处于一个等待的状态,当client发送数据,就会被唤醒,会告诉server socket再去调用recv方法。

同步就是因为要等待send消息,本质是监听器模式。

AIO,在server socket上注册一个回调函数,
socket.addHandler(“recv”, function() {
send(“Hello too”)
)

BIO是同步阻塞,NIO是同步的,但是无需阻塞,AIO是异步非阻塞,通过回调函数向内核态注册了一个回调,AIO使用难度很高,使用最广泛的事NIO。

3.5 数据库基础能力

3.5.1 不同读写方式

读快照
当前读

什么是快照读
select,mysql本身采用MVCC来保证快照读,读取在事务开启时的数据快照
什么是当前读
如for update,就开启了当前读,就会读取当前最新的内容,如果事务比较长,另一个事务会被阻塞直到事务提交锁释放。
update delete 都是当前读
insert不是当前读,通过gap锁来保证。

image.png

3.5.2 mysql锁机制

当前读本质上就是通过数据的锁来保证。
当前读的锁机制

  • 行锁
  • 表锁
  • 间隙锁

image.png

行锁:比如在主键列上做当前读,就会加行锁,如果当前读作用在一个普通索引上,如c字段,当前条件是where c=20;就会锁住两行。

表锁:在无索引条件下使用当前读,如查询d字段,没有办法在索引级别去控制锁的粒度,会将锁机制会降级为表锁,会将所有表所有行锁住,本质是没有索引去帮助锁行。

间隙锁(会发生在RR级别及以上):一般会发生在普通索引上,假设做了一个当前读的操作,select where c =6;于是就会锁定区间,会锁定(5, 10],左开右闭,通过两个索引之间区间来做间隙锁。因此update有间隙锁的时,mysql没办法再次接受对应区间的insert操作的,也就是说,insert操作在遇到间隙锁的间隙内容当中,会被block住,但是,间隙锁,对其他行锁定记录不影响,仍然可以去做其他当前读操作,间隙锁只会对insert操作有影响。

面试官一般会问,当一个对应操作是c=10,除了在c=10这条记录上面加上行锁,还会有别的操作吗?由于c本身只是一个普通索引,会锁定c=10的行,除此之外,在RR级别,会在前后区间都会被锁定住,加上间隙锁,这里锁定的原因,就是c=10,无法保证其他事务插入c=10的新纪录,最保险的方式就是锁住间隙。

如果查询c字段范围6-8,则5-10空间内,都会被加上间隙锁。

这个就是当前读会遇到的一些锁机制,间隙锁上会查询不存在的记录,然后在插入会出现性能问题,所以强烈推荐唯一索引,就不会出现间隙锁,都会退化成行锁。

3.4.3 事务隔离级别

事务隔离做不好会产生

  • 脏读:读到还未提交的内容
  • 不可重复读:两次读到更新后的数据
  • 幻读:两次读读到新增数据

事务隔离级别

  • 读未提交:脏读,不可重复读,幻读
  • 读已提交:不可重复读,幻读
  • 可重复读:幻读(mysql可解决(mvcc+gap锁))
  • 串行化

在可重复读中,如果解决脏读、不可重复读、幻读。

快照读、当前读
脏读的解决:
事务2没有提交
快照读:事务1快照读,读的还是修改前的数据1.
当前读:事务2当前读,则会被阻塞住,等待事务2提交或者回滚,才能获取最新的内容。

不可重复读的解决:
所以的事务隔离级别都要保证,都是当前读或者都是快照读,否则没办法保证食物隔离级别的。

快照读:事务1快照读,第一次读取的数据,即使事务2修改数据提交,读取到的依然是第一次快照读读到的数据。如果说,快照读第一次读取事务2已经提交了,那么就直接读到了最新数据。

当前读:其实理解很简单,事务1必须等到事务2将锁释放才能读到最新的数据,否则会一直被阻塞住,本质上就是串行化,当前读是最能保证事务隔离性。

幻读:
快照读,如果事务读取的是id=1,对应记录不存在,事务2去insert一个id= 1的记录,仍然mvcc产生效果,即使快照读也会由mvcc保证不会产生幻读。
当前读:如果是当前读则间隙锁起到了作用,由于读取了id=1 for update如果id不存在,读不到这个记录,如果事务2进行insert id = 1,会等到事务1提交或者回滚,insert操作才能成功,间隙锁是来控制插入顺序化的。

3.4.4 MySQL索引结构

索引类型分类

B+树,叶子节点的高度不会超过1,节点存在叶子节点上,叶子节点会有指针顺序连接起来,这么做的好处在于,在于矮胖的特性,从根节点查询叶子节点数据,几乎路径是一样的,性能非常优秀,查询效率稳定。
image.png

Hash索引
无法做范围查询,无法排序。

索引形态分类

聚簇索引
非聚簇索引

聚簇索引:索引保存了所有行记录。
image.png

非聚簇索引:存储的是数据行的指针
image.png

非聚簇索引效率更低,聚簇索引效率更高,聚簇索引一般只有一个,非聚簇索引可以有很多个。往往不会无限制添加非聚簇索引,因为增删改查会维护b+树,一般不超过13个非聚簇索引,有些DBA要求不超过5个。

非聚簇索引比没有添加索引高效很多比磁盘上查询。

3.4.5 索引优化

  • 经常被查询的区分度高的列做索引
  • 最左原则
  • 回盘排序
  • 覆盖索引
  • 小表驱动大表.

区分度高:值的可能性很多,比如说,只是男和女两种可能就是区分度低,查询近乎降级为全表扫描。

最左原则:

image.png
image.png
回盘排序原则:
where c = 20 order by d,就会出问题,必须要从通过c列找到数据本身,在磁盘找到数据,然后做一个磁盘内存级别的排序,如果d列内容是不一样的,就会进行磁盘的排序。

往往我们会将c和d进行联合索引,并且要和联合索引顺序保持一致。
image.png

3.4.6 覆盖索引

如果查询的字段都包含在索引内,比如说(a,b,c)联合索引,直接查b按理来说不会走索引,但是如果select a,b,c这些数据都在索引中,就会走索引,无需全表扫描,这就是索引覆盖。来避免回表,避免查询聚簇索引,提高非聚簇索引性能。

3.4.7 小表驱动大表

关于join或者子查询,我们往往会对一些sql语句进行inner join、left join、right join 或者使用sub query,将子表内容带到主表上,去查询主表的内容,无论如何都要记住一个原则,小表驱动大表,我们做对应子查询的时候,一定要使得子查询的内容过滤掉大部分的行记录,然后再讲对应的内容,带到主查询当中,join操作也是一样,我们需要再join之后的where条件上面去将小表的内容尽可能的过滤出来,以此来驱动大表的结果。

3.4.8 索引调优 explain

explain执行type字段出现的结果,表示的含义:

  • system: 仅一行
  • const:主键or唯一键的常量等值查询
  • eq_ref:主键or唯一键的扫描或关联查询
  • ref:非唯一索弓|的常量等值查询
  • range: 索引的范围查询
  • index: 索引全查询(覆盖索引就走这个)
  • all: 遍历表查询

优化到至少range范围

3.6 缓存redis基础能力

3.6.1 数据结构

  • string
  • hash
  • list
  • set
  • zset

hash的好处,可以指定存取部分内容,string必须要取回所有内容。
list基于链表去实现的。

zset的结构?跳表+压缩表

超过了128节点会将要压缩表(链表),会变为跳表。建立了一个跳表,数据越多跳跃越大,空间换换时间,去找跳表,然后确定区间范围。

image.png

3.6.2 数据持久化

  • rdb
  • aof

总是上来说,Redis借助内存以及单线程模型去解决效率问题,但是由于市面上经常会把Redis做一些长期换成所使用,所谓的长期缓存,意思就是说,对应的数据在数据库内落了一份,然后将Redis做缓存使用,并不是说redis挂机或者重启之后,所有的一个请求都会打到数据库上,那这样的话就需要有一个redis的一个过程,因此一般来说对Redis会有一个持久化的需求。

虽然没有必要去保证所有数据会有MySQL这样的存储数据的强一致,但是至少不能落后真正硬盘存储数据太多,因此Redis也提供给我们的两种程序化方式,那第1种程序化方式可以理解为是通过Redis的命令去将整个内存级别的数据,进行全量备份,由于它是以另一个线程去做驱动,因此可以基本不影响线上(bgsave),当Redis在重启做启动时,也会去拉取最近的一次的RDB数据去做数据的拉起。

那接下来我们来看一下AOF,AOF存储能力是用来存储增量的数据的,也就是说增量数据需要通过一些策略去做更新和做调整,那我们来看一下,假设有一个Redis的memory,在Redis memory当中记录了相关内容,然后进行一个全量备份的操作,将memory中的一个快照全部的映射到Redis的硬盘当中,它是属于一个全量的RDB备份。

900秒的这个时间不算长也不算短,很难精确的去控制好时间窗口,另一种策略就是 当发生了900次操作的时候,会自动触发RDB全量备份

那第1种策略,到期写,每隔一些固定的周期做RDB全量备份,比如说设置900秒写一次,
image.png

AOF存在的价值是不可能永远去做RDB备份,于是Redis会提供给我们AOF的三种策略:

  • always:表示数据被修改立刻持久化,写入AOF文件当中
  • everysec:表示每秒持久化,写入AOF文件当中
  • no:将写入AOF文件交给操作系统决定。

RDB+AOP近乎可以完全满足对强一致信息要求不高的一些缓存的场景,这也是Redis和memory等等相关的一些应用的优势所在。

3.6.3 缓存淘汰策略

  • lru: 最近最少使用的淘汰(默认)

allkeys:所有volatile: 设置过期时间的

  • ttl:从已设置过期时间中挑选将要过期的淘汰
  • random:数据中随机淘汰

allkeys:所有volatile: 设置过期时间的

  • no-enviction: 禁止驱逐,直接报错

单线程以及原子性
setnx
为什么能保证原子性,工作线程是单线程的。

slave可能会出现数据不一致,可能要考虑数据库。
image.png

3.7 消息队列基础能力

3.7.1 rocketmq基本结构

  • name server

命名查询服务器

  • producer

消息生产者

  • message broker

消息队列

  • consumer group

消费组

  • consumer

消息消费者

  • topic

消息主题

  • partition

消息分片队列

  • replication

(rocketmq无)
消息副本

消息队列往往会在我们的程序当中,采用到异步通信相关的过程,所有异步化的能力都可以通过消息对接解决,在课程当中,也有讲到过关于分布式事务相关的一些事务型消息,也是借助于像rocketmq这样带分布式事务消息对列处理能力的消息中间件,去解决的,整个的消息队列仅仅只不过是队列的一种,但是如果说我们要满足能够适应互联网要求的一个大型消息队列集群,我们需要以集群的设计思想去做对应的一个分布式的架构体系,因此需要充分的了解整个消息队列的一些基础组件与模块能力,才可以很好回答面试官的问题。

先以RocketMQ为例看一下整个的消息队列的结构,但有的同学可能会问为什么我们不选型?本质上来说阿里采用rocketmq是为了解决当时kafka消息队列解决不了的一些问题,并且在kafka基础上做了更适合于互联网应用的一些开发,比如说延迟消息队列,事务型消息,重试队列等等这样的一些相关的能力,那因此整个学习rocketmq也变相地学习了一些kafka的基础知识,当然针对分布式的副本备份来说,rocketmq和kafka还是有一定的区别,
首先以rocketmq来看一下整个消息队列的结构,rocketmq首先需要有一个nameserver命名查询服务器使用,在这个nameserver上面存储了所有的message broker topic的相关的一些信息,帮助我们去做对应的一个寻址和查找功能,

对应有一个消息生产者producer做内容消息生产使用,并且将生产出来的消息投递到message broker的消息队列当中,然后会有包含了consumer group消费组的一群consumer做对应消费者,以topic为单位去消费对应的消息,

每一个topic内部它是一个逻辑的消息主体,它会分为实实在在的partition,或者在rocketmq当中叫做queue去做消息的分片和消息队列的存储所使用,并且每一个对应的partition的会对应一个replication做消息副本,以防止partition内容的丢失,当然在rocketmq的架构里是没有replication,而是在message broker上做一个主从同步,去保证高可用性。

从namespace上面获取连接信息,每个partition是物理分区,一个consumer只能处理一个partition,建议consumer数量稍大于partition

image.png

主从架构
异步复制,进行同步,如果没来及同步,就会出现丢失,真正高可用就是两个都同步完才会给producer,生产环境一般做主库写入,从库异步复制,这样就能保证百分之99以上的可用性了。

image.png
partition都会有副本,会被均匀的分配到broker副本。

3.7.2 消息消费确认

  • ack
  • offset

最少会给consumer发一次消息,会一直重试,consumer需要对应做防重。做对应的幂等,通过消费方,ack建立在consumer可以接受多次消息基础上建立的。

因为是批量拉取消息,一次性拉去了msg1,3,5,在消费者内部多线程进行消费,可能部分消费成功,部分消费失败,有两种处理机制,3和5消费成功,会将offset移到了5这个位置,将1扔到了重试队列,重试队列会继续向consumer投递消息,consumer又感知了消费队列又感知了重试队列。
kafka只有offet,会把指针放到1的位置,会让1,3,5,3和5就会重复消费。

3.8 网络及操作系统基础能力

3.8.1 select和epoll的区别

  • 数量上限
  • 轮询or回调

select
select拿到socket连接的句柄,就会被阻塞住
select有一些缺陷:1024个数组的限制、遍历轮询触发找到真正有信号的socket连接

image.png

epoll模型linux2.6以上才有 jvm会在内部判断系统版本,确定是否支持epoll模型。
去除了数组大小的限制,被系统回调触发handler,避免了轮询触发机制,异步回调的方式去执行handler。
image.png

3.8.2 https加密

  • 非对称运算
  • 对称运算

https即security http。
通过对称加密运算和非对称加密运算。
image.png

3.8.3 http2.0

  • 二进制传输
  • 多路复用
  • 服务端推送

image.png