因为篇幅限制,本篇接着Java 面试题初级篇 (一)总结

线程安全

不是线程安全,应该是内存安全,堆是共享内存,可以背所有线程访问

当多个线程访问一个对象时,如果不用进行额外的同步控制或其他的协调操作,调用这个对象的行为都可以获得正确的结果,我们就说这个对象是线程安全的

堆是进程和线程共有的空间,分全局堆和局部堆。全局堆就是没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是用完了要还给操作系统,要不然就是内存泄漏。

在Java中,堆是Java虚拟机所管理的内存中最大的一块,是所有线程共享的一块内存区域,在虚拟机启动时创建。堆所存在的内存区域的唯一目的就是存放实例对象,几乎所有的对象实例以及数组都在这里分配内存

栈是每个线程独有的,保存其运行状态和局部自动变量。栈在线程开始的时候初始化,每个线程的栈互相独立,因此,栈是线程安全的。操作系统在切换线程的时候会自动切换栈,占空间不需要在高级语言里面显式的分配和释放

目前主流操作系统都是多任务的,即多个进程同时运行,为了保证安全,每个进程只能访问分配给自己的内存空间,而不能访问别的进程的,这时由操作系统保障的。在每个进程的内存空间中都会有一块特殊的公共区域,通常称为堆(内存)。进程内的所有线程都可以访问到该区域,这就是造成问题的潜在原因

守护线程

守护线程为所有非守护线程提供服务的线程,任何一个守护线程都是整个JVM中所有非守护线程的保姆。守护线程类似于整个进程的一个默默无闻的小喽喽,它的生死无关紧要,它却依赖整个进程而运行,那天其他线程结束了没有要执行的了,程序就结束了,理都没理守护线程,就把它中断了。注意: 由于守护线程的终止是自身无法控制的,因此千万不要把IO File等重要操作逻辑分配给它,因为它不靠谱

守护线程的作用

GC垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。应用场景: 1. 来为其他线程提供服务支持的情况。 2. 或者在任何情况下,程序结束时,这个线程必须正常且立刻关闭,就可以作为守护线程来使用,反之,如果一个正在执行某个操作的线程必须要正确的关闭掉否则就会出现不好的后果,那么这个线程就不能是守护线程,而是用户线程。通常都是些关键的事务,比方说,数据库录入或者更新,这些操作都是不能中断的。Thread.setDaemon(true)必须在Thread.start()之前设置,否则会抛出IllegalThreadStateException异常。你就不能把正在运行的常规线程设置为守护线程

ThreadLocal

每一个Thread对象均含有一个ThreadLocalMap类型的成员变量ThreadLocal,它存储本线程中所有ThreadLocal对象及其对应的值。ThreadLocalMap由一个个Entry对象构成。Entry继承自WeakReference>,一个Entry由ThreadLocal对象和Object构成。由此可见,Entry的key是ThreadLocal对象,并且是一个弱引用。当没指向key的强引用后,该key就会被垃圾收集器回收。当执行set方法时,ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,将值存储进ThreadLocalMap对象中。get方法执行过程类似,ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,获取对应的value。由于每一条线程均含有各自私有的ThreadLocalMap容器,这些容器相互独立互不影响,因此不会存在线程安全性问题,从而也无需使用同步机制来保证多条线程访问容器的互斥性

使用场景

在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束
线程数据隔离
进行事务操作,用于存储线程事务信息
数据库链接,Session会话管理

Spring框架在事务开始时会给当前线程绑定一个JDBC Connection,在整个事务过程都是使用该线程绑定的Connection来执行数据库操作,实现了事务的隔离性。Spring框架里面就是用的ThreadLocal来实现这种隔离

内存泄漏

内存泄露为程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏危害可以忽略,但内存泄漏堆积后果很严重,无论多少内存,迟早会被占光,不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄漏。强引用: 使用最普遍的引用(new),一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。如果想取消强引用和某个对象之间的关联,可以显式的将引用赋值为NULL,这样可以使JVM在合适的时间就会回收该对象。弱引用: JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在Java中,用Java.lang.ref.WeakReference类来表示。可以在缓存中使用弱引用。

ThreadLocal引用关系图

ThreadLocal的实现原理,每一个Thread维护一个ThreadLocalMap,key为使用弱引用的ThreadLocal实例,value为线程变量的副本。ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部强引用时,key势必会被GC回收,这样就导致ThreadLocalMap中key为NULL,而value还存在着强引用,只有Thread线程退出之后,value的强引用链条才会断掉,但如果当前线程迟迟不结束的话,这些key为NULL的Entry的value就会一直存在一条强引用链。

  • key使用强引用,当ThreadLocalMap的Key为强引用回收ThreadLocal时,因为ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏
  • key使用弱引用,当ThreadLocalMap的key为弱引用回收ThreadLocal时,由于ThreadLocalMap持有ThreadLOcal的弱引用,即使没有手动删除,ThreadLocal也会被回收。当key为NULL,在下一次ThreadLocalMap调用set get remove方法的时候会被清除value值

因此,ThreadLocal内存泄漏的根源是由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。ThreadLocal正确的使用方法是 1. 每次使用完ThreadLocal都调用它的remove()方法清除数据。2. 将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉

