作者:图灵课堂———-周瑜

什么是Mysql中的降序索引?

大家可能对索引比较熟悉,而对降序索引比较陌生,事实上,降序索引是索引的子集。

我们通常使用下面的语句来创建一个索引:

  1. create index idx_t1_bcd on t1(b,c,d);

上面sql的意思是在t1表中,针对b,c,d三个字段创建一个联合索引。

但是大家不知道的是,上面这个sql实际上和下面的这个sql是等价的:

  1. create index idx_t1_bcd on t1(b asc,c asc,d asc);

asc表示的是升序,使用这种语法创建出来的索引叫做升序索引。也就是我们平时在创建索引的时候,创建的都是升序索引。

可能你会想到,在创建的索引的时候,可以针对字段设置asc,那是不是也可以设置desc呢?

当然是可以的,比如下面三个语句:

  1. create index idx_t1_bcd on t1(b desc,c desc,d desc);
  2. create index idx_t1_bcd on t1(b asc,c desc,d desc);
  3. create index idx_t1_bcd on t1(b asc,c asc,d desc);

这种语法在mysql中也是支持的,使用这种语法创建出来的索引就叫降序索引,关键问题是:在Mysql8.0之前仅仅只是语法层面的支持,底层并没有真正支持。

我们分别使用Mysql7、Mysql8两个版本来举例子说明一下:

在Mysql7、Mysql8中分别创建一个表,有a,b,c,d,e五个字段:

  1. create table t1 (
  2. a int primary key,
  3. b int,
  4. c int,
  5. d int,
  6. e varchar(20)
  7. ) engine=InnoDB;

然后分别创建一个降序索引:

  1. create index idx_t1_bcd on t1(b desc,c desc,d desc);

创建成功后,我们使用以下sql查看一下索引信息:

  1. show index from t1;

Mysql7中你将得到结果:
image.png

Mysql8中你将得到结果:
image.png

我们只关心Key_name为idx_t1_bcd的三行记录,细心的你应该可以发现,这两个结果中的Collation字段的结果是不一样的:

  • 在Mysql7中,Collation字段的结果为A,A,A,表示b,c,d三个字段的排序方式是asc
  • 在Mysql8中,Collation字段的结果为D,D,D,表示b,c,d三个字段的排序方式是desc

但是我们在创建索引的时候,明明在语法层面已经指定了b,c,d三个字段的排序方式是desc,这就可以看出来在Mysql7中降序索引只是语法层面的支持,底层并没有真正支持,并且固定是升序索引。而在Mysql8中则真正从底层支持了降序索引。

到此为止,大家应该对升序索引和降序索引有了一个大概的了解,但并没有真正理解,因为大家并不知道升序索引与降序索引底层到底是如何实现的。

升序索引底层实现

我们知道,索引是用来提高查询速度的,但是为什么索引能提高查询速度呢?

给定你一个数列,比如[1,3,7,9,2,5,4,6,8],这是一个无序的数列或数组,现在如果想提高这个数列的查询速度,你首先会做什么?

我相信大部分人都能够想到先排序,先把这个无序的数列,按从小到大的顺序进行排序,比如得到[1,2,3,4,5,6,7,8,9],有了这个有序的数列之后,我们就可以利用比如二分法等等算法来提高这个数列的查询速度了。

我举这个例子想告诉大家的是:想提高数据集合的查询速度,首先你可以对这些数据进行排序。

所以,对Mysql表中的存储的数据也是一样的,我们如果想提高这个表的查询速度,我们可以先对这个表里的数据进行排序,那么表里的某一行数据包括了很多字段,我们现在想对这些数据行进行排序,我们应该根据哪些字段来确定这个顺序呢?这就是索引,在创建索引的时候你所指定的列就是用来对表里的数据行进行排序的。

比如我们仍然利用上面所创建的t1表,向t1表里插入8条数据:

  1. insert into t1 values(4,3,1,1,'d');
  2. insert into t1 values(1,1,1,1,'a');
  3. insert into t1 values(8,8,8,8,'h');
  4. insert into t1 values(2,2,2,2,'b');
  5. insert into t1 values(5,2,3,5,'e');
  6. insert into t1 values(3,3,2,2,'c');
  7. insert into t1 values(7,4,5,5,'g');
  8. insert into t1 values(6,6,4,4,'f');

那么这些数据肯定是存储在文件中的,所以文件中保存这些数据的格式大概如下,顺序与插入顺序保持一致:

  1. 4311d
  2. 1111a
  3. 8888h
  4. 2222b
  5. 5235e
  6. 3322c
  7. 7455g
  8. 6644f

注意,t1是Innodb的存储引擎,而且a字段是主键,所以Innodb存储引擎在处理这些插入的数据时,会按主键进行排序,也就是上面我说的文件中保存这些数据的格式是不准确的,因为不想篇幅太长,所以不去深究,感兴趣的同学可以关注一波公众号:1点25,我会专门写一篇文章来讲解Innodb中索引的具体实现,包括B+树到底是如何生成的。

而如果我们基于上面的这种存储方式,来查找数据,比如查找a=3的这行记录,查找需要从第一行记录开始查找,那么要查找6次,而如果我们将上面的数据按照a字段的大小来进行排序:

  1. 1111a
  2. 2222b
  3. 3322c
  4. 4311d
  5. 5235e
  6. 6644f
  7. 7455g
  8. 8888h

排好序之后,如果我们还是查找a=3的这行记录,我们只需要查3次了。而且这样还有一个好处就是,如果我们现在需要查找a=3.5这行数据,如果我们基于未排序之前的存储方式,我们需要查询所有8行数据最终确定a=3.5这行数据不存在,而如果我们利用排好序之后的存储方式,我们就只需要查4次就好了,因为当你查到4311d这行记录时,你会发现4>3.5了,已经可以确定a=3.5的这行记录不存在了。

而如果我们现在对t1创建一个索引,就像上面创建索引一样,如果我们写的是下面的sql:

  1. create index idx_t1_bcd on t1(b,c,d);

这个sql表示要对t1创建一个索引,索引字段是b,c,d,并且是升序的,所以实际上就是对原本的数据按照b,c,d三个字段进行排序,那么排序之后类似:

  1. 1111a
  2. 2222b
  3. 5235e
  4. 4311d
  5. 3322c
  6. 7455g
  7. 6644f
  8. 8888h

可以好好看下,上面的记录是按照b,c,d三个字段来对数据行就行排序的,比如1111a中的b,c,d三个字段的值是111,而2222b中的b,c,d三个字段的值是222, 111是小于222的,所以对应的行排在前面。

那么数据如果这样排序有什么好处呢?其实和刚刚按a字段排序之后的好处是类似的,比如你现在想来查找b=4 and c=4 and d=4的数据也是能查询更快的,实际上这就是索引的原理:
我们对某个表创建一个索引,就是对这个表中的数据进行排序,而排好序之后的数据是能够提高查询速度。

还有一点需要注意的是,排序有很多中方式,或者所可以利用一些数据结构,比如二叉树、红黑树、B+树,这些数据结构实际上就是对数据进行排序,只是排序的形式各不相同而已,每种数据结构有它各自的特点,而大家应该都知道,Mysql中用得最多的就是B+树了,还是一样,因为篇幅不想太长,感兴趣的同学可以关注一波公众号:1点25,我会专门写一篇文章来讲解Innodb中索引的具体实现,包括B+树到底是如何生成的。

相信,看到这里,大家应该对索引重新有了认识,只不过我们上面举的几个例子都是升序排序,而且排好序之后的数据不仅可以提高查询速度,而且对于order by也是管用的,比如我们如果现在想对t1进行order by b asc,c asc,d asc;对于这个排序,如果已经在t1表建立了b,c,d的升序索引,那么就代表对t1表中的数据已经提前按照b,c,d排好序了,所以对于order by语句可以直接使用已经排好序的数据了,不用利用filesort再次进行排序了。

而且如果我们的order by是order by b desc, c desc, d desc,同样可以利用b,c,d的升序索引,因为如果是order by b asc,c asc,d asc就从上往下遍历即可,如果是order by b desc, c desc, d desc就从下往上遍历即可。

那么,如果是order by b asc, c desc, d desc呢?这个order by是不是就没有办法利用b,c,d的升序索引了。

这个时候就需要降序索引了。

降序索引底层实现

我们花了较大篇幅介绍了升序索引的实现原理,总结来说就是对表中的数据按照指定的字段比较大小进行升序排序

升序是什么?是数据进行大小比较后,是小的在上,大的在下,或者如果是B+树的话就是小的在左,大的在右。而降序就是大的在上,小的在下,或者如果是B+树的话就是大的在左,小的在右

所以,对于上面的那份原始数据:

  1. 4311d
  2. 1111a
  3. 8888h
  4. 2222b
  5. 5235e
  6. 3322c
  7. 7455g
  8. 6644f

如果我们将这份数据按照a desc进行排序就是:

  1. 8888h
  2. 7455g
  3. 6644f
  4. 5235e
  5. 4311d
  6. 3322c
  7. 2222b
  8. 1111a

非常简单吧,那如果我们将这份数据按照b desc, c desc, d desc排序就是:

  1. 8888h
  2. 6644f
  3. 7455g
  4. 3322c
  5. 4311d
  6. 5235e
  7. 2222b
  8. 1111a

也非常简单,那如果我们要将这份数据按照b desc, c asc, d desc排序呢?这是不是就有点懵了?

其实不难,排序其实就是对数据比较大小,我们用下面三行数据来模拟一下:

  1. 3322c
  2. 7455g
  3. 4311d

首先,按照b desc, c desc, d desc来排序,得到结果如下:

  1. 7455g
  2. 3322c
  3. 4311d

按照b desc, c asc, d desc来排序,得到结果如下:

  1. 7455g
  2. 4311d
  3. 3322c

可能一部分大佬已经能理解,实际上b desc所表达的意思就是b字段数据大者在上,数据小者在下,数据相等的话则开始比较c字段,而c字段是按升序排的,也就是c字段数据小者在下,数据大者在上。所以就得到了上面的结果。

这就是降序索引

线程池中的线程是如何保活和回收的?

我们知道线程池的作用就是提高线程的利用率,需要线程时,可以直接从线程池中获取线程直接使用,而不用创建线程,那线程池中的线程,在没有任务执行时,是如何保活的呢?

线程池中的线程会不断地的从阻塞队列中获取任务,调用的是workQueue.poll()方法或take(),这两个方法都会阻塞式的从队列中获取元素,区别式poll()方法可以设置一个超时时间,take()不能设置超时时间,所以这也间接的使得线程池中的线程阻塞等待从而达到保活的效果。

当然并不是线程池中的所有线程都需要一直保活,比如只有核心线程需要保活,非核心线程就不需要保活,那非核心线程是怎么回收的呢?

