一、JAVASE基础:

1.1 面向对象有哪些特征以及你对这些特性的理解?

1.1.1 继承:

继承:继承是从已有类得到继承信息创建新类的过程。提供继承信息的类叫做父类(超类,基类),得到继承信息的类叫做子类(派生类)。继承让变化中的软件系统有了一定的延续性,同时继承也是封装程序可变因素的重要手段。

1.1.2 封装:

封装:封装是把数据和操作数据的方法进行绑定起来,对数据的访问只能通过已定义的接口进行访问。在编写类的过程中编写的方法就是对实现细节的封装,编写一个类就是对数据和数据操作的封装。可以说,封装就是隐藏一切可隐藏的东西,只向外界提供最简单的编程接口。

1.1.3 多态:

1.多态是指允许不同子类型的对象对同一操作做出不同的反应。简单的说是同一对象引用调用同一个方法却做了不同的事情。
2.多态分为编译时多态和运行时多态。方法的重载(overload)实现的是编译时多态(提前绑定),方法的重写(override)实现的是运行时多态(后绑定)。
3.实现多态的条件:
(1)方法重写(子类继承父类并重写父类已有的方法或抽象方法)
(2)对象的构造(用父类引用指向子类引用,同样的引用调用同样的方法就会根据子类对象的不同表现做出不同的行为)

1.1.4 抽象:

抽象是将一类对象的共同特征总结出来构造类的过程,包括数据的抽象和行为抽象。抽象只关注对象有哪些属性和方法,并不关心实现的细节。

1.2 权限修饰符的作用域?

修饰符 当前类 同包 子类 其它包
public
protected ×
default × ×
private × × ×

1.3 如何理解clone对象?

1.3.1 为什么要使用clone:

在实际的编程过程中,当一个A类包含了一些有效的属性和行为是,此时需要一个相同于A类的B类,但是操作B类的同时不会影响A类中的属性和行为(AB类同时独立开来)但是B类的初始值有A类确定。

1.3.2 new一个对象和clone一个对象的区别:

new操作符的本意是分配内存。程序执行到new操作符时,首先去看new操作符后面的类型,因为知道了类型,才能知道要分配多大的内存空间。分配完内存之后,再调用构造函数,填充对象的各个域,这一步叫做对象的初始化,构造方法返回后,一个对象创建完毕,可以把他的引用(地址)发布到外部,在外部就可以使用这个引用操纵这个对象。
clone在第一步是和 new相似的,都是分配内存,调用clone方法时,分配的内存和原对象(即调用clone方法的对象)相同,然后再使用原对象中对应的各个域,填充新对象的域,完成后,clone方法返回,一个新的对象被返回。

面试题1:Java 中操作字符串都有哪些类?它们之间有什么区别?

操作字符串的类有:String、StringBuffer、StringBuilder。
  String 和 StringBuffer、StringBuilder 的区别在于 String 声明的是不可变的对象,每次操作都会生成新的 String 对象,然后将指针指向新的 String 对象,而 StringBuffer、StringBuilder 可以在原有对象的基础上进行操作,所以在经常改变字符串内容的情况下最好不要使用 String。

  而StringBuffer 和 StringBuilder 最大的区别在于,StringBuffer 是线程安全的,而 StringBuilder 是非线程安全的,但 StringBuilder 的性能却高于 StringBuffer,所以在单线程环境下推荐使用 StringBuilder,多线程环境下推荐使用 StringBuffer。
image.png
image.png
image.png

面试题2:请你说一下Error 和 Exception 区别是什么?

image.png
1.Exception和Error体现了java平台设计者对不同异常情况的分类,Exception是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应的处理。
2.Error是指正常情况下,不大可能出现的情况,绝大部分的Error都会导致程序处于非正常的、不可恢复的状态。既然是非正常情况,不便于也不需要捕获。常见的比如OutOfMemoryError之类都是Error的子类。

3.Exception又分为可检查(checked)异常和不可检查(unchecked)异常。可检查异常在源代码里必须显式的进行捕获处理,这是编译期检查的一部分。不可检查时异常是指运行时异常,像NullPointerException、ArrayIndexOutOfBoundsException之类,通常是可以编码避免的逻辑错误,具体根据需要来判断是否需要捕获,并不会在编译期强制要求。

面试题3:== 和 equals 的区别是什么

  • == : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数据类型 == 比较的是值,引用数据类型 == 比较的是内存地址)
  • equals(): 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:
  • 情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于调用了Object类的equals() 方法,也就是通过“==”比较这两个对象。