并发 并行 串行

  • 串行在时间上不可能发生重叠,前一个任务没搞定,下一个任务就只能等着
  • 并行在时间上是重叠的,两个任务在同一个时刻互不干扰的同时执行
  • 并发允许两个任务彼此干扰,同一时间点,只有一个任务运行,交替执行

在这里举一个不太恰当的例子😂,假设又一个厕所,厕所里面只有一个坑位。如果大家素质都比较高,排队上厕所,前一个上完后一个进去上,那么此时这个过程就是串行;如果大家素质比较差,都不排队开始抢厕所,谁抢到谁进去,那么这个过程就是并发的过程。突然有一天,物业接到投诉,于是又增加了一个坑位没,那么此时,不管大家是排队还是不排队,同时有两个人能上厕所(多任务同时执行),这就叫并行。串行很好理解,一个接一个有序排队执行,并发和并行的区别在于,同一时刻能否有多个任务同时执行

并发三大特性

  • 原子性

原子性是指在一个操作中CPU不可以中途暂停然后再调度,即不被中断操作,要不全部执行完成,要不都不执行。就好次转账,从账户A向账户B转1000元,那么必然包括2个操作: 从账户A减去1000元,往账户B加上1000元。2个操作必须全部完成

  • 可见性

当多个线程访问同一个变量时,一个线程改变了这个变量的值,其他线程能够立即看得到修改的值

  • 有序性

虚拟机在进行代码编译时,对于那些改变顺序之后不会对最终结果造成影响的代码,虚拟机不一定会按照我们写的代码的顺序来执行,又肯呢个将他们重排序。实际上,对于有些代码进行重拍之后,虽然堆变量的值没有造成影响,但有可能会出现线程安全的问题

为什么使用线程池

  • 降低资源消耗,提高线程利用率,降低创建和销毁线程的消耗
  • 提高响应速度,任务来了,直接有线程可用可执行,而不是先创建线程再执行
  • 提高线程可管理性,线程是稀缺资源,使用线程池可以统一分配资源

SpringBoot

自动配置原理

@SpringBootApplication注解包括@SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan

  • @SpringBootConfiguration

其实@SpringBootConfiguration里面其实就是一个配置类,因为里面有@Configuration

  • @EnableAutoConfiguration里面又包括@AutoConfigurationPackage + @Import(AutoConfigurationImportSelector.class)

@AutoConfigurationPackage里面有@Impor(AutoConfigurationPackages.Registrar.class)

利用Registrar给容器倒入一系列组件
将Main所在包下的组件导入进来

@Import(AutoConfigurationImportSelector.class)

利用getAutoConfigurationEntry给容器批量导入一些组件
调用getCandidateConfigurations获取所有需要导入到容器中的配置类(组件)
利用工厂加载loadSpringFactories得到所有的组件
从META-INF/spring.factories位置来加载一个文件。默认扫描我们当前系统所有META-INF/spring.factories位置的文件
spring-boot-autoconfigure包里面也有META-INF/spring.factories
虽然我们127个场景的所有自动配置启动的时候默认全部加载。但是按照条件装配规则(@Conditional),最终会按需配置
SpringBoot默认在底层配好所有的组件,但是如果用户自己配了以用户的优先

  • @ComponentScan

指定包扫描路径

starter

使用Spring + SpringMVC,如果需要引入Mybatis等框架,需要到xml中定义Mybatis需要的bean。starter就是定义一个starter的jar包,写一个@Configuration配置类,将这些bean定义在里面,然后再starter包的META-INF/spring.factories中写入该配置类,SpringBoot会按照约定来加载该配置类。开发人员只需要将相应的starter包依赖进应用,进行相应属性配置(使用默认配置时,不需要配置),就可以直接进行代码开发使用对应的功能了,比如mybatis-spring-boot-starter,spring-boot-starter-redis

嵌入式服务器

节省了下载安装tomcat,应用也不需要再打war包,然后再放到webapp目录下运行,只需要安装一个Java虚拟机,就可以直接在上面部署应用程序了,SpringBoot已经内置了tomcat.jar,运行main方法时会自动启动tomcat,并利用tomcat的spi机制加载springmvc

Mybatis

  • 优点
    1. 基于SQL语句编程,相当灵活。不会对应用程序或者数据库的现有设计造成任何影响,SQL写在XML里,解除SQL于程序的耦合,便于统一管理,提供XML标签,支持编写动态SQL语句,并可重用
    2. 与JDBC相比,减少了50%以上的代码量,消除了JDBC大量的冗余代码,不需要手动开关链接
    3. 很好的与各种数据库兼容(因为Mybatis使用JDBC来连接数据库,所以只要JDBC支持的数据库Mybatis都支持)
    4. 能够与Spring很好的集成
    5. 提供映射标签,支持对象与数据库的ORM字段关系映射,提供对象关系映射标签。支持对象关系组件维护
  • 缺点
    1. SQL语句的编写工作量较大,尤其当字段多,关联表多时,对开发人员编写SQL语句的功底有一定要求
    2. SQL语句依赖于数据库,导致数据库移植性较差,不能随便更换数据库