底层是这样的,当一个线程处理完当前任务后,就会开始去阻塞队列中获取任务,只不过,在调用poll或take方法之前,会判断当前线程池中有多少个线程,如果多余核心线程数,那么当前线程就会调用poll()并设置超时时间来获取阻塞队列中的任务,这样一旦时间到了还没有获取到任务,那么线程就不会阻塞了,并且没有业务执行,那么线程就会运行结束,也就是回收了。

线程池有哪几种状态,分别是如何变化的?

  1. RUNNING:线程池正常运行中,可以正常的接受并处理任务
  2. SHUTDOWN:线程池关闭了,不能接受新任务,但是线程池会把阻塞队列中的剩余任务执行完,剩余任务都处理完之后,会中断所有工作线程
  3. STOP:线程池停止了,不能接受新任务,并且也不会处理阻塞队列中的任务,会中断所有工作线程
  4. TIDYING:当前线程池中没有工作线程后,也就是线程池没有工作线程在运行了,就会进入TIDYING,这个状态是自动发生在SHUTDOWN和STOP之后的
  5. TERMINATED:线程池处于TIDYING状态后,会执行terminated()方法,执行完后就会进入TERMINATED状态,在ThreadPoolExecutor中terminated()是一个空方法,可以自定义线程池重写这个方法

作者:图灵课堂———-周瑜

Tomcat的最大线程数为什么默认是200?

首先,200只是一个默认值,一般默认值代表大多数情况,按照自己的情况进行修改,所以我们不应该纠结为什么是200,而不是300,这里我们应该想的是,为什么是200,而不是20,因为我们都知道并不是线程越多越好,我们一般会根据计算机的核心数来设置线程数,比如16核,那么要么我们就设置16个线程,或32个线程,怎么也不会设置到200个线程,那么Tomcat为什么默认给的是200,给的这么大呢?

我个人认为,之所以这么设置,是为了用户体验

举个例子,假如你去银行取钱,但是现在只有一台点钞机,正常来说,就开一个窗口就好了,反正就一台点钞机,那对于客户来说,到底是开一个窗口让人心里舒服些,还是开10个窗口让人舒服些呢?

按我去银行取钱的经验来说,是10个,因为我不想排队,这样能让我更快的感受到自己被服务了,虽然服务过程会比较慢。

回到Tomcat,也是一样的,Tomcat中的线程是用来处理客户所发送过来的HTTP请求的,如果只有16个线程,那就表示同一个时刻只能处理16个请求,如果每个请求都需要查询数据库,都需要进行磁盘IO或网络IO,那这16个线程一下子就被这16个请求占用完了,当第17个请求过来时,就没有线程能处理它了,只能排队等着,设置会被拒绝服务。

而Tomcat把线程数设置为200,显而易见,此时Tomcat就能同时处理200个请求了,这个对于发送请求的客户来说体验是比较好的,自己发送的请求正在被处理了,虽然可能会慢一些,但是我更加不愿意排队。

什么是集群脑裂?如何解决脑裂问题?

什么是脑裂

脑裂(split-brain)就是“大脑分裂”,也就是本来一个“大脑”被拆分了两个或多个“大脑”,我们都知道,如果一个人有多个大脑,并且相互独立的话,那么会导致人体“手舞足蹈”,“不听使唤”。

脑裂通常会出现在集群环境中,比如ElasticSearch、Zookeeper集群,而这些集群环境有一个统一的特点,就是它们有一个大脑,比如ElasticSearch集群中有Master节点,Zookeeper集群中有Leader节点。

本篇文章着重来给大家讲一下Zookeeper中的脑裂问题,以及是如果解决脑裂问题的。

Zookeeper集群中的脑裂场景

对于一个集群,想要提高这个集群的可用性,通常会采用多机房部署,比如现在有一个由6台zkServer所组成的一个集群,部署在了两个机房:
image.png

正常情况下,此集群只会有一个Leader,那么如果机房之间的网络断了之后,两个机房内的zkServer还是可以相互通信的,如果不考虑过半机制,那么就会出现每个机房内部都将选出一个Leader。
image.png

这就相当于原本一个集群,被分成了两个集群,出现了两个“大脑”,这就是脑裂。

对于这种情况,我们也可以看出来,原本应该是统一的一个集群对外提供服务的,现在变成了两个集群同时对外提供服务,如果过了一会,断了的网络突然联通了,那么此时就会出现问题了,两个集群刚刚都对外提供服务了,数据该怎么合并,数据冲突怎么解决等等问题。

刚刚在说明脑裂场景时,有一个前提条件就是没有考虑过半机制,所以实际上Zookeeper集群中是不会出现脑裂问题的,而不会出现的原因就跟过半机制有关。

过半机制

在领导者选举的过程中,如果某台zkServer获得了超过半数的选票,则此zkServer就可以成为Leader了。
过半机制的源码实现其实非常简单:

  1. public class QuorumMaj implements QuorumVerifier {
  2. private static final Logger LOG = LoggerFactory.getLogger(QuorumMaj.class);
  3. int half;
  4. // n表示集群中zkServer的个数(准确的说是参与者的个数,参与者不包括观察者节点)
  5. public QuorumMaj(int n){
  6. this.half = n/2;
  7. }
  8. // 验证是否符合过半机制
  9. public boolean containsQuorum(Set<Long> set){
  10. // half是在构造方法里赋值的
  11. // set.size()表示某台zkServer获得的票数
  12. return (set.size() > half);
  13. }
  14. }

大家仔细看一下上面方法中的注释,核心代码就是下面两行:

  1. this.half = n/2;
  2. return (set.size() > half);

举个简单的例子:
如果现在集群中有5台zkServer,那么half=5/2=2,那么也就是说,领导者选举的过程中至少要有三台zkServer投了同一个zkServer,才会符合过半机制,才能选出来一个Leader。

那么有一个问题我们想一下,选举的过程中为什么一定要有一个过半机制验证?
因为这样不需要等待所有zkServer都投了同一个zkServer就可以选举出来一个Leader了,这样比较快,所以叫快速领导者选举算法呗。

那么再来想一个问题,过半机制中为什么是大于,而不是大于等于呢?

这就是更脑裂问题有关系了,比如回到上文出现脑裂问题的场景:
image.png

当机房中间的网络断掉之后,机房1内的三台服务器会进行领导者选举,但是此时过半机制的条件是set.size() > 3,也就是说至少要4台zkServer才能选出来一个Leader,所以对于机房1来说它不能选出一个Leader,同样机房2也不能选出一个Leader,这种情况下整个集群当机房间的网络断掉后,整个集群将没有Leader。

而如果过半机制的条件是set.size() >= 3,那么机房1和机房2都会选出一个Leader,这样就出现了脑裂。所以我们就知道了,为什么过半机制中是大于,而不是大于等于。就是为了防止脑裂。

如果假设我们现在只有5台机器,也部署在两个机房:
image.png

此时过半机制的条件是set.size() > 2,也就是至少要3台服务器才能选出一个Leader,此时机房件的网络断开了,对于机房1来说是没有影响的,Leader依然还是Leader,对于机房2来说是选不出来Leader的,此时整个集群中只有一个Leader。

所以,我们可以总结得出,有了过半机制,对于一个Zookeeper集群,要么没有Leader,要没只有1个Leader,这样就避免了脑裂问题。

作者:图灵课堂———-周瑜

微服务中什么是应用级注册?什么是接口级注册?优缺点是什么?

目前大部分的微服务框架都是应用级注册,比如SpringCloud,包括K8S也是应用注册,应用级注册代表是把整个应用作为一个微服务注册到注册中心,注册中心保存的数据格式为:

应用名:
实例1的ip和port
实例2的ip和port
实例3的ip和port

而接口级注册,代表是把应用中的某个接口作为一个微服务注册到注册中心,注册中心保存的数据格式为:

接口1:
实例1的ip和port
实例2的ip和port
实例3的ip和port
接口2:
实例1的ip和port
实例2的ip和port
实例3的ip和port

目前Dubbo3.0之前的版本采取的就是接口级注册,Dubbo3.0已经支持了应用级注册。

接口级注册的好处在于,对于消费者而言,可以直接面向接口,消费者想要使用哪个接口,就可以直接从注册中心根据接口名找到接口所在的地址,然后直接调用,而不用关心接口在哪个应用。而对于应用级注册,对于消费者而言,需要知道想要使用的接口在哪个应用,然后获取应用实例地址,再去调用接口,相比较接口级注册而言就稍微麻烦了一点。

接口级注册的缺点在于,注册中心的存储的冗余信息较多,注册中心的压力更大,注册中心存储的数据如果越多,那么数据发生改变的频率也就越高,对于消费者而言就需要消耗资源来进行同步,也增加了消费者应用的压力。

SpringBoot的自动配置是如何实现的?

首先,SpringBoot的自动配置主要作用就是帮助程序员配置了Bean,如果我们不使用SpringBoot,而使用Spring,那么我们在使用Mybatis、各种消息队列、AOP、Spring事务等等功能的时候,除开要添加相关的依赖包之外,还要额外的配置相关的Bean,而SpringBoot的Starter机制就是帮我们更便捷的添加相关的Bean,SpringBoot的自动配置就是帮我们配置了一些。

SpringBoot会通过自动配置类来承载这些Bean,按不同的业务功能,分为不同的自动配置类,每个自动配置类中就定义了该业务相关的Bean,自动配置类其实就是Spring的配置类,只不过是SpringBoot默认给我们提供的,所以叫自动配置类。

另外由于这些自动配置类,是在SpringBoot内部定义的,而对于我们的项目而言,一般情况下是扫描不到这些自动配置的,所以在SpringBoot中会利用@Import注解、以及DeferredImportSelector、以及spring.factories来实现自动配置类的加载,spring.factories相当于SpringBoot的SPI机制,所以如果第三方框架,比如Mybatis,也想提供一些自动配置类,那么也可以在自己的spring.factories文件中添加自己的自动配置类,到时候SpringBoot是能够读取出来并进行解析的。

另外SpringBoot中的自动配置功能的实现,还有一个核心就是添加注解,SpringBoot中基于@Conditional注解扩展了很多其他的条件注解,比如@ConditionalOnClass、@ConditionalOnBean等等,这些条件注解可以用来控制,某个自动配置类或某个Bean是否真正生效,符合所定义的条件才生效,比如如果程序员自己定义了一个,那么SpringBoot默认提供的Bean那就不生效了,这些都是利用条件注解来实现的。

作者:图灵课堂———-周瑜

SpringBoot中配置优先级是怎样的?

流程图:https://www.processon.com/view/link/62d399e71e08530a89222b23

如何设计一个RPC框架?

首先,RPC表示远程过程调用,在Java中表示远程方法调用,所以,一个RPC框架就是一个能支持调用远程方法的框架。