情况2:类覆盖了 equals() 方法。一般,我们都会覆盖 equals() 方法来两个对象的内容相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。

面试题4:为什么要用 Redis ?业务在哪块儿用到的?

Redis优势:
1.读写性能优异, Redis能读的速度是110000次/s,写的速度是81000次/s。
2.支持数据持久化,支持AOF和RDB两种持久化方式。
3.支持事务,Redis的所有操作都是原子性的,同时Redis还支持对几个操作合并后的原子性执行。
4.数据结构丰富,除了支持string类型的value外还支持hash、set、zset、list等数据结构。
5.支持主从复制,主机会自动将数据同步到从机,可以进行读写分离。
6.支持大量集群节点。

通俗来讲:
假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在数Redis中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。同样,我们可以把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接打到缓存而不是数据库(即半路拦截掉了)。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!
  在我们业务中,包括热点词查询、一些实时排行榜数据、访问量点赞量统计、Session共享等等都可以引入Redis来处理。

追问1:Redis里有哪些数据类型?

image.png
[

](https://blog.csdn.net/qq_39390545/article/details/117478149)

追问2:Redis与Memcached有哪些区别?

image.png

追问3:那Redis怎样防止异常数据不丢失的?如何持久化?

RDB 持久化 (快照)
将某个时间点的所有数据生成快照,存放到硬盘上。当数据量很大时,会很慢。
可以将快照复制到其它服务器从而创建具有相同数据的服务器副本。
如果系统发生故障,将会丢失最后一次创建快照之后的数据。

AOF 持久化(即时更新)
将写命令添加到 AOF 文件(Append Only File)的末尾。
使用 AOF 持久化需要设置同步选项,从而确保写命令同步到磁盘文件上的时机。这是因为对文件进行写入并不会马上将内容同步到磁盘上,而是先存储到缓冲区,然后由操作系统决定什么时候同步到磁盘。

  有以下同步选项(同步频率): always 每个写命令都同步;everysec 每秒同步一次;no 让操作系统来决定何时同步。
  everysec 选项比较合适,可以保证系统崩溃时只会丢失一秒左右的数据,并且 Redis 每秒执行一次同步对服务器性能几乎没有任何影响;

面试题1:介绍一下你对Redis哨兵模式的了解吧?

哨兵模式介绍:

  1. - Sentinel(哨兵)进程是用于监控redis集群中Master主服务器工作的状态;
  2. - Master主服务器发生故障的时候,可以实现MasterSlave服务器的切换,保证系统的高可用(HA);

哨兵进程的作用:
1 . 通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。

  1. 2 当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。

3 . 然而一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。

追问1:介绍一下Redis故障自动切换过程:

假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。

追问2:那你说一下主观下线以及客观下线的区别吧:

  1. - `主观下线`Subjectively Down 简称 SDOWN)指的是单个 Sentinel 实例对服务器做出的下线判断。
  2. - `客观下线`Objectively Down 简称 ODOWN)指的是多个 Sentinel 实例在对同一个服务器做出 SDOWN 判断, 并且通过 SENTINEL is-master-down-by-addr 命令互相交流之后, 得出的服务器下线判断。

从主观下线状态切换到客观下线状态并没有使用严格的法定人数算法(strong quorum algorithm), 而是使用了流言协议: 如果 Sentinel 在给定的时间范围内, 从其他 Sentinel 那里接收到了足够数量的主服务器下线报告, 那么 Sentinel 就会将主服务器的状态从主观下线改变为客观下线。 如果之后其他 Sentinel 不再报告主服务器已下线, 那么客观下线状态就会被移除。

客观下线条件只适用于主服务器。 对于任何其他类型的 Redis 实例, Sentinel 在将它们判断为下线前不需要进行协商, 所以从服务器或者其他 Sentinel 永远不会达到客观下线条件。

追问3:说一下哨兵进程的工作方式吧:

1 . 每个Sentinel(哨兵)进程以每秒钟一次的频率向整个集群中的Master主服务器,Slave从服务器以及其他Sentinel(哨兵)进程发送一个 PING 命令。
2 . 如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过down-after-milliseconds选项所指定的值,则这个实例会被 Sentinel(哨兵)进程标记为主观下线(SDOWN)。
3 . 如果一个Master主服务器被标记为主观下线(SDOWN),则正在监视这个Master主服务器的所有Sentinel(哨兵)进程要以每秒一次的频率确认Master主服务器的确进入了主观下线状态。
4 . 当有足够数量的 Sentinel(哨兵)进程(大于等于配置文件指定的值)在指定的时间范围内确认Master主服务器进入了主观下线状态(SDOWN), 则Master主服务器会被标记为客观下线(ODOWN)。
5 . 在一般情况下, 每个Sentinel(哨兵)进程会以每 10 秒一次的频率向集群中的所有Master主服务器、Slave从服务器发送 INFO 命令。
6 . 当Master主服务器被 Sentinel(哨兵)进程标记为客观下线(ODOWN)时,Sentinel(哨兵)进程向下线的 Master主服务器的所有 Slave从服务器发送 INFO 命令的频率会从 10 秒一次改为每秒一次。
7 . 若没有足够数量的 Sentinel(哨兵)进程同意 Master主服务器下线, Master主服务器的客观下线状态就会被移除。若 Master主服务器重新向 Sentinel(哨兵)进程发送 PING 命令返回有效回复,Master主服务器的主观下线状态就会被移除。

追问4:配置哨兵模式:

image.png
一、首先配置Redis的主从服务器,修改redis.conf文件如下:
# 使得Redis服务器可以跨网络访问
bind 0.0.0.0
# 设置密码
requirepass “123456”
# 指定主服务器,注意:有关slaveof的配置只是配置从服务器,主服务器不需要配置
slaveof 192.168.101.90 6379
# 主服务器密码,注意:有关slaveof的配置只是配置从服务器,主服务器不需要配置
masterauth 123456
# 上述内容主要是配置Redis服务器,从服务器比主服务器多一个slaveof的配置和密码。

二、配置3个哨兵,每个哨兵的配置都是一样的。在Redis安装目录下有一个sentinel.conf文件,copy一份进行修改:
# 禁止保护模式
protected-mode no
# 配置监听的主服务器,这里sentinel monitor代表监控,mymaster代表服务器的名称,可以自定义,192.168.101.90代表监控的主服务器,6379代表端口,2代表只有两个或两个以上的哨兵认为主服务器不可用的时候,才会进行failover操作。
sentinel monitor mymaster 192.168.101.90 6379 2
# sentinel author-pass定义服务的密码,mymaster是服务名称,123456是Redis服务器密码
# sentinel auth-pass
sentinel auth-pass mymaster 123456
四、有了上述的修改,我们可以进入Redis的安装目录的src目录,通过下面的命令启动服务器和哨兵:
# 启动Redis服务器进程
./redis-server ../redis.conf
# 启动哨兵进程
./redis-sentinel ../sentinel.conf

 注意启动的顺序。首先是主机(192.168.101.90)的Redis服务进程,然后启动从机的服务进程,最后启动3个哨兵的服务进程。

面试题2:redis 的过期策略和内存淘汰策略是一个东西么?

redis 过期策略:
在Redis中过期的key不会立刻从内存中删除,而是会同时以下面两种策略进行删除:

  1. - `定期删除``:`每隔一段时间,随机检查设置了过期的key并删除已过期的key;维护定时器消耗CPU资源;
  2. 1. 随机取20个设置了过期策略的key
  3. 1. 检查20key中过期时间中已过期的key并删除;
  4. 1. 如果有超过25%的key已过期则重复第一步;

4 . 这种循环随机操作会持续到过期key可能仅占全部key的25%以下时,并且为了保证不会出现循环过多的情况,默认扫描时间不会超过25ms;

  1. - `惰性删除``:`key被访问时检查该key的过期时间,若已过期则删除;已过期未被访问的数据仍保持在内存中,消耗内存资源;

1 . 定期删除可能会导致很多过期 key 到了时间并没有被删除掉,那咋整呢?所以就是惰性删除了。这就是说,在你获取某个 key 的时候,redis 会检查一下 ,这个 key 如果设置了过期时间那么是否过期了?如果过期了此时就会删除,不会给你返回任何东西。
2 . 获取 key 的时候,如果此时 key 已经过期,就删除,不会返回任何东西。
3 . 但是实际上这还是有问题的,如果定期删除漏掉了很多过期 key,然后你也没及时去查,也就没走惰性删除,此时会怎么样?如果大量过期 key 堆积在内存里,导致 redis 内存块耗尽了,咋整?
4 . 那这就要提到了Redis的内存淘汰机制。

内存淘汰机制:
 Redis的内存淘汰策略是指在Redis的用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据,如何淘汰旧数据给新数据腾出内存空间
redis 内存淘汰机制有以下几个:
1 . noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。