#{}和${}

  • ${}是字符串替换,是拼接符,#{}是预编译处理,是占位符
  • MyBatis在处理#{}时,会将SQL中#{}替换为?,调用PreparedStatement来赋值
  • MyBatis在处理${}时,就是把${}替换成变量的值,调用Statement来赋值
  • ${}的变量替换是在DBMS外,变量替换后,${}对应的变量不会加上单引号,#{}的变量替换是在DBMS中,变量替换后,#{}对应的变量自动加上单引号
  • 使用#{}可以有效的防止SQL注入,提高系统的安全性

Mysql

索引的基本原理

索引用来快速的寻找那些具有特定值的记录。如果没有索引,一般来说执行查询时遍历整张表。索引的原理: 就是把无序的数据变成有序的查询

  • 把创建了索引的列内容进行排序
  • 对排序结果生成倒排表
  • 在倒排表内容上拼上数据地址链
  • 在查询的时候,先拿到倒排表内容,再取数据地址链,从而拿到具体数据

聚簇和非聚簇索引

都是B+树的数据结构。聚簇索引: 将数据存储与索引放在了一起,并且时按照一定的顺序组织的,找到索引也就找到了数据,数据的物理存放顺序与索引顺序是一致的,即只要索引是相邻的,那么对应的数据一定也是相邻的存放在磁盘上的。非簇簇索引: 叶子节点不存放数据,存储的是数据行地址,也就是说根据索引查找到数据行的位置再取磁盘查找数据,这个就有点类似一本树的目录,比如我们要找第三章第一节,那我们先在这个目录里面找,找到对应的页码后再去对应的页码看文章

优势:

  • 查询通过聚簇索引可以直接获取数据,相比非聚簇索引需要第二次查询(非覆盖索引的情况下)效率要高
  • 聚簇索引对于范围查询的效率很高,因为其数据是按照大小排列的
  • 聚簇索引适合用在排序的场合,非聚簇索引不适合

劣势:

  • 维护索引很昂贵,特别是插入新行或者主键被更新导致要分页的时候,建议在大量插入新行后,选在负载较低的时间段,通过OPTIMIZE TABLE优化表,因为必须被移动的行数据可能造成碎片。使用独享表空间可以弱化碎片
  • 表因为使用UUID作为主键,使数据存储稀疏,这就会出现聚簇索引有可能有比全表扫描更慢,所以建议使用int的auto_increment作为主键
  • 如果主键比较大的话,那辅助索引将会变的更大,因为辅助索引的叶子存储的是主键值,过长的主键值,会导致非叶子节点占用更多的物理空间

InnoDB中一定有主键,主键一定是聚簇索引,不手动设置,则会使用unique索引,没有unique索引则会使用数据库内部的一个行的隐藏ID来当作主键索引。在聚簇索引之上创建的索引称之为辅助索引,辅助索引访问数据总是需要二次查找,非簇聚索引都是辅助索引,像复合索引,前缀索引,唯一索引,辅助索引叶子节点存储的不再是行的物理位置,而是主键值

MyISM使用的是非聚簇索引,没有簇聚索引,非聚簇索引的两棵B+树看上去没有什么区别,节点的结构完全一致只是存储的内容不同而已,主键索引B+树的节点存储了主键,辅助键索引B+树存储了辅助键。表数据存储在独立地方,这两棵B+树的叶子结点都使用一个地址指向真正的表数据,对于表数据来说,这两个键没有任何差别。由于索引树是独立的,通过辅助键检索无需访问主键的索引树

如果涉及到大数据量的排序,全表扫描,count之类的操作的话,还是MyISM占优势,因为索引所占空间小,这些操作是需要在内存中完成的

MySQL索引的数据结构

索引的数据结构和具体存储引擎的实现有关,在MySQL中使用较多的索引有Hash索引,B+树索引等,InnoDB存储引擎的默认索引实现为B+树索引。对于Hash索引来说,底层的数据结构就是哈希表,因为在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快,其余大部分场景,建议选择B+树索引

B+树:

B+树是一个平衡的多叉树,从根节点到每个叶子节点的高度差值不超过1,而且同层级的节点之间有指针相互链接。在B+树上的常规检索,从根节点到叶子节点的搜索效率基本相当,不会出现大幅波动,而且基于索引的顺序扫描时,也可以利用双向指针快速左右移动,效率非常高。因此,B+树索引被广泛应用于数据库,文件系统等场景

B+树

Hash索引:

哈希索引就是采用一定的哈希算法,把键值换算成新的哈希值,检索时不需要类似B+树那样从根节点到叶子结点逐级查找,只需一次哈希算法即可立即定位到相应的位置,速度非常快。如果是等值查询,那么哈希索引明显有绝对优势因为只需要经过一次算法即可找到相应的键值,前提时键值都是唯一的。如果键值不是唯一的,就需要先找到该键所在位置,然后再根据链表往后扫描,直到找到相应的数据;如果时范围查询检索,这时候哈希索引就毫无用武之地了。因为原先是有序的键值,经过哈希算法后,又肯呢个变成不连续的了,就没办法再利用索引完成范围查询检索

哈希索引

哈希索引也没办法利用索引完成排序,以及like ‘xxx%’这样的部分模糊查询(这种部分模糊查询,其实本质上也是范围查询)。哈希索引也不支持多列联合索引的最左匹配原则。B+树索引的关键字检索效率比较平均,不像B树那样波动幅度大,在有大量重复键值情况下,哈希索引的效率也是较低,因为存在哈希碰撞问题