作为一个RPC框架,那最核心的就是网络调用了,也就是如何高效的调用远程方法,比如:

  1. 通过网络,要发送哪些数据,比如至少得发送类名或接口名、要调用的方法名、调用方法时传入的参数等
  2. 方法参数通常是对象,那如果要通过网络发送对象,就要考虑序列化和反序列化,那使用何种序列化机制也是要考虑的
  3. 确定好了要发送的数据后,那通过什么方式发出去呢,是直接通过Socket发出去呢,还是利用HTTP发出去
  4. 如果利用HTTP来发,那是用HTTP1.1呢,还是用HTTP2呢,请求头放什么数据,请求体放什么数据呢?
  5. 如何直接通过Socket来发,那就需要自己设计一个数据格式了,类似与HTTP协议,不然服务端接收到字节流之后不知道该如何解析字节流
  6. 确定好数据格式和网络传输方式之后,就要考虑是否支持异步,是否支持回调等功能了
  7. 另外像负载均衡、服务容错、服务路由、服务重试等功能也要一步步丰满起来了,那这又是一大堆需要考虑的了

所以,对于如何设计一个RPC框架,大体思路是先设计一个最小可用的远程调用框架,然后不断优化性能,然后再不断扩展其功能,作为成为一个成熟的RPC框架,甚至于一个微服务框架。

看以下代码回答问题(一)

  1. public static void main(String[] args) {
  2. String s = new String("abc");
  3. // 在这中间可以添加N行代码,但必须保证s引用的指向不变,最终将输出变成abcd
  4. System.out.println(s);
  5. }

答案:

  1. public static void main(String[] args) {
  2. String s = new String("abc");
  3. // 在这中间可以添加N行代码,但必须保证s引用的指向不变,最终将输出变成abcd
  4. Field value = s.getClass().getDeclaredField("value");
  5. value.setAccessible(true);
  6. value.set(s, "abcd".toCharArray());
  7. System.out.println(s);
  8. }


看以下代码回答问题(二)

  1. public static void main(String[] args) {
  2. String s1 = new String("abc");
  3. String s2 = "abc";
  4. // s1 == s2?
  5. String s3 = s1.intern();
  6. // s2 == s3?
  7. }

答案:

  1. s1 == s2为false
  2. s2 == s3为true

String对象的intern方法,首先会检查字符串常量池中是否存在”abc”,如果存在则返回该字符串引用,如果不存在,则把”abc”添加到字符串常量池中,并返回该字符串常量的引用。

看以下代码回答问题(三)

  1. public static void main(String[] args) {
  2. Integer i1 = 100;
  3. Integer i2 = 100;
  4. // i1 == i2?
  5. Integer i3 = 128;
  6. Integer i4 = 128;
  7. // i3 == i4?
  8. }

答案:

  1. i1 == i2为true
  2. i3 == i4为false

在Interger类中,存在一个静态内部类IntegerCache, 该类中存在一个Integer cache[], 并且存在一个static块,会在加载类的时候执行,会将-128至127这些数字提前生成Integer对象,并缓存在cache数组中,当我们在定义Integer数字时,会调用Integer的valueOf方法,valueOf方法会判断所定义的数字是否在-128至127之间,如果存在则直接从cache数组中获取Integer对象,如果超过,则生成一个新的Integer对象。

String、StringBuffer、StringBuilder的区别

  1. String是不可变的,如果尝试去修改,会新生成一个字符串对象,StringBuffer和StringBuilder是可变的
  2. StringBuffer是线程安全的,StringBuilder是线程不安全的,所以在单线程环境下StringBuilder效率会更高


ArrayList和LinkedList有哪些区别

  1. 首先,他们的底层数据结构不同,ArrayList底层是基于数组实现的,LinkedList底层是基于链表实现的
  2. 由于底层数据结构不同,他们所适用的场景也不同,ArrayList更适合随机查找,LinkedList更适合删除和添加,查询、添加、删除的时间复杂度不同
  3. 另外ArrayList和LinkedList都实现了List接口,但是LinkedList还额外实现了Deque接口,所以LinkedList还可以当做队列来使用


CopyOnWriteArrayList的底层原理是怎样的

  1. 首先CopyOnWriteArrayList内部也是用数组来实现的,在向CopyOnWriteArrayList添加元素时,会复制一个新的数组,写操作在新数组上进行,读操作在原数组上进行
  2. 并且,写操作会加锁,防止出现并发写入丢失数据的问题
  3. 写操作结束之后会把原数组指向新数组
  4. CopyOnWriteArrayList允许在写操作时来读取数据,大大提高了读的性能,因此适合读多写少的应用场景,但是CopyOnWriteArrayList会比较占内存,同时可能读到的数据不是实时最新的数据,所以不适合实时性要求很高的场景


HashMap的扩容机制原理

1.7版本

  1. 先生成新数组
  2. 遍历老数组中的每个位置上的链表上的每个元素
  3. 取每个元素的key,并基于新数组长度,计算出每个元素在新数组中的下标
  4. 将元素添加到新数组中去
  5. 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性

1.8版本

  1. 先生成新数组
  2. 遍历老数组中的每个位置上的链表或红黑树
  3. 如果是链表,则直接将链表中的每个元素重新计算下标,并添加到新数组中去
  4. 如果是红黑树,则先遍历红黑树,先计算出红黑树中每个元素对应在新数组中的下标位置
    1. 统计每个下标位置的元素个数
    2. 如果该位置下的元素个数超过了8,则生成一个新的红黑树,并将根节点的添加到新数组的对应位置
    3. 如果该位置下的元素个数没有超过6,那么则生成一个链表,并将链表的头节点添加到新数组的对应位置
  5. 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性

ConcurrentHashMap的扩容机制

1.7版本

  1. 1.7版本的ConcurrentHashMap是基于Segment分段实现的
  2. 每个Segment相对于一个小型的HashMap
  3. 每个Segment内部会进行扩容,和HashMap的扩容逻辑类似
  4. 先生成新的数组,然后转移元素到新数组中
  5. 扩容的判断也是每个Segment内部单独判断的,判断是否超过阈值

1.8版本

  1. 1.8版本的ConcurrentHashMap不再基于Segment实现
  2. 当某个线程进行put时,如果发现ConcurrentHashMap正在进行扩容那么该线程一起进行扩容
  3. 如果某个线程put时,发现没有正在进行扩容,则将key-value添加到ConcurrentHashMap中,然后判断是否超过阈值,超过了则进行扩容
  4. ConcurrentHashMap是支持多个线程同时扩容的
  5. 扩容之前也先生成一个新的数组
  6. 在转移元素时,先将原数组分组,将每组分给不同的线程来进行元素的转移,每个线程负责一组或多组的元素转移工作


ThreadLocal的底层原理

  1. ThreadLocal是Java中所提供的线程本地存储机制,可以利用该机制将数据缓存在某个线程内部,该线程可以在任意时刻、任意方法中获取缓存的数据
  2. ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象(注意不是ThreadLocal对象)中都存在一个ThreadLocalMap,Map的key为ThreadLocal对象,Map的value为需要缓存的值
  3. 如果在线程池中使用ThreadLocal会造成内存泄漏,因为当ThreadLocal对象使用完之后,应该要把设置的key,value,也就是Entry对象进行回收,但线程池中的线程不会回收,而线程对象是通过强引用指向ThreadLocalMap,ThreadLocalMap也是通过强引用指向Entry对象,线程不被回收,Entry对象也就不会被回收,从而出现内存泄漏,解决办法是,在使用了ThreadLocal对象之后,手动调用ThreadLocal的remove方法,手动清楚Entry对象
  4. ThreadLocal经典的应用场景就是连接管理(一个线程持有一个连接,该连接对象可以在不同的方法之间进行传递,线程之间不共享同一个连接)

2022年金九银十面试题 - 图7

作者:图灵课堂———-周瑜

如何理解volatile关键字

在并发领域中,存在三大特性:原子性、有序性、可见性。volatile关键字用来修饰对象的属性,在并发环境下可以保证这个属性的可见性,对于加了volatile关键字的属性,在对这个属性进行修改时,会直接将CPU高级缓存中的数据写回到主内存,对这个变量的读取也会直接从主内存中读取,从而保证了可见性,底层是通过操作系统的内存屏障来实现的,由于使用了内存屏障,所以会禁止指令重排,所以同时也就保证了有序性,在很多并发场景下,如果用好volatile关键字可以很好的提高执行效率。

ReentrantLock中的公平锁和非公平锁的底层实现

首先不管是公平锁和非公平锁,它们的底层实现都会使用AQS来进行排队,它们的区别在于:线程在使用lock()方法加锁时,如果是公平锁,会先检查AQS队列中是否存在线程在排队,如果有线程在排队,则当前线程也进行排队,如果是非公平锁,则不会去检查是否有线程在排队,而是直接竞争锁。

不管是公平锁还是非公平锁,一旦没竞争到锁,都会进行排队,当锁释放时,都是唤醒排在最前面的线程,所以非公平锁只是体现在了线程加锁阶段,而没有体现在线程被唤醒阶段。

另外,ReentrantLock是可重入锁,不管是公平锁还是非公平锁都是可重入的。
ReentrantLock公平锁加锁.png ReentrantLock非公平锁加锁.png

ReentrantLock中tryLock()和lock()方法的区别

  1. tryLock()表示尝试加锁,可能加到,也可能加不到,该方法不会阻塞线程,如果加到锁则返回true,没有加到则返回false
  2. lock()表示阻塞加锁,线程会阻塞直到加到锁,方法也没有返回值


CountDownLatch和Semaphore的区别和底层原理

CountDownLatch表示计数器,可以给CountDownLatch设置一个数字,一个线程调用CountDownLatch的await()将会阻塞,其他线程可以调用CountDownLatch的countDown()方法来对CountDownLatch中的数字减一,当数字被减成0后,所有await的线程都将被唤醒。
对应的底层原理就是,调用await()方法的线程会利用AQS排队,一旦数字被减为0,则会将AQS中排队的线程依次唤醒。

Semaphore表示信号量,可以设置许可的个数,表示同时允许最多多少个线程使用该信号量,通过acquire()来获取许可,如果没有许可可用则线程阻塞,并通过AQS来排队,可以通过release()方法来释放许可,当某个线程释放了某个许可后,会从AQS中正在排队的第一个线程开始依次唤醒,直到没有空闲许可。

Sychronized的偏向锁、轻量级锁、重量级锁

  1. 偏向锁:在锁对象的对象头中记录一下当前获取到该锁的线程ID,该线程下次如果又来获取该锁就可以直接获取到了
  2. 轻量级锁:由偏向锁升级而来,当一个线程获取到锁后,此时这把锁是偏向锁,此时如果有第二个线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻量级锁底层是通过自旋来实现的,并不会阻塞线程
  3. 如果自旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞
  4. 自旋锁:自旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就无所谓唤醒线程,阻塞和唤醒这两个步骤都是需要操作系统去进行的,比较消耗时间,自旋锁是线程通过CAS获取预期的一个标记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程一直在运行中,相对而言没有使用太多的操作系统资源,比较轻量。