2 . allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。(这个是最常用的)

3 . allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。

4 . volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。

5 . volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。

6 . volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。

追问1: 简单介绍一下LRU淘汰机制吧?

image.pngLRU(Least recently used),**淘汰最近最少使用的**;算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
算法思路:
1 . 新数据插入到链表头部;
2 . 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
3 . 当链表满的时候,将链表尾部的数据丢弃。
4 . 在服务器配置中保存了 lru 计数器 server.lrulock,会定时(redis 定时程序 serverCorn())更新,server.lrulock 的值是根据 server.unixtime 计算出来进行排序的,然后选择最近使用时间最久的数据进行删除。另外,从 struct redisObject 中可以发现,每一个 redis 对象都会设置相应的 lru。每一次访问数据,会更新对应redisObject.lru
5 . 在Redis中,LRU算法是一个近似算法,默认情况下,Redis会随机挑选5个键,并从中选择一个最久未使用的key进行淘汰。在配置文件中,按maxmemory-samples选项进行配置,选项配置越大,消耗时间就越长,但结构也就越精准。

追问2:Redis的内存用完了会发生什么实际问题?

如果达到设置的上限,Redis的写命令会返回错误信息(但是读命令还可以正常返回。)或者你可以配置内存淘汰机制,当Redis达到内存上限时会冲刷掉旧的内容

追问3:Redis如何做内存优化?

可以好好利用Hash,list,sorted set,set等集合类型数据,因为通常情况下很多小的Key-Value可以用更紧凑的方式存放到一起。尽可能使用散列表(hashes),散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。

面试题5:聊一下对缓存穿透、缓存击穿、缓存雪崩的理吧?