索引的设计原则

查询更快,占用空间更小

  • 适合索引的列是出现在where子句中的列或者连接子句中指定的列
  • 基数较小的表,索引效果较差,没有必要在此列建立索引
  • 使用短索引,如果对长字符串列进行索引,应该指定一个前缀长度,这样能够节省大量索引空间,如果搜索词超过索引前缀长度,则使用索引排除不匹配的行,然后检查其余行是否可能匹配
  • 不要过度索引。索引需要额外的磁盘空间,并降低写操作的性能,在修改表内容的时候,索引会进行更新甚至重构,索引列越多,这个时间就会越长。所以只保持需要的索引有利于查询即可
  • 定义有外键的数据列一定要建立索引
  • 更新频繁字段不适合创建索引
  • 若是不能有效区分数据的列不适合做索引列(如性别,男女未知,最多也就三种,区分度实在太低)
  • 尽量在扩展索引,不要心间索引。比如表中已经有A的索引,现在要加(A, B)的索引,那么只需要修改原来的索引即可
  • 对于那些查询中很少涉及的列,重复值比较多的列不要建立索引
  • 对于定义为text,image和bit的数据类型的列不要建立索引

MySQL锁的类型

基于锁的属性分类: 共享锁,排它锁
基于锁的粒度分类: 行级锁(InnoDB),表级锁(InnoDB,MyISAM),页级锁(BDB引擎),记录锁,间隙锁,临键锁
基于锁的状态分类: 意向共享锁,意向排他锁

  • 共享锁(Share Lock)

共享锁又称读锁,简称S锁。当一个事务为数据加上读锁之后,其他事务只能对该数据加读锁,而不能对数据加写锁,直到所有的读锁释放之后其他事务才能对其进行加持写锁。共享锁的特性主要是为了支持并发的读取数据,读取数据的时候不支持修改,避免出现重复读的问题

  • 排它锁(Exclusive Lock)

排它锁又称写锁,简称X锁。当一个事务为数据加上写锁,其他请求将不能再为数据加任何锁,直到该锁释放之后,其他事务才能对数据进行加锁。排它锁的目的是在数据修改时候,不允许其他人同时修改,也不允许其他人读取,避免了出现脏数据和脏读的问题

  • 表锁

表锁是指锁的时候锁住的是整个表,当下一个事务访问该表的时候,必须等前一个事务释放了该锁才能进行对表进行访问,
特点: 粒度大,加锁简单,容易冲突

  • 行锁

行锁是指锁的时候锁住的是表的某一行或多行记录,其他事务访问同一张表时,只有被锁住的记录不能访问,其他的记录可以正常访问
特点: 粒度小,加锁比表锁麻烦,不容易冲突,相比表锁支持的并发要高

  • 记录锁(Record Lock)

记录锁也是属于行锁的一种,只不过记录锁的范围只是表中的某一条记录,记录锁是说事务在加锁后锁住的只是表的某一条记录。精准条件命中,并且命中的条件字段是唯一索引,加了记录锁之后数据可以避免在查询的时候被修改的重复读问题,也避免了在修改事务未提交之前被其他事务读取的脏读问题

  • 页锁

页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折中的页级,一次锁定相邻的一组记录。
特点: 开销和加锁时间介于表锁和行锁之间,会出现死锁,锁粒度介于表锁和行锁之间,并发度一般

  • 间隙锁(Gap Lock)

属于行锁中的一种,间隙锁实在事务加锁后其所在的是表记录的某一个区间,当表的相邻ID之间出现空袭则会形成一个区间,遵循左开右闭原则。范围查询并且查询未命中记录,查询条件必须命中索引,间隙锁只会出现在REPEATABLE_READ(重复读)的事务级别中。
触发条件: 防止幻读问题,事务并发的时候,如果没有间隙锁,就会发生问题,在同一个事务里,A事务的两次查询的结果会不一样
比如表里面的数据ID为1,4,5,7,10,那么会形成一下几个间隙区间,-n~1区间,1~4区间,7~10区间,10~n区间(-n代表无穷大,n代表正无穷大)

  • 临键锁(Next-Key Lock)

也属于行锁的一种,并且它是InnoDB的行锁默认算法,总结来说它就是记录锁和间隙锁的组合,临键锁会把查询出来的记录锁住,同时也会把该范围查询内的所有间隙空间也会锁住,再之它会把相邻的下一个区间也会锁住
触发条件: 范围查询并命中,查询命中了索引
结合记录锁和间隙锁的特性,临键锁避免了在范围查询时出现脏读,重复读,幻读问题。加了临键锁之后,在范围区间内数据不允许被修改和插入

如果当事务A加锁成功之后就设置一个状态告诉后面的人,已经有人对表里的行加了一个排它锁了,你们不能对整个表加共享锁或排它锁了,那么后面需要对整个表加锁的人只需要获取这个状态就知道自己是不是可以对表加锁,避免了对整个索引树的每个节点扫描是否加锁,而这个状态就是意向锁

-意向共享锁

当一个事务试图对整个表进行加共享锁之前,首先需要获得这个表的意向共享锁

  • 意向排它锁

当一个事务试图对整个表进行加排它锁之前,首先需要获得这个表的意向排它锁

MySQL执行计划