Sychronized和ReentrantLock的区别

  1. sychronized是一个关键字,ReentrantLock是一个类
  2. sychronized会自动的加锁与释放锁,ReentrantLock需要程序员手动加锁与释放锁
  3. sychronized的底层是JVM层面的锁,ReentrantLock是API层面的锁
  4. sychronized是非公平锁,ReentrantLock可以选择公平锁或非公平锁
  5. sychronized锁的是对象,锁信息保存在对象头中,ReentrantLock通过代码中int类型的state标识来标识锁的状态
  6. sychronized底层有一个锁升级的过程

作者:图灵课堂———-周瑜

线程池有哪些状态?每种状态的效果是怎样的?

  1. RUNNING: Accept new tasks and process queued tasks,线程池正常运行,既接受新任务,也会处理队列中的任务
  2. SHUTDOWN:Don’t accept new tasks, but process queued tasks,当调用线程池的shutdown()方法时,线程池就进入SHUTDOWN状态,线程池不会接受新任务了,但是会继续处理队列中的任务
  3. STOP:Don’t accept new tasks, don’t process queued tasks, and interrupt in-progress tasks,当调用线程池的shutdownnow()方法时,线程池就进入STOP状态,线程池既不会接受新任务了,也不会处理队列中的任务,并且正在运行的线程也会被中断
  4. TIDYING:All tasks have terminated, workerCount is zero, the thread transitioning to state TIDYING will run the terminated() hook method,当线程池中没有线程在运行后,线程池的状态就会自动变为TIDYING,并且会调用terminated(),该方法是空方法,留给程序员进行扩展。
  5. TERMINATED:terminated() has completed,terminated()方法执行完之后,线程池状态就会变为TERMINATED

线程池的底层工作原理

线程池内部是通过队列+线程实现的,当我们利用线程池执行任务时:

  1. 如果此时线程池中的线程数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
  2. 如果此时线程池中的线程数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放入缓冲队列。
  3. 如果此时线程池中的线程数量大于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。
  4. 如果此时线程池中的线程数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。
  5. 当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数

JVM中哪些是线程共享区

堆区和方法区是所有线程共享的,栈、本地方法栈、程序计数器是每个线程独有的
2022年金九银十面试题 - 图10

JVM中哪些可以作为gc root

什么是gc root,JVM在进行垃圾回收时,需要找到“垃圾”对象,也就是没有被引用的对象,但是直接找“垃圾”对象是比较耗时的,所以反过来,先找“非垃圾”对象,也就是正常对象,那么就需要从某些“根”开始去找,根据这些“根”的引用路径找到正常对象,而这些“根”有一个特征,就是它只会引用其他对象,而不会被其他对象引用,例如:栈中的本地变量、方法区中的静态变量、本地方法栈中的变量、正在运行的线程等可以作为gc root。

你们项目如何排查JVM问题

对于还在正常运行的系统:

  1. 可以使用jmap来查看JVM中各个区域的使用情况
  2. 可以通过jstack来查看线程的运行情况,比如哪些线程阻塞、是否出现了死锁
  3. 可以通过jstat命令来查看垃圾回收的情况,特别是fullgc,如果发现fullgc比较频繁,那么就得进行调优了
  4. 通过各个命令的结果,或者jvisualvm等工具来进行分析
  5. 首先,初步猜测频繁发生fullgc的原因,如果频繁发生fullgc但是又一直没有出现内存溢出,那么表示fullgc实际上是回收了很多对象了,所以这些对象最好能在younggc过程中就直接回收掉,避免这些对象进入到老年代,对于这种情况,就要考虑这些存活时间不长的对象是不是比较大,导致年轻代放不下,直接进入到了老年代,尝试加大年轻代的大小,如果改完之后,fullgc减少,则证明修改有效
  6. 同时,还可以找到占用CPU最多的线程,定位到具体的方法,优化这个方法的执行,看是否能避免某些对象的创建,从而节省内存

对于已经发生了OOM的系统:

  1. 一般生产系统中都会设置当系统发生了OOM时,生成当时的dump文件(-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/base)
  2. 我们可以利用jsisualvm等工具来分析dump文件
  3. 根据dump文件找到异常的实例对象,和异常的线程(占用CPU高),定位到具体的代码
  4. 然后再进行详细的分析和调试

总之,调优不是一蹴而就的,需要分析、推理、实践、总结、再分析,最终定位到具体的问题

说说类加载器双亲委派模型

JVM中存在三个默认的类加载器:

  1. BootstrapClassLoader
  2. ExtClassLoader
  3. AppClassLoader

AppClassLoader的父加载器是ExtClassLoader,ExtClassLoader的父加载器是BootstrapClassLoader。

JVM在加载一个类时,会调用AppClassLoader的loadClass方法来加载这个类,不过在这个方法中,会先使用ExtClassLoader的loadClass方法来加载类,同样ExtClassLoader的loadClass方法中会先使用BootstrapClassLoader来加载类,如果BootstrapClassLoader加载到了就直接成功,如果BootstrapClassLoader没有加载到,那么ExtClassLoader就会自己尝试加载该类,如果没有加载到,那么则会由AppClassLoader来加载这个类。

所以,双亲委派指得是,JVM在加载类时,会委派给Ext和Bootstrap进行加载,如果没加载到才由自己进行加载。

Tomcat中为什么要使用自定义类加载器

一个Tomcat中可以部署多个应用,而每个应用中都存在很多类,并且各个应用中的类是独立的,全类名是可以相同的,比如一个订单系统中可能存在com.zhouyu.User类,一个库存系统中可能也存在com.zhouyu.User类,一个Tomcat,不管内部部署了多少应用,Tomcat启动之后就是一个Java进程,也就是一个JVM,所以如果Tomcat中只存在一个类加载器,比如默认的AppClassLoader,那么就只能加载一个com.zhouyu.User类,这是有问题的,而在Tomcat中,会为部署的每个应用都生成一个类加载器实例,名字叫做WebAppClassLoader,这样Tomcat中每个应用就可以使用自己的类加载器去加载自己的类,从而达到应用之间的类隔离,不出现冲突。另外Tomcat还利用自定义加载器实现了热加载功能。

Tomcat如何进行优化?

对于Tomcat调优,可以从两个方面来进行调整:内存和线程。
首先启动Tomcat,实际上就是启动了一个JVM,所以可以按JVM调优的方式来进行调整,从而达到Tomcat优化的目的。
另外Tomcat中设计了一些缓存区,比如appReadBufSize、bufferPoolSize等缓存区来提高吞吐量。
还可以调整Tomcat的线程,比如调整minSpareThreads参数来改变Tomcat空闲时的线程数,调整maxThreads参数来设置Tomcat处理连接的最大线程数。
并且还可以调整IO模型,比如使用NIO、APR这种相比于BIO更加高效的IO模型。

浏览器发出一个请求到收到响应经历了哪些步骤?

  1. 浏览器解析用户输入的URL,生成一个HTTP格式的请求
  2. 先根据URL域名从本地hosts文件查找是否有映射IP,如果没有就将域名发送给电脑所配置的DNS进行域名解析,得到IP地址
  3. 浏览器通过操作系统将请求通过四层网络协议发送出去
  4. 途中可能会经过各种路由器、交换机,最终到达服务器
  5. 服务器收到请求后,根据请求所指定的端口,将请求传递给绑定了该端口的应用程序,比如8080被tomcat占用了
  6. tomcat接收到请求数据后,按照http协议的格式进行解析,解析得到所要访问的servlet
  7. 然后servlet来处理这个请求,如果是SpringMVC中的DispatcherServlet,那么则会找到对应的Controller中的方法,并执行该方法得到结果
  8. Tomcat得到响应结果后封装成HTTP响应的格式,并再次通过网络发送给浏览器所在的服务器
  9. 浏览器所在的服务器拿到结果后再传递给浏览器,浏览器则负责解析并渲染

谈谈你对IOC的理解

通常,我们认为Spring有两大特性IoC和AOP,那到底该如何理解IoC呢?

对于很多初学者来说,IoC这个概念给人的感觉就是我好像会,但是我说不出来

那么IoC到底是什么,接下来来说说我的理解,实际上这是一个非常大的问题,所以我们就把它拆细了来回答,IoC表示控制反转,那么:

  1. 什么是控制?控制了什么?
  2. 什么是反转?反转之前是谁控制的?反转之后是谁控制的?如何控制的?
  3. 为什么要反转?反转之前有什么问题?反转之后有什么好处?

这就是解决这一类大问题的思路,大而化小。

那么,我们先来解决第一个问题:什么是控制?控制了什么?
我们在用Spring的时候,我们需要做什么:

  1. 建一些类,比如UserService、OrderService
  2. 用一些注解,比如@Autowired

但是,我们也知道,当程序运行时,用的是具体的UserService对象、OrderService对象,那这些对象是什么时候创建的?谁创建的?包括对象里的属性是什么时候赋的值?谁赋的?所有这些都是我们程序员做的,以为我们只是写了类而已,所有的这些都是Spring做的,它才是幕后黑手。

这就是控制

  1. 控制对象的创建
  2. 控制对象内属性的赋值

如果我们不用Spring,那我们得自己来做这两件事,反过来,我们用Spring,这两件事情就不用我们做了,我们要做的仅仅是定义类,以及定义哪些属性需要Spring来赋值(比如某个属性上加@Autowired),而这其实就是第二个问题的答案,这就是反转,表示一种对象控制权的转移。

那反转有什么用,为什么要反转?

如果我们自己来负责创建对象,自己来给对象中的属性赋值,会出现什么情况?

比如,现在有三个类:

  1. A类,A类里有一个属性C c;
  2. B类,B类里也有一个属性C c;
  3. C类

现在程序要运行,这三个类的对象都需要创建出来,并且相应的属性都需要有值,那么除开定义这三个类之外,我们还得写:

  1. A a = new A();
  2. B b = new B();
  3. C c = new C();
  4. a.c = c;
  5. b.c = c;

这五行代码是不用Spring的情况下多出来的代码,而且,如果类在多一些,类中的属性在多一些,那相应的代码会更多,而且代码会更复杂。所以我们可以发现,我们自己来控制比交给Spring来控制,我们的代码量以及代码复杂度是要高很多的,反言之,将对象交给Spring来控制,减轻了程序员的负担。

总结一下,IoC表示控制反转,表示如果用Spring,那么Spring会负责来创建对象,以及给对象内的属性赋值,也就是如果用Spring,那么对象的控制权会转交给Spring。

Spring中的Bean创建的生命周期有哪些步骤

Spring中一个Bean的创建大概分为以下几个步骤:

  1. 推断构造方法
  2. 实例化
  3. 填充属性,也就是依赖注入
  4. 处理Aware回调
  5. 初始化前,处理@PostConstruct注解
  6. 初始化,处理InitializingBean接口
  7. 初始化后,进行AOP

当然其实真正的步骤更加细致,可以看下面的流程图
Bean的生命周期.png

Spring中Bean是线程安全的吗

Spring本身并没有针对Bean做线程安全的处理,所以:

  1. 如果Bean是无状态的,那么Bean则是线程安全的
  2. 如果Bean是有状态的,那么Bean则不是线程安全的