缓存穿透:
指缓存和数据库中都没有的数据,导致所有的请求都打到数据库上,然后数据库还查不到(如null),造成数据库短时间线程数被打满而导致其他服务阻塞,最终导致线上服务不可用,这种情况一般来自黑客同学。
缓存击穿:
指缓存中没有但数据库中有的数据(一般是热点数据缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去查,引起数据库压力瞬间增大,线上系统卡住。

缓存雪崩:
指缓存同一时间大面积的失效,缓存击穿升级版。

追问1:那你说一下针对缓存击穿的解决方法?

  1. 根据实际业务情况,在Redis中维护一个热点数据表,批量设为永不过期(如top1000),并定时更新top1000数据。
  2. 加互斥锁(mutex key)

    1. 互斥锁
      缓存击穿后,多个线程会同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个互斥锁来锁住它。
      其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。后面的线程进来发现已经有缓存了,就直接走缓存。
    2. static Lock reenLock = new ReentrantLock();

      public List getData04() throws InterruptedException {
      List result = new ArrayList();
      // 从缓存读取数据
      result = getDataFromCache();
      if (result.isEmpty()) {
      if (reenLock.tryLock()) {
      try {
      System.out.println(“拿到锁了,从DB获取数据库后写入缓存”);
      // 从数据库查询数据
      result = getDataFromDB();
      // 将查询到的数据写入缓存
      setDataToCache(result);
      } finally {
      reenLock.unlock();// 释放锁
      }

      } else {
      result = getDataFromCache();// 先查一下缓存
      if (result.isEmpty()) {
      System.out.println(“我没拿到锁,缓存也没数据,先小憩一下”);
      Thread.sleep(100);// 小憩一会儿
      return getData04();// 重试
      }
      }
      }
      return result;
      }

面试题6:对比 Vector、ArrayList、LinkedList 有何区别?适合在什么场景下使用?

image.png
Vector:  
是 Java 早期提供的线程安全的动态数组,如果不需要线程安全,并不建议选择,毕竟同步是有额外开销的。
  Vector 内部是使用对象数组来保存数据,可以根据需要自动的增加容量。当数组已满,开始扩容时,会先创建新的扩容后数组,并拷贝原有数组数据,最后删除原数组。
ArrayList(擅长 “查询” 和 “更新” 场景):  
是应用更加广泛的动态数组实现,它本身不是线程安全的,所以性能要好很多。与 Vector 近似,ArrayList 也是可以根据需要调整容量,不过两者的调整逻辑有所区别,Vector 在扩容时会提高 1 倍,而 ArrayList 则是增加 50%。

数据结构:ArrayList 是动态数组的数据结构实现;
随机查询效率:(优势),ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找,而ArrayList根据角标index直接锁定位置。
插入和删除效率:在List中间插入和删除数据时,ArrayList 要比 LinkedList 效率低很多,因为 ArrayList 增删操作要影响数组内的其他数据的下标(整体移动),而如果是正常的末尾追加方式,效率大体相同。
内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。

LinkedList(擅长 “插入” 和 “删除” 场景):  
顾名思义是 Java 提供的双向链表,所以它不需要像上面两种那样调整容量,它也不是线程安全的。
数据结构:LinkedList 是双向链表的数据结构实现。
随机查询效率:相比ArrayList (劣势)
插入和删除效率:LinkedList按序号查询数据时需要进行前向或后向遍历,但插入数据时只需要记录当前项的前后项即可,增删时也只需修改链表指向即可,所以 LinkedList 插入和删除速度较快。(优势)
内存空间占用:相比ArrayList (劣势)

追问1:多线程场景下就不能使用ArrayList么?

如果遇到多线程场景,可以通过 Collections 的 synchronizedList 方法将其转换成线程安全的容器后再使用。例如像下面这样:
List syncList = Collections.synchronizedList(arraylist);

面试题7:List 和 Set 有哪些区别?

 List、Set 都是继承自Collection 接口,区别主要有以下几点:
重复对象:
  list方法可以允许重复的对象,而set方法不允许重复对象;

null元素:
  list可以插入多个null元素,而set只允许插入一个null元素;

容器是否有序:
  list是一个有序的容器,保持了每个元素的插入顺序。即输出顺序就是输入序,而set方法是无序容器,无法保证每个元素的存储顺序,TreeSet通过 Comparator 或者 Comparable 维护了一个排序顺序

常用的实现类:
  list方法常用的实现类有:

  ArrayList、LinkedList 和 Vector。ArrayList最常用,提供使用索引(index)访问,定位、查询效率高;而LinkedList 则对于经常需要从 List 中添加或删除元素的场合更为合适,Vector 表示底层数组,线程安全,效率低被边缘化~

  Set方法中常用的实现类有:

  HashSet、LinkedHashSet 以及 TreeSet。最常用的是基于 HashMap 实现的 HashSet;另外TreeSet 还实现了 SortedSet 接口(支持排序),因此 TreeSet 是一个可根据 compare() 和compareTo()方法进行排序的有序容器。

遍历方式:
  List 支持for循环,也就是通过下标来遍历,也可以用迭代器(Iterator),但是set只能用迭代,因为他无序,无法用下标来取得想要的值。
[

](https://blog.csdn.net/qq_39390545/article/details/117531057)

追问2:说一下 HashSet 的实现原理?

 HashSet 底层是基于 HashMap 实现的,能够继承 HashMap 的所有特性,因此 HashSet 结构也是数组+链表+红黑树,同样也不能使用get方法,HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成,不允许key重复,但支持null对象作为key。

追问3:HashSet是如何保证Key不重复的?

  HashSet 的值是不能重复的,在业务上经常被用来做数据去重的工作,那么,他是怎么保证元素不重复的呢?

  当我们对一个HashSet 的实例添加一个值时,使用到的是它的 add 方法,源码如下:

public boolean add(E e) {
return map.put(e, PRESENT)==null;
}

  由代码中的 add 方法实现可知,其维护了一个 HashMap 来实现元素的添加;我们知道,HashMap 作为双列集合,它的键是不能够重复的,HashMap 针对 hashCode 相同且 equals 比较值相同的时候执行的是更新操作,所以Hashmap中的key是唯一的,也决定了hashset元素值也是唯一的。
[

](https://blog.csdn.net/qq_39390545/article/details/117531057)

面试题8:你对数据库优化有哪些了解呀?

[

](https://blog.csdn.net/qq_39390545/article/details/117531057)
image.png
性价比如上图,我们针对数据库的优化优先级大致如下:
高:从SQL优化、索引优化入手,优化慢SQL、利用好索引,是重中之重;
中:SQL优化之后,是对数据表结构设计、横纵分表分库,对数据量级的处理;
低:通过修改数据库系统配置,最大化里用服务器内存等资源;
低:通过以上方式还不行,那就是服务器资源瓶颈了,加机器。

优化成本:硬件 > 系统配置 > 数据库表结构 > SQL及索引。
优化效果:硬件 < 系统配置 < 数据库表结构 < SQL及索引。

追问1:那你对SQL优化方面有哪些技巧呢?

  • 最大化利用索引;
  • 尽可能避免全表扫描;
  • 减少无效数据的查询;

首先要清楚SELECT语句 - 执行顺序:
image.png

SQL优化策略:
一、避免不走索引的场景:
1.尽量避免在字段开头模糊查询,会导致数据库引擎放弃索引进行全表扫描。如下:
SELECT FROM t WHERE username LIKE ‘%陈%’
优化方式:尽量在字段后面使用模糊查询。
2.尽量避免使用 or,会导致数据库引擎放弃索引进行全表扫描;
优化方式:可以用union代替or。如下:
SELECT
FROM t WHERE id = 1 UNION SELECT FROM t WHERE id = 3
3.尽量避免进行null值的判断,会导致数据库引擎放弃索引进行全表扫描。
SELECT
FROM t WHERE score IS NULL 1
优化方式:可以给字段添加默认值0,对0值进行判断。如下:
SELECT FROM t WHERE score = 0
4.尽量避免在where条件中等号的左侧进行表达式、函数操作,会导致数据库引擎放弃索引进行全表扫描。
— 全表扫描SELECT
FROM T WHERE score/10=9
— 走索引SELECTFROM T WHERE score =109
5.当数据量大时,避免使用where 1=1的条件。通常为了方便拼装查询条件,我们会默认使用该条件,数据库引擎会放弃索引进行全表扫描
SELECT username, age, sex FROM T WHERE1=1
6.查询条件不要用 <> 或者 !=
7.where条件仅包含复合索引非前置列
8.隐式类型转换造成不使用索引
SQL语句由于索引对列类型为varchar,但给定的值为数值,涉及隐式类型转换,造成不能正确走索引。
select col1 from table where col_varchar=123;
二、避免不走索引的场景:

  1. 1. 避免出现select *:

使用select * 取出全部列,会让优化器无法完成索引覆盖扫描这类优化,会影响优化器对执行计划的选择,也会增加网络带宽消耗,更会带来额外的I/O,内存和CPU消耗。
2 . 避免出现不确定结果的函数:
由于原理上从库复制的是主库执行的语句,使用如now()、rand()、sysdate()、current_user()等不确定结果的函数很容易导致主库与从库相应的数据不一致。另外不确定值的函数,产生的SQL语句无法利用query cache。
3 . 多表关联查询时,小表在前,大表在后:
4 . 使用表的别名:
5 . 用where字句替换HAVING字句:
因为HAVING只会在检索出所有记录之后才对结果集进行过滤,而where则是在聚合前刷选记录,如果能通过where字句限制记录的数目,那就能减少这方面的开销。HAVING中的条件一般用于聚合函数的过滤

一、最左匹配原则的原理:

MySQL 建立多列索引(联合索引)有最左匹配的原则,即最左优先:
如果有一个 2 列的索引 (a, b),则已经对 (a)、(a, b) 上建立了索引;
如果有一个 3 列索引 (a, b, c),则已经对 (a)、(a, b)、(a, b, c) 上建立了索引;

二、违背最左原则导致索引失效的情况:

(下面以联合索引 abc_index:(a,b,c) 来进行讲解,便于理解)
1、查询条件中,缺失优先级最高的索引 “a”:
  当 where b = 6300 and c = ‘JJJ疾风剑豪’ 这种没有以 a 为条件来检索时;B+树就不知道第一步该查哪个节点,从而需要去全表扫描了(即不走索引)。因为建立搜索树的时候 a 就是第一个比较因子,必须要先根据 a 来搜索,进而才能往后继续查询b 和 c,这点我们通过上面的存储结构图可以看明白。

2、查询条件中,缺失优先级居中的索引 “b”:
  当 where a =1 and c =“JJJ疾风剑豪” 这样的数据来检索时;B+ 树可以用 a 来指定第一步搜索方向,但由于下一个字段 b 的缺失,所以只能把 a = 1 的数据主键ID都找到,通过查到的主键ID回表查询相关行,再去匹配 c = ‘JJJ疾风剑豪’ 的数据了,当然,这至少把 a = 1 的数据筛选出来了,总比直接全表扫描好多了。
1、如果建的索引顺序是 (a, b)。而查询的语句是 where b = 1 AND a = ‘陈哈哈’; 为什么还能利用到索引?
  理论上索引对顺序是敏感的,但是由于 MySQL 的查询优化器会自动调整 where 子句的条件顺序以使用适合的索引,所以 MySQL 不存在 where 子句的顺序问题而造成索引失效。当然了,SQL书写的好习惯要保持,这也能让其他同事更好地理解你的SQL。

2、还有一个特殊情况说明下,下面这种类型的SQL, a 与 b 会走索引,c不会走。

select * from LOL where a = 2 and b > 1000 and c=’JJJ疾风剑豪’;

  对于上面这种类型的sql语句;mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配(包括like ‘陈%’这种)。在a、b走完索引后,c已经是无序了,所以c就没法走索引,优化器会认为还不如全表扫描c字段来的快。所以只使用了(a,b)两个索引,影响了执行效率。

三、需要你mark的知识点?

1、如何通过有序索引排序,避免冗余执行order by ?

ps:当order by 与where 语句同时出现,order by的排序功能无效
数据库的处理顺序是:
第一步:根据where条件和统计信息生成执行计划,得到数据。

第二步:将得到的数据排序。当执行处理数据(order by)时,数据库会先查看第一步的执行计划,看order by 的字段是否在执行计划中利用了索引。如果是,则可以利用索引顺序而直接取得已经排好序的数据。如果不是,则排序操作。

第三步:返回排序后的数据。

2、like 语句的索引问题 ?

如果通配符 % 不出现在开头,则可以用到索引,但根据具体情况不同可能只会用其中一个前缀,在 like “value%” 可以使用索引,但是 like “%value%” 违背了最左匹配原则,不会使用索引,走的是全表扫描。

3、不要在列上进行运算?

如果查询条件中含有函数或表达式,将导致索引失效而进行全表扫描

4、索引不会包含有 NULL 值的列?

只要列中包含有 NULL 值都将不会被包含在索引中,复合索引中只要有一列含有 NULL 值,那么这一列对于此复合索引就是无效的。所以在数据库设计时不要让字段的默认值为 NULL

5、尽量选择区分度高的列作为索引?

区分度的公式是count(distinct col)/count(*),表示字段不重复的比例,比例越大我们扫描的记录数越少,唯一键的区分度是 1,而一些状态、性别字段可能在大数据面前区分度就是 0。一般需要 join 的字段都要求区分度 0.1 以上,即平均 1 条扫描 10 条记录

6、覆盖索引的好处?

如果一个索引包含所有需要的查询的字段的值,我们称之为覆盖索引。覆盖索引是非常有用的工具,能够极大的提高性能。因为,只需要读取索引,而无需读表,极大减少数据访问量,这也是不建议使用Select * 的原因。

面试题9:MySQL删除数据的方式都有哪些?

通过 delete、truncate、drop 关键字进行删除;这三种都可以用来删除数据,但用于的场景不同。
一、从执行速度上来说:
drop > truncate >> DELETE
二、从原理上讲:

  1. - **DELETE:**

1 . DELETE属于数据库DML操作语言,只删除数据不删除表的结构,会走事务,执行时会触发trigger;

2 . 在 InnoDB 中,DELETE其实并不会真的把数据删除,mysql 实际上只是给删除的数据打了个标记为已删除,因此 delete 删除表中的数据时,表文件在磁盘上所占空间不会变小,存储空间不会被释放,只是把删除的数据行设置为不可见。虽然未释放磁盘空间,但是下次插入数据的时候,仍然可以重用这部分空间(重用 → 覆盖)。

3 . DELETE执行时,会先将所删除数据缓存到rollback segement中,事务commit之后生效;

4 . delete from table_name删除表的全部数据,对于MyISAM 会立刻释放磁盘空间,InnoDB 不会释放磁盘空间;

5 . 对于delete from table_name where xxx 带条件的删除, 不管是InnoDB还是MyISAM都不会释放磁盘空间;

6 . delete操作以后使用 optimize table table_name 会立刻释放磁盘空间。不管是InnoDB还是MyISAM 。所以要想达到释放磁盘空间的目的,delete以后执行optimize table 操作。

7 . delete 操作是一行一行执行删除的,并且同时将该行的的删除操作日志记录在redo和undo表空间中以便进行回滚(rollback)和重做操作,生成的大量日志也会占用磁盘空间。

  1. - **truncate:**

1 . truncate:属于数据库DDL定义语言,不走事务,原数据不放到 rollback segment 中,操作不触发 trigger。
2 . truncate table table_name 立刻释放磁盘空间 ,不管是 InnoDB和MyISAM 。
3 . truncate能够快速清空一个表。并且重置auto_increment的值

  1. - **drop:**

drop table table_name 立刻释放磁盘空间 ,不管是 InnoDB 和 MyISAM; drop 语句将删除表的结构被依赖的约束(constrain)、触发器(trigger)、索引(index); 依赖于该表的存储过程/函数将保留,但是变为 invalid 状态

面试题10:说一下抽象类和接口有哪些区别?

抽象类和接口的主要区别:

从设计层面来说,抽象类是对类的抽象,是一种模板设计;接口是行为的抽象,是一种行为的规范。

1 . 一个类可以有多个接口 只能有继承一个父类
2 . 抽象类可以有构造方法,接口中不能有构造方法。
3 . 抽象类中可以有普通成员变量,接口中没有普通成员变量
4 . 接口里边全部方法都必须是abstract的;抽象类的可以有实现了的方法
5 . 抽象类中的抽象方法的访问类型可以是public,protected;但接口中的抽象方法只能是public类型的,并且默认即为public abstract类型
6 . 抽象类中可以包含静态方法,接口中不能包含静态方法
7 . 抽象类和接口中都可以包含静态成员变量,抽象类中的静态成员变量的访问类型可以任意;但接口中定义的变量只能是public static final类型,并且默认即为public static final类型。

追问1:用抽象类实现一个接口,和普通类实现接口会有什么不同么?

一般来说我们使用普通类来实现接口,这个普通类就必须实现接口中所有的方法,这样的结果就是普通类中就需要实现多余的方法,造成代码冗余。但是如果我们使用的是抽象类来实现接口,那么就可以只实现接口中的部分方法,并且当其他类继承这个抽象类时,仍然可以实现接口中有但抽象类并未实现的方法。

追问2:能分别说一下final、finally、finalize的区别么?

1 . final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值。

2 . finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块中,表示不管是否出现异常,该代码块都会执 行,一般用来存放一些关闭资源的代码。当然,还有多种情况走不了finally~

3 . finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调用,当我们调用System.gc() 方法的时候,由垃圾回收器调用finalize(),回收垃圾,一个对象是否可回收的最后判断。

面试题3:你对Java序列化了解么?

序列化过程:
  是指把一个Java对象变成二进制内容,实质上就是一个byte[]数组。因为序列化后可以把byte[]保存到文件中,或者把byte[]通过网络传输到远程(IO),这样,就相当于把Java对象存储到文件或者通过网络传输出去了。
反序列化过程:
   把一个二进制内容(也就是byte[]数组)变回Java对象。有了反序列化,保存到文件中的byte[]数组又可以“变回”Java对象,或者从网络上读取byte[]并把它“变回”Java对象。
必须实现一个特殊的java.io.Serializable``接口

追问1:Java序列化是如何工作的?

1 . 当且仅当对象的类实现java.io.Serializable接口时,该对象才有资格进行序列化。可序列化 是一个标记接口(不包含任何方法),该接口告诉Java虚拟机(JVM)该类的对象已准备好写入持久性存储或通过网络进行读取。

2 . 默认情况下,JVM负责编写和读取可序列化对象的过程。序列化/反序列化功能通过对象流类的以下两种方法公开:

3 . ObjectOutputStream。writeObject(Object):将可序列化的对象写入输出流。如果要序列化的某些对象未实现Serializable接口,则此方法将引发NotSerializableException。

4 . ObjectInputStream。readObject():从输入流读取,构造并返回一个对象。如果找不到序列化对象的类,则此方法将引发ClassNotFoundException。

5 . 如果序列化使用的类有问题,则这两种方法都将引发InvalidClassException,如果发生I / O错误,则将引发IOException。无论NotSerializableException和InvalidClassException是子类IOException异常。

追问2:什么是serialVersionUID常数?

serialVersionUID是一个常数,用于唯一标识可序列化类的版本。从输入流构造对象时,JVM在反序列化过程中检查此常数。如果正在读取的对象serialVersionUID与类中指定的序列号不同,则JVM抛出InvalidClassException。这是为了确保正在构造的对象与具有相同serialVersionUID的类兼容。

  1. 请注意,serialVersionUID是可选的。这意味着如果您不显式声明Java编译器,它将生成一个。 <br />那么,为什么要显式声明serialVersionUID呢?<br /> 原因是:<br />自动生成的serialVersionUID是基于类的元素(成员变量,方法,构造函数等)计算的。如果这些元素之一发生更改,serialVersionUID也将更改。

面试题4:Java中创建对象的几种方式?

1.new一个对象
2.利用反射创建对象newinstence
3.clone
4.反序列化
补充:数组里的括号代表的是内存地址的偏移量,
数组的引用指向的是数组首地址(int【】 arr = {1,2,3,};
System.out.print(arr[1]))