执行计划就是SQL的执行查询顺序,以及如何使用索引查询,返回的结果集的行数

id select_type table partitions type possible_keys key key_len ref rows filtered Extra

  • id: 是一个有顺序的编号,是查询的顺序号,有几个SELECT就显示几行。id的顺序是按SELECT出现的顺序增长的。id列的值越大执行优先级越高越先执行,id列的值相同则从上往下执行,id列的值为NULL最后执行
  • select_type表示查询中每个SELECT子句的类型
    1. SIMPLE: 表示此查询不包含UNION查询或子查询
    2. PRIMARY: 表示此查询是最外层的查询(包含子查询)
    3. SUBQUERY: 子查询中的第一个SELECT
    4. UNION: 表示此查询是UNION的第二或随后的查询
    5. DEPENDENT UNION: UNION的第二个或后面的查询语句,取决于外卖呢的查询
    6. UNION RESULT: UNION的结果
    7. DEPENDENT SUBQUERY: 子查询中的第一个SELECT,取决于外面的查询,即子查询依赖于外层查询的结果
    8. DERIVED: 衍生,表示导出表的SELECT(FROM子句的子查询)
  • table: 表示该语句查询的表
  • type: 优化SQL的重要字段,也是我们判断SQL性能和优化程度重要指标。它的取值类型范围:
    1. const: 通过索引一次命中,匹配一行数据
    2. system: 表中只有一行记录,相当于系统表
    3. eq_ref: 唯一性索引扫描,对于每个索引键表中只有一条记录与之匹配
    4. ref: 非唯一性索引扫描,返回匹配某个值的所有
    5. range: 只检索给定范围的行,使用一个索引来选择行,一般用于between,<,>
    6. index: 只遍历索引树
    7. ALL: 表示全表扫描,这个类型的查询是性能最差的查询之一。那么基本就是随着表的数量增多,执行效率越慢
    8. 执行效率: ALL < index < range < eq_ref < const < system最好是避免ALL和index
  • possible_key: 它表示MySQL在执行该SQL语句的时候,可能用到的索引信息,仅仅是可能,实际不一定会用到
  • key: 此字段是MySQL在当前查询时所真正使用到的索引,它是possible_key的子集
  • key_len: 表示查询优化器使用了索引的字节数,这个字段可以评估组合索引是否完全被使用,这也是我们优化SQL时,评估索引的重要指标
  • rows: MySQL查询优化器根据统计信息,估算该SQL返回结果集需要扫描读取的行数,这个值相当重要,索引优化之后,扫描读取的行数越多,说明索引设置不对或者字段传入的类型之类的问题,说明要优化空间越大
  • filtered: 返回结果的行占需要读到的行(rows列的值)的百分比,就是百分比越高,说明需要查到数据越准确,百分比越小,说明查到的数据量大,而结果集很少
  • extra
    1. using filesort: 表示MySQL对结果集进行外部排序,不能通过索引顺序达到排序效果。一般有using filesort都建议优化去掉,因为这样的查询CPU资源消耗大,延时大
    2. using index: 覆盖索引扫描表示查询在索引树中就可查找所需数据,不用扫描表数据文件,往往说明性能不错
    3. using temporary: 查询有使用临时表,一般出现于排序,分组和多表join的情况,查询效率不高,建议优化
    4. using where: SQL使用了where过滤,效率较高

事务的基本特性和隔离级别

事务基本特性ACID分别是:

  • 原子性指的是一个事务的操作要么全部成功,要么全部失败
  • 一致性指的是数据库总是从一个一致性状态转换到另一个一致性状态。比如A转账给B100块钱,假设A只有90块,支付之前我们数据库里的数据都是符合约束的,但是如果事务执行成功了,我们的数据库数据就破坏了,因此事务不能成功,这里我们说事务提供了一致性的保证
  • 隔离性指的是一个事务的修改在最终提交前对其他事务是不可见的
  • 持久性指的是一旦事务提交,所做的修改就会永久保存到数据库中

隔离性有4个隔离级别,分别是:

  • read uncommit 读未提交,可能会读到其他事务未提交的数据,也叫做脏读。用户本来应该读取到id=1的用户age应该是10,结果读取到了其他事务还没有提交的事务,读取结果age=20,这就是脏读
  • read commit 读已提交,两次读取结果不一致,叫做不可重复读。不可重复读解决了脏读的问题,它只会读取已经提交的事务。用户开启事务读取id=1用户,查询age=10,再次读取发现age=20,在同一个事务里同一个查询读取到不同的结果叫做不可重复读
  • repeatable read 可重复读,这是MySQL的默认级别,就是每次读取结果都一样,但是有可能产生幻读
  • serializable 串行,一般是不会使用的,他会给每一行读取的数据加锁,会导致大量超时和锁竞争的问题

慢查询处理

在业务系统中,除了使用主键进行的查询,其他的都会在测试库上测试其耗时,慢查询的统计主要由运维在做,会定期将业务中的慢查询反馈给我们。慢查询的优化首先要搞明白慢的原因是什么?是查询条件没有命中索引?是load了不需要的数据列?还是数据量太大?所以优化也是针对这三个方向来的:

  • 首先分析语句,看看是否load了额外的数据,可能是查询了多余的行并且抛弃掉了,可能是加载了许多结果中并不需要的列,对语句进行分析以及重写
  • 分析语句的执行计划,然后获得其使用索引的情况,之后修改语句或者修改索引,使得语句可以尽可能的命中
  • 如果对语句的优化已经无法进行,可以考虑表中的数据量是否太大,如果是的话可以进行横向或纵向的分表