另外,Bean是不是线程安全,跟Bean的作用域没有关系,Bean的作用域只是表示Bean的生命周期范围,对于任何生命周期的Bean都是一个对象,这个对象是不是线程安全的,还是得看这个Bean对象本身。

ApplicationContext和BeanFactory有什么区别

BeanFactory是Spring中非常核心的组件,表示Bean工厂,可以生成Bean,维护Bean,而ApplicationContext继承了BeanFactory,所以ApplicationContext拥有BeanFactory所有的特点,也是一个Bean工厂,但是ApplicationContext除开继承了BeanFactory之外,还继承了诸如EnvironmentCapable、MessageSource、ApplicationEventPublisher等接口,从而ApplicationContext还有获取系统环境变量、国际化、事件发布等功能,这是BeanFactory所不具备的

Spring中的事务是如何实现的

  1. Spring事务底层是基于数据库事务和AOP机制的
  2. 首先对于使用了@Transactional注解的Bean,Spring会创建一个代理对象作为Bean
  3. 当调用代理对象的方法时,会先判断该方法上是否加了@Transactional注解
  4. 如果加了,那么则利用事务管理器创建一个数据库连接
  5. 并且修改数据库连接的autocommit属性为false,禁止此连接的自动提交,这是实现Spring事务非常重要的一步
  6. 然后执行当前方法,方法中会执行sql
  7. 执行完当前方法后,如果没有出现异常就直接提交事务
  8. 如果出现了异常,并且这个异常是需要回滚的就会回滚事务,否则仍然提交事务
  9. Spring事务的隔离级别对应的就是数据库的隔离级别
  10. Spring事务的传播机制是Spring事务自己实现的,也是Spring事务中最复杂的
  11. Spring事务的传播机制是基于数据库连接来做的,一个数据库连接一个事务,如果传播机制配置为需要新开一个事务,那么实际上就是先建立一个数据库连接,在此新数据库连接上执行sql

2022年金九银十面试题 - 图12

Spring中什么时候@Transactional会失效

因为Spring事务是基于代理来实现的,所以某个加了@Transactional的方法只有是被代理对象调用时,那么这个注解才会生效,所以如果是被代理对象来调用这个方法,那么@Transactional是不会失效的。

同时如果某个方法是private的,那么@Transactional也会失效,因为底层cglib是基于父子类来实现的,子类是不能重载父类的private方法的,所以无法很好的利用代理,也会导致@Transactianal失效

Spring容器启动流程是怎样的

  1. 在创建Spring容器,也就是启动Spring时:
  2. 首先会进行扫描,扫描得到所有的BeanDefinition对象,并存在一个Map中
  3. 然后筛选出非懒加载的单例BeanDefinition进行创建Bean,对于多例Bean不需要在启动过程中去进行创建,对于多例Bean会在每次获取Bean时利用BeanDefinition去创建
  4. 利用BeanDefinition创建Bean就是Bean的创建生命周期,这期间包括了合并BeanDefinition、推断构造方法、实例化、属性填充、初始化前、初始化、初始化后等步骤,其中AOP就是发生在初始化后这一步骤中
  5. 单例Bean创建完了之后,Spring会发布一个容器启动事件
  6. Spring启动结束
  7. 在源码中会更复杂,比如源码中会提供一些模板方法,让子类来实现,比如源码中还涉及到一些BeanFactoryPostProcessor和BeanPostProcessor的注册,Spring的扫描就是通过BenaFactoryPostProcessor来实现的,依赖注入就是通过BeanPostProcessor来实现的
  8. 在Spring启动过程中还会去处理@Import等注解

2022年金九银十面试题 - 图13

作者:图灵课堂———-周瑜

为什么不推荐使用@Autowired字段注入

  1. @Autowired注解是有Spring提供的,而一旦后续不用Spring了,想用其他的IoC容器时,那@Autowired注解就没有用了,所以建议用@Resource,因为它是Java层面提供的
  2. 另外推荐用构造方法进行依赖注入,因为我们使用字段注入是没有限制的,可以给很多个字段进行注入,而我们通过构造方法注入时,如果有很多属性需要注入,那么构造方法将会有非常多的入参,这时就会提醒你,是否这些依赖都是必须的

Spring用到了哪些设计模式

Spring中的设计模式 (2).png

SpringMVC的底层工作流程

  1. 用户发送请求至前端控制器DispatcherServlet。
  2. DispatcherServlet 收到请求调用 HandlerMapping 处理器映射器。
  3. 处理器映射器找到具体的处理器(可以根据 xml 配置、注解进行查找),生成处理器及处理器拦截器(如果有则生成)一并返回给 DispatcherServlet。
  4. DispatcherServlet 调用 HandlerAdapter 处理器适配器。
  5. HandlerAdapter 经过适配调用具体的处理器(Controller,也叫后端控制器)
  6. Controller 执行完成返回 ModelAndView。
  7. HandlerAdapter 将 controller 执行结果 ModelAndView 返回给 DispatcherServlet。
  8. DispatcherServlet 将 ModelAndView 传给 ViewReslover 视图解析器。
  9. ViewReslover 解析后返回具体 View。
  10. DispatcherServlet 根据 View 进行渲染视图(即将模型数据填充至视图中)。
  11. DispatcherServlet 响应用户。

SpringBoot中常用注解及其底层实现

  1. @SpringBootApplication注解:这个注解标识了一个SpringBoot工程,它实际上是另外三个注解的组合,这三个注解是:
    1. @SpringBootConfiguration:这个注解实际就是一个@Configuration,表示启动类也是一个配置类
    2. @EnableAutoConfiguration:向Spring容器中导入了一个Selector,用来加载ClassPath下SpringFactories中所定义的自动配置类,将这些自动加载为配置Bean
    3. @ComponentScan:标识扫描路径,因为默认是没有配置实际扫描路径,所以SpringBoot扫描的路径是启动类所在的当前目录
  2. @Bean注解:用来定义Bean,类似于XML中的标签,Spring在启动时,会对加了@Bean注解的方法进行解析,将方法的名字做为beanName,并通过执行方法得到bean对象
  3. @Controller、@Service、@ResponseBody、@Autowired都可以说

SpringBoot是如何启动Tomcat的

  1. 首先,SpringBoot在启动时会先创建一个Spring容器
  2. 在创建Spring容器过程中,会利用@ConditionalOnClass技术来判断当前classpath中是否存在Tomcat依赖,如果存在则会生成一个启动Tomcat的Bean
  3. Spring容器创建完之后,就会获取启动Tomcat的Bean,并创建Tomcat对象,并绑定端口等,然后启动Tomcat

SpringBoot中配置文件的加载顺序是怎样的?

优先级从高到低,高优先级的配置覆盖低优先级的配置,所有配置会形成互补配置。

  1. 命令行参数。所有的配置都可以在命令行上进行指定;
  2. Java系统属性(System.getProperties());
  3. 操作系统环境变量 ;
  4. jar包外部的application-{profile}.properties或application.yml(带spring.profile)配置文件
  5. jar包内部的application-{profile}.properties或application.yml(带spring.profile)配置文件 再来加载不带profile
  6. jar包外部的application.properties或application.yml(不带spring.profile)配置文件
  7. jar包内部的application.properties或application.yml(不带spring.profile)配置文件
  8. @Configuration注解类上的@PropertySource

Mybatis存在哪些优点和缺点

优点:

  1. 基于 SQL 语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL单独写,解除 sql 与程序代码的耦合,便于统一管理。
  2. 与 JDBC 相比,减少了 50%以上的代码量,消除了 JDBC 大量冗余的代码,不需要手动开关连接;
  3. 很好的与各种数据库兼容( 因为 MyBatis 使用 JDBC 来连接数据库,所以只要JDBC 支持的数据库MyBatis 都支持)。
  4. 能够与 Spring 很好的集成;
  5. 提供映射标签, 支持对象与数据库的 ORM 字段关系映射; 提供对象关系映射标签, 支持对象关系组件维护。

缺点:

  1. SQL 语句的编写工作量较大, 尤其当字段多、关联表多时, 对开发人员编写SQL 语句的功底有一定要求。
  2. SQL 语句依赖于数据库, 导致数据库移植性差, 不能随意更换数据库

Mybatis中#{}和${}的区别是什么?

  1. {}是预编译处理、是占位符, ${}是字符串替换、是拼接符

  2. Mybatis 在处理#{}时,会将 sql 中的#{}替换为?号,调用 PreparedStatement 来赋值
  3. Mybatis 在处理${}时, 就是把${}替换成变量的值,调用 Statement 来赋值
  4. 使用#{}可以有效的防止SQL注入,提高系统安全性
  1. -- 假设
  2. name="zhouyu"
  3. password="1 or 1=1"
  4. select * from user where name = #{name} and password = #{password} 将转为
  5. select * from user where name = 'zhouyu' and password = '1 or 1=1'
  6. select * from user where name = ${name} and password = ${password} 将转为
  7. select * from user where name = zhouyu and password = 1 or 1=1

什么是CAP理论

CAP理论是分布式领域中非常重要的一个指导理论,C(Consistency)表示强一致性,A(Availability)表示可用性,P(Partition Tolerance)表示分区容错性,CAP理论指出在目前的硬件条件下,一个分布式系统是必须要保证分区容错性的,而在这个前提下,分布式系统要么保证CP,要么保证AP,无法同时保证CAP。

分区容错性表示,一个系统虽然是分布式的,但是对外看上去应该是一个整体,不能由于分布式系统内部的某个结点挂点,或网络出现了故障,而导致系统对外出现异常。所以,对于分布式系统而言是一定要保证分区容错性的。

强一致性表示,一个分布式系统中各个结点之间能及时的同步数据,在数据同步过程中,是不能对外提供服务的,不然就会造成数据不一致,所以强一致性和可用性是不能同时满足的。

可用性表示,一个分布式系统对外要保证可用。

什么是BASE理论

由于不能同时满足CAP,所以出现了BASE理论:

  1. BA:Basically Available,表示基本可用,表示可以允许一定程度的不可用,比如由于系统故障,请求时间变长,或者由于系统故障导致部分非核心功能不可用,都是允许的
  2. S:Soft state:表示分布式系统可以处于一种中间状态,比如数据正在同步
  3. E:Eventually consistent,表示最终一致性,不要求分布式系统数据实时达到一致,允许在经过一段时间后再达到一致,在达到一致过程中,系统也是可用的

    什么是RPC

    RPC,表示远程过程调用,对于Java这种面试对象语言,也可以理解为远程方法调用,RPC调用和HTTP调用是有区别的,RPC表示的是一种调用远程方法的方式,可以使用HTTP协议、或直接基于TCP协议来实现RPC,在Java中,我们可以通过直接使用某个服务接口的代理对象来执行方法,而底层则通过构造HTTP请求来调用远端的方法,所以,有一种说法是RPC协议是HTTP协议之上的一种协议,也是可以理解的。