ACID靠什么保证

  • A原子性由undo log日志保证,它记录了需要回滚的日志信息,事务回滚时撤销已经执行成功的SQL
  • C一致性由其他三大特征保证,程序代码要保证业务的一致性
  • I隔离性由MVCC来保证
  • D持久性由内存 + redo log来保证,MySQL修改数据同时在内存和redo log记录这次操作,宕机的时候可以从redo log恢复

MVCC

多版本并发控制: 读取数据时通过一种类似快照的方式将数据保存下来,这样读锁和写锁不冲突了,不同的事务session会看到自己特定版本的数据,版本链。MVCC只在READ COMMITTED 和 REPEATABLE READ两个隔离级别下工作。其他两个隔离级别和MVCC不兼容。因为READ UNCOMMITTED总是读取最新的数据行而不是符合当前事务版本的数据行。而SERIALIZABLE则会对所有读取的行都加锁。
聚簇索引记录中有两个必要的隐藏列:

  • trx_id: 用来存储每次对某条聚簇索引记录进行修改的时候的事务ID
  • roll_pointer: 每次对哪条聚簇索引有修改的时候,都会把老版本写入undo日志中。这个roll_pointer就是存了一个指针,它指向这条聚簇索引记录的上一个版本的位置,通过它来获得上一个版本的记录信息。(注意插入操作的undo日志没有这个属性,因为它没有老版本)
  • 已提交读和可重复读的区别就在于它们生成ReadView的策略不同

开始事务时创建ReadView,ReadView维护当前活动的事务id,即未提交的事务id,排序生成一个数组访问数据,获取数据中的事务id(获取的是事务id最大的记录),对比ReadView: 如果在ReadView的左边(比ReadView都小),可以访问(在左边意味着该事务已经提交)。如果在ReadView的右边(比ReadView都大)或者就在ReadView中,不可以访问,获取roll_pointer,取上一版本重新对比(在右边意味着,该事务在ReadView生成之后出现,在ReadView中意味着该事务还未提交)。以及条读隔离级别下的事务在每次查询的开始都会生成一个独立的ReadView,而可重复读隔离级别则在第一次读的时候生成一个ReadView,之后的读都复用之前的ReadView。这就是MySQL的MVCC通过版本链实现多版本,可并发读-写,写-读。通过ReadView生成策略的不同实现不同的隔离级别

主从复制

MySQL的主从复制中主要有三个线程: master,slave,master一条线程和slave中两条线程。

  • 主节点binlog,主从复制的基础是主库记录数据库的所有变更记录到binlog。binlog是数据库服务器启动的那一刻起,保存所有修改数据库结构或内容的一个文件
  • 主节点log dump线程,当Binlog有变动时,log dump线程读取其内容并发送给从节点
  • 从节点I/O线程接受binlog内容,并将其写入到relay log文件中
  • 从节点的SQL线程读取relay log文件内容堆数据更新进行重放,最终保证主从数据库的一致性
  • 注: 主从节点使用binlog文件 + position偏移量来定位主从同步的位置,从节点会保存其已接收到的偏移量,如果从节点发生宕机重启,则会自动从position的位置发起同步

由于MySQL默认的复制方式是异步的,主库把日志发送给从库后不关心从库是否已经处理,这样会产生一个问题就是假设主库挂了,从库失败了,这时候从库升为主库后,日志就丢失了。由此产生两个概念。

全同步复制

主库写入binlog后强制同步日志到从库,所有的从库都执行完成后才返回给客户端,但是很显然这个方式的话性能会受到严重影响

半同步复制

和全同步不同的是,半同步复制的逻辑是这样,从库写入日志成功后返回ACK确认给从库,主库收到至少一个从库的确认就认为写操作完成

MyISAM和InnoDB

MyISAM

不支持事务,但是每次查询都是原子的
支持表级锁,即每次操作是对整个表加锁
存储表的总行数
一个MyISAM表有三个文件: 索引文件,表结构文件,数据文件
采用非聚簇索引,索引文件的数据域存储指向数据文件的指针。辅助索引与主索引基本一致,但是辅助索引不用保证唯一性

InnoDB

支持ACID的事务,支持事务的四种隔离级别
支持行级锁及外键约束,因此可以支持写并发
不存储总行数
一个InnoDB引擎存储在一个文件空间(共享表空间,表大小不受操作系统控制,一个表可能分布在多个文件里),也有肯呢个为多个(设置为独立表空间,表大小受操作系统文件大小限制,一般为2G),受操作系统文件大小的限制
主键索引采用聚簇索引(索引的数据域存储数据文件本身),辅助索引的数据域存储主键的值,因此从辅助索引查找数据,需要先通过辅助索引找到主键值再访问辅助索引,最好使用自增主键,防止数据插入时,为维持B+树结构,文件的大调整

MySQL中索引类型及对数据库的性能的影响

  • 普通索引: 允许被索引的数据列包含重复的值
  • 唯一索引: 可以保证数据记录的唯一性
  • 主键索引: 是一种特殊的唯一索引,在一张表中只能定义一个主键索引,主键用于唯一标识一条记录,使用关键字PRIMARY KEY来创建
  • 联合索引: 索引可以覆盖多个数据列,如像INDEX(columnA, columnB)索引
  • 全文索引: 通过建立倒排索引,可以极大的提升检索效率,解决判断字段是否包含的问题,是目前搜索引擎使用的一种关键技术。可以通过ALTER TABLE table_name ADD FULLTEXT(column)创建全文索引

索引可以极大的提高数据的查询速度。通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。但是会降低插入,删除,更新表的速度,因为在执行这些写操作时,还要操作索引文件。索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索引,那么需要的空间机会更大,如果非聚簇索引很多,一旦聚簇索引改变,那么所有非聚簇索引都会跟着变

Redis

RDB和AOF

RDB: Redis DataBase。在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储

优点

整个Redis数据库将只包含一个文件dump.rdb,方便持久化
容灾性好,方便备份
性能最大化,fork子进程来完成写操作,让主进程继续处理命令,所以是I/O最大化。使用单独子进程来进行持久化,主进程不会进程任何I/O操作,保证了Redis的性能
相对于数据集大时,比AOF的启动效率更高

缺点

数据安全性低。RDB是间隔一段时间进行持久化,如果持久化之间Redis发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候
由于RDB是通过fork子进程来协助完成数据持久化工作的,因此如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒甚至一秒钟/

AOF: Append Only File。以日志的形式记录服务器所处理的每一个写,删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录

优点

数据安全,Redis中提供了3种不同策略,即每秒同步,每修改同步和不同步。事实上,每秒同步也是异步完成的,其效率也是非常高的,所差的是一旦系统出现宕机现象,那么这一秒种之内修改的数据将会丢失。而没修改同步,我们可以将其视为同步持久化,即每次发生的数据变化都会立即记录到磁盘中
通过append模式写文件,即使中途服务器宕机也不会破坏已经存在的内容,可以通过redis-check-aof工具解决数据一致性问题
AOF机制的rewrite模式,定期对AOF文件进行重写,以达到压缩的目的

缺点

AOF文件比RDB文件大,且恢复速度慢
数据集大的时候,比RDB启动效率低
运行效率没有RDB高

总结

AOF文件比RDB更新频率高,优先使用AOF还原数据
AOF比RDB更安全也更大
RDB性能比AOF好
如果两个都配了优先加载AOF

Redis的过期键的删除策略

Redis是key-value数据库,我们可以设置Redis中缓存的key的过期时间。Redis的过期策略就是指当Redis中缓存的key过期了,Redis如何处理

  • 惰性过期: 只有当访问一个key时,才会判断该key是否已经过期,过期则清除。该策略可以最大化的节省CPU资源,却对内存非常不友好。极端情况可能出现大量过期的key没有再次被访问,从而不会被清除,占用大量内存
  • 定期过期: 每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个这种方案。通过调整定是扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果

expired字典会保存所有设置了过期时间的key的过期时间数据,其中key是指向键空间中某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。Redis中同时使用了惰性过期和定期过期两种过期策略

缓存雪崩 缓存穿透 缓存击穿

缓存雪崩是指缓存同一时间大面积的失效,所以后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉

解决方案

缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生
给每一个缓存数据增加相应的缓存标记,记录缓存是否失效,如果缓存标记失效,则更新数据缓存
缓存预热
互斥锁

缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量的请求而崩掉

解决方案

接口层增加校验,如用户权限校验,id做基础检验,id < 0的直接拦截
从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没有读到数据,有同事去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库

解决方案

设置热点数据永远不过期
加互斥锁

Redis事务实现

  • 事务开始

MULTI命令执行,标识着一个事务的开始。MULTI命令会将客户端状态的flags属性中打开REDIS_MULTI标识来完成的

  • 命令入队

当一个客户端切换到事务状态之后,服务器会根据这个客户端发送来的命令来执行不同的操作。如果客户端发送的命令为MULTI,EXEC,WATCH,DISCARD中的一个,立即执行这个命令,否则将命令放入一个事务队列里面,然后向客户端返回QUEUE回复。如果客户端发送的命令为EXEC,DISCARD,WATCH,MULTI四个命令的其中一个,那么服务器立即执行这个命令。如果客户端发送的是四个命令以外的其他命令,那么服务器并不立即执行这个命令。首先检查此命令的格式是否正确,如果不正确,服务器会在客户端状态的flags属性关闭REDIS_MULTI标识,并且返回错误信息给客户端,如果正确,将这个命令放入一个事务队列里面,然后向客户端返回QUEUE回复。事务队列是按照FIFO的方式保存入队的命令

  • 事务执行