分布式ID是什么?有哪些解决方案?

在开发中,我们通常会需要一个唯一ID来标识数据,如果是单体架构,我们可以通过数据库的主键,或直接在内存中维护一个自增数字来作为ID都是可以的,但对于一个分布式系统,就会有可能会出现ID冲突,此时有以下解决方案:

  1. uuid,这种方案复杂度最低,但是会影响存储空间和性能
  2. 利用单机数据库的自增主键,作为分布式ID的生成器,复杂度适中,ID长度较之uuid更短,但是受到单机数据库性能的限制,并发量大的时候,此方案也不是最优方案
  3. 利用redis、zookeeper的特性来生成id,比如redis的自增命令、zookeeper的顺序节点,这种方案和单机数据库(mysql)相比,性能有所提高,可以适当选用
  4. 雪花算法,一切问题如果能直接用算法解决,那就是最合适的,利用雪花算法也可以生成分布式ID,底层原理就是通过某台机器在某一毫秒内对某一个数字自增,这种方案也能保证分布式架构中的系统id唯一,但是只能保证趋势递增。业界存在tinyid、leaf等开源中间件实现了雪花算法。

分布式锁的使用场景是什么?有哪些实现方案?

在单体架构中,多个线程都是属于同一个进程的,所以在线程并发执行时,遇到资源竞争时,可以利用ReentrantLock、synchronized等技术来作为锁,来控制共享资源的使用。

而在分布式架构中,多个线程是可能处于不同进程中的,而这些线程并发执行遇到资源竞争时,利用ReentrantLock、synchronized等技术是没办法来控制多个进程中的线程的,所以需要分布式锁,意思就是,需要一个分布式锁生成器,分布式系统中的应用程序都可以来使用这个生成器所提供的锁,从而达到多个进程中的线程使用同一把锁。

目前主流的分布式锁的实现方案有两种:

  1. zookeeper:利用的是zookeeper的临时节点、顺序节点、watch机制来实现的,zookeeper分布式锁的特点是高一致性,因为zookeeper保证的是CP,所以由它实现的分布式锁更可靠,不会出现混乱
  2. redis:利用redis的setnx、lua脚本、消费订阅等机制来实现的,redis分布式锁的特点是高可用,因为redis保证的是AP,所以由它实现的分布式锁可能不可靠,不稳定(一旦redis中的数据出现了不一致),可能会出现多个客户端同时加到锁的情况

什么是分布式事务?有哪些实现方案?

在分布式系统中,一次业务处理可能需要多个应用来实现,比如用户发送一次下单请求,就涉及到订单系统创建订单、库存系统减库存,而对于一次下单,订单创建与减库存应该是要同时成功或同时失败的,但在分布式系统中,如果不做处理,就很有可能出现订单创建成功,但是减库存失败,那么解决这类问题,就需要用到分布式事务。常用解决方案有:

  1. 本地消息表:创建订单时,将减库存消息加入在本地事务中,一起提交到数据库存入本地消息表,然后调用库存系统,如果调用成功则修改本地消息状态为成功,如果调用库存系统失败,则由后台定时任务从本地消息表中取出未成功的消息,重试调用库存系统
  2. 消息队列:目前RocketMQ中支持事务消息,它的工作原理是:
    1. 生产者订单系统先发送一条half消息到Broker,half消息对消费者而言是不可见的
    2. 再创建订单,根据创建订单成功与否,向Broker发送commit或rollback
    3. 并且生产者订单系统还可以提供Broker回调接口,当Broker发现一段时间half消息没有收到任何操作命令,则会主动调此接口来查询订单是否创建成功
    4. 一旦half消息commit了,消费者库存系统就会来消费,如果消费成功,则消息销毁,分布式事务成功结束
    5. 如果消费失败,则根据重试策略进行重试,最后还失败则进入死信队列,等待进一步处理
  3. Seata:阿里开源的分布式事务框架,支持AT、TCC等多种模式,底层都是基于两阶段提交理论来实现的

image.png

什么是ZAB协议

ZAB协议是Zookeeper用来实现一致性的原子广播协议,该协议描述了Zookeeper是如何实现一致性的,分为三个阶段:

  1. 领导者选举阶段:从Zookeeper集群中选出一个节点作为Leader,所有的写请求都会由Leader节点来处理
  2. 数据同步阶段:集群中所有节点中的数据要和Leader节点保持一致,如果不一致则要进行同步
  3. 请求广播阶段:当Leader节点接收到写请求时,会利用两阶段提交来广播该写请求,使得写请求像事务一样在其他节点上执行,达到节点上的数据实时一致

但值得注意的是,Zookeeper只是尽量的在达到强一致性,实际上仍然只是最终一致性的。

为什么Zookeeper可以用来作为注册中心

可以利用Zookeeper的临时节点和watch机制来实现注册中心的自动注册和发现,另外Zookeeper中的数据都是存在内存中的,并且Zookeeper底层采用了nio,多线程模型,所以Zookeeper的性能也是比较高的,所以可以用来作为注册中心,但是如果考虑到注册中心应该是注册可用性的话,那么Zookeeper则不太合适,因为Zookeeper是CP的,它注重的是一致性,所以集群数据不一致时,集群将不可用,所以用Redis、Eureka、Nacos来作为注册中心将更合适。

Zookeeper中的领导者选举的流程是怎样的?

对于Zookeeper集群,整个集群需要从集群节点中选出一个节点作为Leader,大体流程如下:

  1. 集群中各个节点首先都是观望状态(LOOKING),一开始都会投票给自己,认为自己比较适合作为leader
  2. 然后相互交互投票,每个节点会收到其他节点发过来的选票,然后pk,先比较zxid,zxid大者获胜,zxid如果相等则比较myid,myid大者获胜
  3. 一个节点收到其他节点发过来的选票,经过PK后,如果PK输了,则改票,此节点就会投给zxid或myid更大的节点,并将选票放入自己的投票箱中,并将新的选票发送给其他节点
  4. 如果pk是平局则将接收到的选票放入自己的投票箱中
  5. 如果pk赢了,则忽略所接收到的选票
  6. 当然一个节点将一张选票放入到自己的投票箱之后,就会从投票箱中统计票数,看是否超过一半的节点都和自己所投的节点是一样的,如果超过半数,那么则认为当前自己所投的节点是leader
  7. 集群中每个节点都会经过同样的流程,pk的规则也是一样的,一旦改票就会告诉给其他服务器,所以最终各个节点中的投票箱中的选票也将是一样的,所以各个节点最终选出来的leader也是一样的,这样集群的leader就选举出来了

    Zookeeper集群中节点之间数据是如何同步的

  8. 首先集群启动时,会先进行领导者选举,确定哪个节点是Leader,哪些节点是Follower和Observer

  9. 然后Leader会和其他节点进行数据同步,采用发送快照和发送Diff日志的方式
  10. 集群在工作过程中,所有的写请求都会交给Leader节点来进行处理,从节点只能处理读请求
  11. Leader节点收到一个写请求时,会通过两阶段机制来处理
  12. Leader节点会将该写请求对应的日志发送给其他Follower节点,并等待Follower节点持久化日志成功
  13. Follower节点收到日志后会进行持久化,如果持久化成功则发送一个Ack给Leader节点
  14. 当Leader节点收到半数以上的Ack后,就会开始提交,先更新Leader节点本地的内存数据
  15. 然后发送commit命令给Follower节点,Follower节点收到commit命令后就会更新各自本地内存数据
  16. 同时Leader节点还是将当前写请求直接发送给Observer节点,Observer节点收到Leader发过来的写请求后直接执行更新本地内存数据
  17. 最后Leader节点返回客户端写请求响应成功
  18. 通过同步机制和两阶段提交机制来达到集群中节点数据一致

    Dubbo支持哪些负载均衡策略

  19. 随机:从多个服务提供者随机选择一个来处理本次请求,调用量越大则分布越均匀,并支持按权重设置随机概率

  20. 轮询:依次选择服务提供者来处理请求, 并支持按权重进行轮询,底层采用的是平滑加权轮询算法
  21. 最小活跃调用数:统计服务提供者当前正在处理的请求,下次请求过来则交给活跃数最小的服务器来处理
  22. 一致性哈希:相同参数的请求总是发到同一个服务提供者

https://www.yuque.com/renyong-jmovm/ds/gwu187#yGxRv

Dubbo是如何完成服务导出的?

  1. 首先Dubbo会将程序员所使用的@DubboService注解或@Service注解进行解析得到程序员所定义的服务参数,包括定义的服务名、服务接口、服务超时时间、服务协议等等,得到一个ServiceBean。
  2. 然后调用ServiceBean的export方法进行服务导出
  3. 然后将服务信息注册到注册中心,如果有多个协议,多个注册中心,那就将服务按单个协议,单个注册中心进行注册
  4. 将服务信息注册到注册中心后,还会绑定一些监听器,监听动态配置中心的变更
  5. 还会根据服务协议启动对应的Web服务器或网络框架,比如Tomcat、Netty等

    Dubbo是如何完成服务引入的?

  6. 当程序员使用@Reference注解来引入一个服务时,Dubbo会将注解和服务的信息解析出来,得到当前所引用的服务名、服务接口是什么

  7. 然后从注册中心进行查询服务信息,得到服务的提供者信息,并存在消费端的服务目录中
  8. 并绑定一些监听器用来监听动态配置中心的变更
  9. 然后根据查询得到的服务提供者信息生成一个服务接口的代理对象,并放入Spring容器中作为Bean

    Dubbo的架构设计是怎样的?

    Dubbo中的架构设计是非常优秀的,分为了很多层次,并且每层都是可以扩展的,比如:

  10. Proxy服务代理层,支持JDK动态代理、javassist等代理机制

  11. Registry注册中心层,支持Zookeeper、Redis等作为注册中心
  12. Protocol远程调用层,支持Dubbo、Http等调用协议
  13. Transport网络传输层,支持netty、mina等网络传输框架
  14. Serialize数据序列化层,支持JSON、Hessian等序列化机制

各层说明

  • config 配置层:对外配置接口,以 ServiceConfig, ReferenceConfig 为中心,可以直接初始化配置类,也可以通过 spring 解析配置生成配置类
  • proxy 服务代理层:服务接口透明代理,生成服务的客户端 Stub 和服务器端 Skeleton, 以 ServiceProxy 为中心,扩展接口为 ProxyFactory
  • registry 注册中心层:封装服务地址的注册与发现,以服务 URL 为中心,扩展接口为 RegistryFactory, Registry, RegistryService
  • cluster 路由层:封装多个提供者的路由及负载均衡,并桥接注册中心,以 Invoker 为中心,扩展接口为 Cluster, Directory, Router, LoadBalance
  • monitor 监控层:RPC 调用次数和调用时间监控,以 Statistics 为中心,扩展接口为 MonitorFactory, Monitor, MonitorService
  • protocol 远程调用层:封装 RPC 调用,以 Invocation, Result 为中心,扩展接口为 Protocol, Invoker, Exporter
  • exchange 信息交换层:封装请求响应模式,同步转异步,以 Request, Response 为中心,扩展接口为 Exchanger, ExchangeChannel, ExchangeClient, ExchangeServer
  • transport 网络传输层:抽象 mina 和 netty 为统一接口,以 Message 为中心,扩展接口为 Channel, Transporter, Client, Server, Codec
  • serialize 数据序列化层:可复用的一些工具,扩展接口为 Serialization, ObjectInput, ObjectOutput, ThreadPool

关系说明

  • 在 RPC 中,Protocol 是核心层,也就是只要有 Protocol + Invoker + Exporter 就可以完成非透明的 RPC 调用,然后在 Invoker 的主过程上 Filter 拦截点。
  • 图中的 Consumer 和 Provider 是抽象概念,只是想让看图者更直观的了解哪些类分属于客户端与服务器端,不用 Client 和 Server 的原因是 Dubbo 在很多场景下都使用 Provider, Consumer, Registry, Monitor 划分逻辑拓普节点,保持统一概念。
  • 而 Cluster 是外围概念,所以 Cluster 的目的是将多个 Invoker 伪装成一个 Invoker,这样其它人只要关注 Protocol 层 Invoker 即可,加上 Cluster 或者去掉 Cluster 对其它层都不会造成影响,因为只有一个提供者时,是不需要 Cluster 的。
  • Proxy 层封装了所有接口的透明化代理,而在其它层都以 Invoker 为中心,只有到了暴露给用户使用时,才用 Proxy 将 Invoker 转成接口,或将接口实现转成 Invoker,也就是去掉 Proxy 层 RPC 是可以 Run 的,只是不那么透明,不那么看起来像调本地服务一样调远程服务。
  • 而 Remoting 实现是 Dubbo 协议的实现,如果你选择 RMI 协议,整个 Remoting 都不会用上,Remoting 内部再划为 Transport 传输层和 Exchange 信息交换层,Transport 层只负责单向消息传输,是对 Mina, Netty, Grizzly 的抽象,它也可以扩展 UDP 传输,而 Exchange 层是在传输层之上封装了 Request-Response 语义。
  • Registry 和 Monitor 实际上不算一层,而是一个独立的节点,只是为了全局概览,用层的方式画在一起。

2022年金九银十面试题 - 图16

Spring Cloud有哪些常用组件,作用是什么?

  1. Eureka:注册中心
  2. Nacos:注册中心、配置中心
  3. Consul:注册中心、配置中心
  4. Spring Cloud Config:配置中心
  5. Feign/OpenFeign:RPC调用
  6. Kong:服务网关
  7. Zuul:服务网关
  8. Spring Cloud Gateway:服务网关
  9. Ribbon:负载均衡
  10. Spring CLoud Sleuth:链路追踪
  11. Zipkin:链路追踪
  12. Seata:分布式事务
  13. Dubbo:RPC调用
  14. Sentinel:服务熔断
  15. Hystrix:服务熔断

    Spring Cloud和Dubbo有哪些区别?

    Spring Cloud是一个微服务框架,提供了微服务领域中的很多功能组件,Dubbo一开始是一个RPC调用框架,核心是解决服务调用间的问题,Spring Cloud是一个大而全的框架,Dubbo则更侧重于服务调用,所以Dubbo所提供的功能没有Spring Cloud全面,但是Dubbo的服务调用性能比Spring Cloud高,不过Spring Cloud和Dubbo并不是对立的,是可以结合起来一起使用的。

    什么是服务雪崩?什么是服务限流?

  16. 当服务A调用服务B,服务B调用C,此时大量请求突然请求服务A,假如服务A本身能抗住这些请求,但是如果服务C抗不住,导致服务C请求堆积,从而服务B请求堆积,从而服务A不可用,这就是服务雪崩,解决方式就是服务降级和服务熔断。

  17. 服务限流是指在高并发请求下,为了保护系统,可以对访问服务的请求进行数量上的限制,从而防止系统不被大量请求压垮,在秒杀中,限流是非常重要的。
    1. 固定窗口(计数器)算法
    2. 滑动窗口算法
    3. 令牌桶算法
    4. 漏桶算法

什么是服务熔断?什么是服务降级?区别是什么?

  1. 服务熔断是指,当服务A调用的某个服务B不可用时,上游服务A为了保证自己不受影响,从而不再调用服务B,直接返回一个结果,减轻服务A和服务B的压力,直到服务B恢复。
  2. 服务降级是指,当发现系统压力过载时,可以通过关闭某个服务,或限流某个服务来减轻系统压力,这就是服务降级。

相同点:

  1. 都是为了防止系统崩溃
  2. 都让用户体验到某些功能暂时不可用

不同点:熔断是下游服务故障触发的,降级是为了降低系统负载

SOA、分布式、微服务之间有什么关系和区别?

  1. 分布式架构是指将单体架构中的各个部分拆分,然后部署不同的机器或进程中去,SOA和微服务基本上都是分布式架构的
  2. SOA是一种面向服务的架构,系统的所有服务都注册在总线上,当调用服务时,从总线上查找服务信息,然后调用
  3. 微服务是一种更彻底的面向服务的架构,将系统中各个功能个体抽成一个个小的应用程序,基本保持一个应用对应的一个服务的架构

    BIO、NIO、AIO分别是什么

  4. BIO:同步阻塞IO,使用BIO读取数据时,线程会阻塞住,并且需要线程主动去查询是否有数据可读,并且需要处理完一个Socket之后才能处理下一个Socket

  5. NIO:同步非阻塞IO,使用NIO读取数据时,线程不会阻塞,但需要线程主动的去查询是否有IO事件
  6. AIO:也叫做NIO 2.0,异步非阻塞IO,使用AIO读取数据时,线程不会阻塞,并且当有数据可读时会通知给线程,不需要线程主动去查询

    零拷贝是什么

    零拷贝指的是,应用程序在需要把内核中的一块区域数据转移到另外一块内核区域去时,不需要经过先复制到用户空间,再转移到目标内核区域去了,而直接实现转移。

2022年金九银十面试题 - 图17 2022年金九银十面试题 - 图18

Netty是什么?和Tomcat有什么区别?特点是什么?

Netty是一个基于NIO的异步网络通信框架,性能高,封装了原生NIO编码的复杂度,开发者可以直接使用Netty来开发高效率的各种网络服务器,并且编码简单。

Tomcat是一个Web服务器,是一个Servlet容器,基本上Tomcat内部只会运行Servlet程序,并处理HTTP请求,而Netty封装的是底层IO模型,关注的是网络数据的传输,而不关心具体的协议,可定制性更高。

Netty的特点:

  1. 异步、NIO的网络通信框架
  2. 高性能
  3. 高扩展,高定制性
  4. 易用性

    Netty的线程模型是怎么样的

Netty同时支持Reactor单线程模型 、Reactor多线程模型和Reactor主从多线程模型,用户可根据启动参数配置在这三种模型之间切换。

image.png
服务端启动时,通常会创建两个NioEventLoopGroup实例,对应了两个独立的Reactor线程池,bossGroup负责处理客户端的连接请求,workerGroup负责处理I/O相关的操作,执行系统Task、定时任务Task等。用户可根据服务端引导类ServerBootstrap配置参数选择Reactor线程模型,进而最大限度地满足用户的定制化需求。

Netty的高性能体现在哪些方面

  1. NIO模型,用最少的资源做更多的事情。
  2. 内存零拷贝,尽量减少不必要的内存拷贝,实现了更高效率的传输。
  3. 内存池设计,申请的内存可以重用,主要指直接内存。内部实现是用一颗二叉查找树管理内存分配情况。
  4. 串行化处理读写 :避免使用锁带来的性能开销。即消息的处理尽可能再同一个线程内完成,期间不进行线程切换,这样就避免了多线程竞争和同步锁。表面上看,串行化设计似乎CPU利用率不高,并发程度不够。但是,通过调整NIO线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队里-多个工作线程模型性能更优。
  5. 高性能序列化协议 :支持protobuf等高性能序列化协议。
  6. 高效并发编程的体现 :volatile的大量、正确使用;CAS和原子类的广泛使用;线程安全容器的使用;通过读写锁提升并发性能。

Redis有哪些数据结构?分别有哪些典型的应用场景?

Redis的数据结构有:

  1. 字符串:可以用来做最简单的数据,可以缓存某个简单的字符串,也可以缓存某个json格式的字符串,Redis分布式锁的实现就利用了这种数据结构,还包括可以实现计数器、分布式ID
  2. 哈希表:可以用来存储一些key-value对,更适合用来存储对象
  3. 列表:Redis的列表通过命令的组合,既可以当做栈,也可以当做队列来使用,可以用来缓存类似微信公众号、微博等消息流数据
  4. 集合:和列表类似,也可以存储多个元素,但是不能重复,集合可以进行交集、并集、差集操作,从而可以实现类似,我和某人共同关注的人、朋友圈点赞等功能
  5. 有序集合:集合是无序的,有序集合可以设置顺序,可以用来实现排行榜功能

Redis分布式锁底层是如何实现的?

  1. 首先利用setnx来保证:如果key不存在才能获取到锁,如果key存在,则获取不到锁
  2. 然后还要利用lua脚本来保证多个redis操作的原子性
  3. 同时还要考虑到锁过期,所以需要额外的一个看门狗定时任务来监听锁是否需要续约
  4. 同时还要考虑到redis节点挂掉后的情况,所以需要采用红锁的方式来同时向N/2+1个节点申请锁,都申请到了才证明获取锁成功,这样就算其中某个redis节点挂掉了,锁也不能被其他客户端获取到

    Redis主从复制的核心原理

    全量同步:

  5. 一般发生在从节点初始化的时候

  6. 从节点发送SYNC命令连接主节点
  7. 主节点接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件,并使用缓冲区replication buffer记录在这个过程中接收到的写命令
  8. 主节点BGSAVE命令执行完后,向所有从节点发送RDB文件,并在发送缓冲区replication buffer记录的写命令
  9. 从节点收到RDB文件后丢弃所有旧数据,载入收到的RDB文件中的数据
  10. 主节点RDB文件发送完毕后,开始向从节点发送缓冲区replication buffer中的写命令
  11. 从节点完成RDB的载入后,开始接收客户端命令,并执行来自主节点缓冲区replication buffer的写命令

增量同步:
Redis主节点每执行一个写命令就会向从节点异步发送相同的写命令,从节点接收并执行收到的写命令

缓存穿透、缓存击穿、缓存雪崩分别是什么