客户端发送EXEC命令,服务器执行EXEC命令逻辑。如果客户端状态的flags属性不包含REDIS_MULTI标识或者包含REDIS_DIRTY_CAS或者REDIS_DIRTY_EXEC标识,那么就直接取消事务的执行。否则客户端处于事务状态(flags有REDIS_MULTI标识),服务器会遍历客户端的事务队列,然后执行事务队列中的所有命令,最后将返回的结果全部返回给客户端。Redis不支持事务回滚机制,但是它会检查每一个事务中的命令是否错误。Redis事务不支持检查那些程序员自己逻辑错误。例如对String类型的数据库键执行对HashMap类型的操作

  • WATCH命令是一个乐观锁,可以为Redis事务提供check-and-set(CAS)行为。可以监控一个或多个键,一旦其中又一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令
  • MULTI命令用于开启一个事务,它总是返回OK。MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行
  • EXEC执行所有事物块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排序。当操作被打断时,返回空值NULL
  • 通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务,并且客户端会从事务状态中退出
  • UNWATCH命令可以取消watch对所有key的监控

Redis集群方案

  • 哨兵模式

sentinel,哨兵是Redis集群中非常重要的一个组件,主要以下几个功能

  1. 集群监控: 负责监控Redis master和slave进程是否正常工作
  2. 消息通知: 如果某个Redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员
  3. 故障转移: 如果master node 挂掉了,会自动转移到slave node上
  4. 配置中心: 如果故障转移发生了,通知client客户端新的master地址

哨兵用于实现Redis集群的高可用,本身也是分布式的,作为一个哨兵进群去运行,互相协同工作

  1. 故障转移时,判断一个master node是否宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举
  2. 即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的
  3. 哨兵通常需要3个实例来保证自己的健壮性
  4. 哨兵 + Redis主从的部署架构是不保证数据零丢失的,只能保证Redis集群的高可用性
  5. 对于哨兵 + Redis主从这种复杂的部署架构,尽量在测试环境和生产环境都进行充足的测试和演练
  • Redis Cluster

Redis Cluster是一种服务端Sharding技术,3.0版本开始正式提供。采用slot(槽)的概念,默认分配了16384个槽位。将请求发送到任意节点,接收请求的节点会将查询请求发送到正确的节点上执行

  1. 通过哈希的方式,将数据分片,每个节点均分存储一定哈希槽(哈希值)区间的数据,默认分配了16384个槽位
  2. 每份数据分片会存储在多个护卫主从的多节点上
  3. 数据写入先写主节点,再同步从节点(支持配置为阻塞同步)
  4. 同一分片多个节点间的数据不保持强一致性
  5. 读取数据时,当客户端操作的key没有分配在该节点上时,Redis会返回转向指令,指向正确的节点
  6. 扩容时需要把就节点数据前一一部分到新节点

在Redis Cluster架构下,每个Redis要放开两个端口号,比如一个是6379,另外一个就是加1w的端口号,比如16379。16379端口号是用来进行节点通信的,也就是Cluster Bus的通信,用来进行故障检测,配置更新,故障转移授权。Cluster Bus用了另外一种二进制的协议,gossip协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间

优点

无中心架构,支持动态扩容,对业务透明
具备Sentinel的监控和自动Failover(故障转移)能力
客户端不需要链接集群所有节点,链接集群中任何一个可用节点即可
高性能,客户端直连Redis服务免去了proxy代理的损耗

缺点

运维也很复杂,数据迁移需要人工干预
只能使用0号数据库
不支持批量操作(pipeline管道操作)
分布式逻辑和存储模块耦合等

Redis Sharding是Redis Cluster出来之前,业界普遍使用的多Redis实例集群方法。其主要思想是采用哈希算法将Redis数据的key进行散列,通过Hash函数,特定的key会映射到特定的Redis节点上。Java Redis客户端驱动Jedis,支持Redis Sharding功能,即ShardedJedis以及结合缓存池的ShardedJedisPool

优点

优势在于非常简单,服务端的Redis实例彼此独立相互无关联,每个Redis实例像单服务器一样运行,非常容易线性扩展,系统的灵活性很强

缺点

由于Sharding处理放到客户端,规模进一步扩大时给运维带来挑战
客户端Sharding不支持动态增删节点。服务端Redis实例群拓扑结构有变化时,每个客户端都需要更新调整。链接不能共享,当应用规模增大时,资源浪费制约优化

主从复制

通过执行slaveOf命令设置salveOf选项,让一个服务器去复制另一个服务器的数据。主数据库可以进行读写操作,当写操作导致数据变化时会自动将数据同步给数据库。而从数据库一般是只读的,并接收主数据库同步过来的数据。一个主数据库可以拥有多个从数据库,而一个从数据库只能拥有一个主数据库

全量复制

主节点通过bgSave命令fork子进程进行RDB持久化,该过程是非常消耗CPU,内存,硬盘I/O的
主节点通过网络将RDB文件发送给从节点,对主节点的带宽都会带来很多的消耗
从节点清空老数据,载入新RDB文件的过程是阻塞的,无法响应客户端的命令,如果从节点执行bgrewriteaof也会带来额外的消耗

部分复制

复制偏移量: 执行复制的双方,主从节点,分别会维护一个复制偏移量offset
复制积压缓冲区: 主节点内部维护了一个固定长度的,先进先出(FIFO)队列作为复制积压缓冲区,当从及诶单offset的差距过大超过缓冲区长度时,将无法执行部分复制,只能执行全量复制
服务器运行ID(runId): 每个Redis节点,都有其运行ID,运行由节点在启动时自动生成,主节点会将自己的运行ID发送给从节点,从节点会将主节点的运行ID存起来。从节点Redis断开重连的时候,就是根据运行ID来判断同步的进度

参考文献