缓存中存放的大多都是热点数据,目的就是防止请求可以直接从缓存中获取到数据,而不用访问Mysql。

  1. 缓存雪崩:如果缓存中某一时刻大批热点数据同时过期,那么就可能导致大量请求直接访问Mysql了,解决办法就是在过期时间上增加一点随机值,另外如果搭建一个高可用的Redis集群也是防止缓存雪崩的有效手段
  2. 缓存击穿:和缓存雪崩类似,缓存雪崩是大批热点数据失效,而缓存击穿是指某一个热点key突然失效,也导致了大量请求直接访问Mysql数据库,这就是缓存击穿,解决方案就是考虑这个热点key不设过期时间
  3. 缓存穿透:假如某一时刻访问redis的大量key都在redis中不存在(比如黑客故意伪造一些乱七八糟的key),那么也会给数据造成压力,这就是缓存穿透,解决方案是使用布隆过滤器,它的作用就是如果它认为一个key不存在,那么这个key就肯定不存在,所以可以在缓存之前加一层布隆过滤器来拦截不存在的key

    Redis和Mysql如何保证数据一致

  4. 先更新Mysql,再更新Redis,如果更新Redis失败,可能仍然不一致

  5. 先删除Redis缓存数据,再更新Mysql,再次查询的时候在将数据添加到缓存中,这种方案能解决1方案的问题,但是在高并发下性能较低,而且仍然会出现数据不一致的问题,比如线程1删除了Redis缓存数据,正在更新Mysql,此时另外一个查询再查询,那么就会把Mysql中老数据又查到Redis中
  6. 延时双删,步骤是:先删除Redis缓存数据,再更新Mysql,延迟几百毫秒再删除Redis缓存数据,这样就算在更新Mysql时,有其他线程读了Mysql,把老数据读到了Redis中,那么也会被删除掉,从而把数据保持一致

Explain语句结果中各个字段分表表示什么

列名 描述
id 查询语句中每出现一个SELECT关键字,MySQL就会为它分配一个唯一的id值,某些子查询会被优化为join查询,那么出现的id会一样
select_type SELECT关键字对应的那个查询的类型
table 表名
partitions 匹配的分区信息
type 针对单表的查询方式(全表扫描、索引)
possible_keys 可能用到的索引
key 实际上使用的索引
key_len 实际使用到的索引长度
ref 当使用索引列等值查询时,与索引列进行等值匹配的对象信息
rows 预估的需要读取的记录条数
filtered 某个表经过搜索条件过滤后剩余记录条数的百分比
Extra 一些额外的信息,比如排序等

索引覆盖是什么

索引覆盖就是一个SQL在执行时,可以利用索引来快速查找,并且此SQL所要查询的字段在当前索引对应的字段中都包含了,那么就表示此SQL走完索引后不用回表了,所需要的字段都在当前索引的叶子节点上存在,可以直接作为结果返回了

最左前缀原则是什么

当一个SQL想要利用索引是,就一定要提供该索引所对应的字段中最左边的字段,也就是排在最前面的字段,比如针对a,b,c三个字段建立了一个联合索引,那么在写一个sql时就一定要提供a字段的条件,这样才能用到联合索引,这是由于在建立a,b,c三个字段的联合索引时,底层的B+树是按照a,b,c三个字段从左往右去比较大小进行排序的,所以如果想要利用B+树进行快速查找也得符合这个规则

Innodb是如何实现事务的

Innodb通过Buffer Pool,LogBuffer,Redo Log,Undo Log来实现事务,以一个update语句为例:

  1. Innodb在收到一个update语句后,会先根据条件找到数据所在的页,并将该页缓存在Buffer Pool中
  2. 执行update语句,修改Buffer Pool中的数据,也就是内存中的数据
  3. 针对update语句生成一个RedoLog对象,并存入LogBuffer中
  4. 针对update语句生成undolog日志,用于事务回滚
  5. 如果事务提交,那么则把RedoLog对象进行持久化,后续还有其他机制将Buffer Pool中所修改的数据页持久化到磁盘中
  6. 如果事务回滚,则利用undolog日志进行回滚

B树和B+树的区别,为什么Mysql使用B+树

B树的特点:

  1. 节点排序
  2. 一个节点了可以存多个元素,多个元素也排序了

B+树的特点:

  1. 拥有B树的特点
  2. 叶子节点之间有指针
  3. 非叶子节点上的元素在叶子节点上都冗余了,也就是叶子节点中存储了所有的元素,并且排好顺序

Mysql索引使用的是B+树,因为索引是用来加快查询的,而B+树通过对数据进行排序所以是可以提高查询速度的,然后通过一个节点中可以存储多个元素,从而可以使得B+树的高度不会太高,在Mysql中一个Innodb页就是一个B+树节点,一个Innodb页默认16kb,所以一般情况下一颗两层的B+树可以存2000万行左右的数据,然后通过利用B+树叶子节点存储了所有数据并且进行了排序,并且叶子节点之间有指针,可以很好的支持全表扫描,范围查找等SQL语句。

Mysql锁有哪些,如何理解

按锁粒度分类:

  1. 行锁:锁某行数据,锁粒度最小,并发度高
  2. 表锁:锁整张表,锁粒度最大,并发度低
  3. 间隙锁:锁的是一个区间

还可以分为:

  1. 共享锁:也就是读锁,一个事务给某行数据加了读锁,其他事务也可以读,但是不能写
  2. 排它锁:也就是写锁,一个事务给某行数据加了写锁,其他事务不能读,也不能写

还可以分为:

  1. 乐观锁:并不会真正的去锁某行记录,而是通过一个版本号来实现的
  2. 悲观锁:上面所的行锁、表锁等都是悲观锁

在事务的隔离级别实现中,就需要利用锁来解决幻读

作者:图灵课堂———-周瑜

InnoDB中的B+树的高度该如何计算

image.png

InnoDB中主键索引对应的B+树:

  1. 叶子节点存储的是完整的一条条的行数据
  2. 非叶子节点存储的是主键和页地址

假设一行数据为1kb,主键为bigint(一个主键占8b),由于Innodb中一页为16kb,一页就是B+树中的一个节点,那也就是B+树中的一个节点能存储16条数据(16kb/1kb),或者一个节点能存储主键:页地址对为16kb/(8b+6b)=1170对,每对对应一个数据页,所以两层高的B+树能存储的数据条数为1170*16=18724条

image.png

如果是三层高的B+树,那就是1170117016=21,907,748条,也就是2000万条左右。

根据这个例子,我们可以结合实际请求,算出两层和三层时能存储的数据条数,然后看目前表里有多少条数据,从而知道B+数的高度是多少。

Mysql慢查询该如何优化?

  1. 检查是否走了索引,如果没有则优化SQL利用索引
  2. 检查所利用的索引,是否是最优索引
  3. 检查所查字段是否都是必须的,是否查询了过多字段,查出了多余数据
  4. 检查表中数据是否过多,是否应该进行分库分表了
  5. 检查数据库实例所在机器的性能配置,是否太低,是否可以适当增加资源

    消息队列有哪些作用

  6. 解耦:使用消息队列来作为两个系统之间的通讯方式,两个系统不需要相互依赖了

  7. 异步:系统A给消息队列发送完消息之后,就可以继续做其他事情了
  8. 流量削峰:如果使用消息队列的方式来调用某个系统,那么消息将在队列中排队,由消费者自己控制消费速度

    死信队列是什么?延时队列是什么?

  9. 死信队列也是一个消息队列,它是用来存放那些没有成功消费的消息的,通常可以用来作为消息重试

  10. 延时队列就是用来存放需要在指定时间被处理的元素的队列,通常可以用来处理一些具有过期性操作的业务,比如十分钟内未支付则取消订单

    Kafka为什么吞吐量高

    Kafka的生产者采用的是异步发送消息机制,当发送一条消息时,消息并没有发送到Broker而是缓存起来,然后直接向业务返回成功,当缓存的消息达到一定数量时再批量发送给Broker。这种做法减少了网络io,从而提高了消息发送的吞吐量,但是如果消息生产者宕机,会导致消息丢失,业务出错,所以理论上kafka利用此机制提高了性能却降低了可靠性。

    Kafka的Pull和Push分别有什么优缺点

  11. pull表示消费者主动拉取,可以批量拉取,也可以单条拉取,所以pull可以由消费者自己控制,根据自己的消息处理能力来进行控制,但是消费者不能及时知道是否有消息,可能会拉到的消息为空

  12. push表示Broker主动给消费者推送消息,所以肯定是有消息时才会推送,但是消费者不能按自己的能力来消费消息,推过来多少消息,消费者就得消费多少消息,所以可能会造成网络堵塞,消费者压力大等问题

    RocketMQ的事务消息是如何实现的

    image.png

    1. 生产者订单系统先发送一条half消息到Broker,half消息对消费者而言是不可见的
    2. 再创建订单,根据创建订单成功与否,向Broker发送commit或rollback
    3. 并且生产者订单系统还可以提供Broker回调接口,当Broker发现一段时间half消息没有收到任何操作命令,则会主动调此接口来查询订单是否创建成功
    4. 一旦half消息commit了,消费者库存系统就会来消费,如果消费成功,则消息销毁,分布式事务成功结束
    5. 如果消费失败,则根据重试策略进行重试,最后还失败则进入死信队列,等待进一步处理

      消息队列如何保证消息可靠传输

      消息可靠传输代表了两层意思,既不能多也不能少。
  13. 为了保证消息不多,也就是消息不能重复,也就是生产者不能重复生产消息,或者消费者不能重复消费消息

  14. 首先要确保消息不多发,这个不常出现,也比较难控制,因为如果出现了多发,很大的原因是生产者自己的原因,如果要避免出现问题,就需要在消费端做控制
  15. 要避免不重复消费,最保险的机制就是消费者实现幂等性,保证就算重复消费,也不会有问题,通过幂等性,也能解决生产者重复发送消息的问题
  16. 消息不能少,意思就是消息不能丢失,生产者发送的消息,消费者一定要能消费到,对于这个问题,就要考虑两个方面
  17. 生产者发送消息时,要确认broker确实收到并持久化了这条消息,比如RabbitMQ的confirm机制,Kafka的ack机制都可以保证生产者能正确的将消息发送给broker
  18. broker要等待消费者真正确认消费到了消息时才删除掉消息,这里通常就是消费端ack机制,消费者接收到一条消息后,如果确认没问题了,就可以给broker发送一个ack,broker接收到ack后才会删除消息

    TCP的三次握手和四次挥手

    TCP协议是7层网络协议中的传输层协议,负责数据的可靠传输。
    在建立TCP连接时,需要通过三次握手来建立,过程是:

  19. 客户端向服务端发送一个SYN

  20. 服务端接收到SYN后,给客户端发送一个SYN_ACK
  21. 客户端接收到SYN_ACK后,再给服务端发送一个ACK

在断开TCP连接时,需要通过四次挥手来断开,过程是:

  1. 客户端向服务端发送FIN
  2. 服务端接收FIN后,向客户端发送ACK,表示我接收到了断开连接的请求,客户端你可以不发数据了,不过服务端这边可能还有数据正在处理
  3. 服务端处理完所有数据后,向客户端发送FIN,表示服务端现在可以断开连接
  4. 客户端收到服务端的FIN,向服务端发送ACK,表示客户端也会断开连接了