java面试宝典
2021-07-16 16:47:12 60 举报
AI智能生成
宝典
作者其他创作
大纲/内容
Mybatis
集合
多线程
RabbitMq
基本面试题
Redis
基本数据结构
String
原理
struct``sdshdr{
//等于SDS保存字符串的长度
intlen;
//记录buf数组中未使用字节的数量
intfree;
//字节数组,用于保存字符串
charuf[];
}
//等于SDS保存字符串的长度
intlen;
//记录buf数组中未使用字节的数量
intfree;
//字节数组,用于保存字符串
charuf[];
}
list
原理就是LinkedList
set
map
原理就是HashMap
zset
Geo
经纬度存储
HyperLogLog
Pub/Sub
发布订阅模式
BitMap
底层原理
redis是由c语言开发,但是数据节后都是自己实现的SDS模式
高可用
主从模式
优点
读写分离,提高效率
数据热备份,提供多个副本
数据热备份,提供多个副本
缺点
主节点故障,集群则无法进行工作,可用性比较低,从节点升主节点需要人工手动干预
主节点的存储能力和写能力受到限制(只有一个主节点)
全量同步可能会造成毫秒或者秒级的卡顿现象
主节点的存储能力和写能力受到限制(只有一个主节点)
全量同步可能会造成毫秒或者秒级的卡顿现象
数据同步
全量复制
从节点第一次复制主节点数据
断点续传没有找到以前的主节点
断点续传没有找到以前的主节点
增量复制
参考断点续传(tudo)
断点续传
主节点会维护一个**backlog**记录所有从节点同步的**replica offset**和master的**run id**
每个从节点也会记录一份自己的**replica offset**和**master**的**run id**方便下次增量复制
每个从节点也会记录一份自己的**replica offset**和**master**的**run id**方便下次增量复制
哨兵模式
怎么判断主节点是否下线
如果一个master被标记为主观下线,那么正在监视这个master的所有Sentinel节点,要以每秒一次的频率确认该主服务器是否的确进入了主观下线状态。
如果有足够数量的Sentinel(至少要达到配置文件指定的数量)在指定的时间范围内同意这一判断,那么这个该主服务器被标记为客观下线。
如果有足够数量的Sentinel(至少要达到配置文件指定的数量)在指定的时间范围内同意这一判断,那么这个该主服务器被标记为客观下线。
领头哨兵怎么选出来的
使用Raft算法,第一个哨兵向其他哨兵发送请求,如果超过半数同意则他成为为领头哨兵
当有多个哨兵参选,则会出现没有任何节点当选的可能,此时每个参选节点将等待一个随即时间重新发起竞选,直到选举成功。
当有多个哨兵参选,则会出现没有任何节点当选的可能,此时每个参选节点将等待一个随即时间重新发起竞选,直到选举成功。
从节点怎么选举为主节点
所有先线的从数据库中,选择优先级最高的,优先级可以通过slave-priority来设置;
如果有多个一样优先级的从数据库,则复制的命令偏移量越大,越优先(与down掉的主数据库最接近);
如果还有多个备选,则选择运行ID较小的(运行ID不会重复);
选择好节点后,领头哨兵将想这个节点发送slaveofnoone,升级他为主数据库。
如果有多个一样优先级的从数据库,则复制的命令偏移量越大,越优先(与down掉的主数据库最接近);
如果还有多个备选,则选择运行ID较小的(运行ID不会重复);
选择好节点后,领头哨兵将想这个节点发送slaveofnoone,升级他为主数据库。
原来的主节点何去何从
已经停止服务的旧的主数据库更新为新的主数据库的从数据库,当其回复后自动以从数据库的身份加入到主从架构中
缺点
单节点写操作,限制了写的能力,如果master挂了,切换节点过程中,则不提供写能力
集群模式
简介
缺点
占用大量内存,系统相对复杂维护成本高
优点
基本符合高可用模式,能主动容错(哨兵模式)
实现了分布式,每个节点保存的数据都是不一样的
实现了分布式,每个节点保存的数据都是不一样的
基本模式
基本概念
多个哨兵模式共同建立的集群模式,一个哨兵模式就是一个节点
每个节点都有两个东西:插槽(slot)和cluster,插槽取值范围0-16383,平均分配到每个master节点上)
cluster是一个集群管理的插件,负责维护node<->slot<->value
每个节点都有两个东西:插槽(slot)和cluster,插槽取值范围0-16383,平均分配到每个master节点上)
cluster是一个集群管理的插件,负责维护node<->slot<->value
持久化
AOF
数据完善
只存储所有的增删操作
文件大数据恢复速度慢
针对淘汰机制的处理
从内存数据库持久化数据到AOF文件:
当key过期后,还没有被删除,此时进行执行持久化操作(该key是不会进入aof文件的,因为没有发生修改命令)
当key过期后,在发生删除操作时,程序会向aof文件追加一条del命令(在将来的以aof文件恢复数据的时候该过期的键就会被删掉)
AOF重写:重写时,会先判断key是否过期,已过期的key不会重写到aof文件
当key过期后,还没有被删除,此时进行执行持久化操作(该key是不会进入aof文件的,因为没有发生修改命令)
当key过期后,在发生删除操作时,程序会向aof文件追加一条del命令(在将来的以aof文件恢复数据的时候该过期的键就会被删掉)
AOF重写:重写时,会先判断key是否过期,已过期的key不会重写到aof文件
RDB
可通过配置文件设置快照生成速度
save 900 1 #在900秒(15分钟)之后,如果至少有1个key发生变化,则dump内存快照。
save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,则dump内存快照。
save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,则dump内存快照。
save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,则dump内存快照。
save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,则dump内存快照。
生成标准规则
在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,
先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储
先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储
存储数据不及时
最多5分钟重新生成一个快照
消耗cpu
针对淘汰机制的处理
从内存数据库持久化数据到RDB文件,持久化key之前,会检查是否过期,过期的key不进入RDB文件
从RDB文件恢复数据到内存数据库数据载入数据库之前,会对key先进行过期检查,如果过期,不导入数据库(主库情况)
从RDB文件恢复数据到内存数据库数据载入数据库之前,会对key先进行过期检查,如果过期,不导入数据库(主库情况)
过期策略
定时删除
含义
在设置key的过期时间的同时,为该key创建一个定时器,让定时器在key的过期时间来临时,对key进行删除
优点
保证内存被及时的尽快的释放
缺点
若过期key很多,删除这些key会占用很多的CPU时间,在CPU时间紧张的情况下,CPU不能把所有的时间用来做要紧的事儿,还需要去花时间删除这些key
定时器的创建耗时,若为每一个设置过期时间的key创建一个定时器(将会有大量的定时器产生),性能影响严重
没人用
定时器的创建耗时,若为每一个设置过期时间的key创建一个定时器(将会有大量的定时器产生),性能影响严重
没人用
惰性删除
含义
key过期的时候不删除,每次从数据库获取key的时候去检查是否过期,若过期,则删除,返回null
优点
删除操作只发生在从数据库取出key的时候发生,而且只删除当前key,所以对CPU时间的占用是比较少的,而且此时的删除是已经到了非做不可的地步
(如果此时还不删除的话,我们就会获取到了已经过期的key了)
(如果此时还不删除的话,我们就会获取到了已经过期的key了)
缺点
若大量的key在超出超时时间后,很久一段时间内,都没有被获取过,那么可能发生内存泄露(无用的垃圾占用了大量的内存)
定期删除
含义
每隔一段时间执行一次删除(在redis.conf配置文件设置hz,1s刷新的频率)过期key操作
优点
通过限制删除操作的时长和频率,来减少删除操作对CPU时间的占用--处理"定时删除"的缺点
定期删除过期key--处理"惰性删除"的缺点
定期删除过期key--处理"惰性删除"的缺点
缺点
在内存友好方面,不如"定时删除"
在CPU时间友好方面,不如"惰性删除"
在CPU时间友好方面,不如"惰性删除"
难点
合理设置删除操作的执行时长(每次删除执行多长时间)和执行频率(每隔多长时间做一次删除)(这个要根据服务器运行情况来定了)
总结
定时删除和定期删除为主动删除:Redis会定期主动淘汰一批已过去的key
惰性删除为被动删除:用到的时候才会去检验key是不是已过期,过期就删除
惰性删除为redis服务器内置策略
定期删除可以通过:
-第一、配置redis.conf的hz选项,默认为10(即1秒执行10次,100ms一次,值越大说明刷新频率越快,最Redis性能损耗也越大)
-第二、配置redis.conf的maxmemory最大值,当已用内存超过maxmemory限定时,就会触发主动清理策略
惰性删除为被动删除:用到的时候才会去检验key是不是已过期,过期就删除
惰性删除为redis服务器内置策略
定期删除可以通过:
-第一、配置redis.conf的hz选项,默认为10(即1秒执行10次,100ms一次,值越大说明刷新频率越快,最Redis性能损耗也越大)
-第二、配置redis.conf的maxmemory最大值,当已用内存超过maxmemory限定时,就会触发主动清理策略
淘汰机制
含义
子主题
机制
noeviction:禁止驱逐数据。默认配置都是这个。当内存使用达到阀值的时候,所有引起申请内存的命令都会报错。
volatile-lru:从设置了过期时间的数据集中挑选最近最少使用的数据淘汰。
volatile-ttl:从已设置了过期时间的数据集中挑选即将要过期的数据淘汰。
volatile-random:从已设置了过期时间的数据集中任意选择数据淘汰。
allkeys-lru:从数据集中挑选最近最少使用的数据淘汰。
allkeys-random:从数据集中任意选择数据淘汰。
volatile-lru:从设置了过期时间的数据集中挑选最近最少使用的数据淘汰。
volatile-ttl:从已设置了过期时间的数据集中挑选即将要过期的数据淘汰。
volatile-random:从已设置了过期时间的数据集中任意选择数据淘汰。
allkeys-lru:从数据集中挑选最近最少使用的数据淘汰。
allkeys-random:从数据集中任意选择数据淘汰。
基本信息
缓存级别的中间件
单节点处理数据能力
写:8w
读:10w
常见面试题
缓存击穿
缓存雪崩
Jvm
基础
基本组成
方法区
常量池
虚拟栈
有多个线程,线程中有多个栈帧(详解)
局部变量表
操作数栈
动态链接
方法出口
本地方法栈
堆
新生区
老年区
永久区
默认的,新生代(Young)与老年代(Old)的比例的值为1:2,可以通过参数–XX:NewRatio配置
默认的,Edem:from:to=8:1:1(可以通过参数–XX:SurvivorRatio来设定)
Survivor区中的对象被复制次数为15(对应虚拟机参数-XX:+MaxTenuringThreshold)
新生区
老年区
永久区
默认的,新生代(Young)与老年代(Old)的比例的值为1:2,可以通过参数–XX:NewRatio配置
默认的,Edem:from:to=8:1:1(可以通过参数–XX:SurvivorRatio来设定)
Survivor区中的对象被复制次数为15(对应虚拟机参数-XX:+MaxTenuringThreshold)
程序计数器
可达性算法
类加载机制
双亲委派原则
超级简单不会自己百度
流程
加载
通过一个类的全限定名来获取定义此类的二进制字节流
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
验证
确保被加载的类的正确性
确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
文件格式验证:验证字节流是否符合Class文件格式的规范,如:是否以模数0xCAFEBABE开头、主次版本号是否在当前虚拟机处理范围内等等。
元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求;如:这个类是否有父类,是否实现了父类的抽象方法,是否重写
了父类的final方法,是否继承了被final修饰的类等等
字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的,如:操作数栈的数据类型与指令代码序列能配合工作,保证方法中的类型转换有效等等
符号引用验证:确保解析动作能正确执行;如:通过符合引用能找到对应的类和方法,符号引用中类、属性、方法的访问性是否能被当前类访问等等
验证阶段是非常重要的,但不是必须的。可以采用-Xverify:none参数来关闭大部分的类验证措施
确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
文件格式验证:验证字节流是否符合Class文件格式的规范,如:是否以模数0xCAFEBABE开头、主次版本号是否在当前虚拟机处理范围内等等。
元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求;如:这个类是否有父类,是否实现了父类的抽象方法,是否重写
了父类的final方法,是否继承了被final修饰的类等等
字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的,如:操作数栈的数据类型与指令代码序列能配合工作,保证方法中的类型转换有效等等
符号引用验证:确保解析动作能正确执行;如:通过符合引用能找到对应的类和方法,符号引用中类、属性、方法的访问性是否能被当前类访问等等
验证阶段是非常重要的,但不是必须的。可以采用-Xverify:none参数来关闭大部分的类验证措施
准备
为类的静态变量分配内存,并将其赋默认值
为类变量分配内存并设置类变量初始值,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:
只对static修饰的静态变量进行内存分配、赋默认值(如0、0L、null、false等)
对final的静态字面值常量直接赋初值(赋初值不是赋默认值,如果不是字面值静态常量,那么会和静态变量一样赋默认值)
为类变量分配内存并设置类变量初始值,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:
只对static修饰的静态变量进行内存分配、赋默认值(如0、0L、null、false等)
对final的静态字面值常量直接赋初值(赋初值不是赋默认值,如果不是字面值静态常量,那么会和静态变量一样赋默认值)
解析
将常量池中的符号引用替换为直接引用(内存地址)的过程
符号引用就是一组符号来描述目标,可以是任何字面量。属于编译原理方面的概念如:包括类和接口的全限定名、字段的名称和描述符、方法的名称和描述符
直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。如指向方法区某个类的一个指针
符号引用就是一组符号来描述目标,可以是任何字面量。属于编译原理方面的概念如:包括类和接口的全限定名、字段的名称和描述符、方法的名称和描述符
直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。如指向方法区某个类的一个指针
初始化
为类的静态变量赋初值,只有对类的主动使用才会导致类的初始化
使用
在编译生成class文件时,编译器会产生两个方法加于class文件中,一个是类的初始化方法clinit,另一个是实例的初始化方法init
clinit
如果类中没有静态变量或静态代码块,那么clinit方法将不会被生成
在执行clinit方法时,必须先执行父类的clinit方法
clinit方法只执行一次
static变量的赋值操作和静态代码块的合并顺序由源文件中出现的顺序决定
在执行clinit方法时,必须先执行父类的clinit方法
clinit方法只执行一次
static变量的赋值操作和静态代码块的合并顺序由源文件中出现的顺序决定
init
如果类中没有成员变量和代码块,那么clinit方法将不会被生成
在执行init方法时,必须先执行父类的init方法
init方法每实例化一次就会执行一次
init方法先为实例变量分配内存空间,再执行赋默认值,然后根据源码中的顺序执行赋初值或代码块
在执行init方法时,必须先执行父类的init方法
init方法每实例化一次就会执行一次
init方法先为实例变量分配内存空间,再执行赋默认值,然后根据源码中的顺序执行赋初值或代码块
卸载
执行了System.exit()方法
程序正常执行结束
程序在执行过程中遇到了异常或错误而异常终止
由于操作系统出现错误而导致Java虚拟机进程终止
程序正常执行结束
程序在执行过程中遇到了异常或错误而异常终止
由于操作系统出现错误而导致Java虚拟机进程终止
类加载方式
隐式加载
创建类对象
使用类的静态域
创建子类对象
使用子类的静态域
在JVM启动时,BootStrapLoader会加载一些JVM自身运行所需的class(父类未nul,因为jvm是C语言编写java无法表示l)
在JVM启动时,ExtClassLoader会加载指定目录下一些特殊的class
在JVM启动时,AppClassLoader会加载classpath路径下的class,以及main函数
使用类的静态域
创建子类对象
使用子类的静态域
在JVM启动时,BootStrapLoader会加载一些JVM自身运行所需的class(父类未nul,因为jvm是C语言编写java无法表示l)
在JVM启动时,ExtClassLoader会加载指定目录下一些特殊的class
在JVM启动时,AppClassLoader会加载classpath路径下的class,以及main函数
显式加载
ClassLoader.loadClass(className),只加载和连接、不会进行初始化
Class.forName(Stringname,booleaninitialize,ClassLoaderloader);使用loader进行加载和连接,根据参数initialize决定是否初始化。
Class.forName(Stringname,booleaninitialize,ClassLoaderloader);使用loader进行加载和连接,根据参数initialize决定是否初始化。
垃圾回收
面试题
简单说说你了解的类加载器,可以打破双亲委派么,怎么打破
打破双亲委派机制则不仅要继承ClassLoader类,还要重写loadClass和findClass方法
强引用、软引用、弱引用、虚引用的区别
子主题
spring
核心
Ioc
Aop
子主题
springCloud
注册中心
nacos
eureka
zookeeper
网关
zuul
getway
负载均衡
ribbon
服务调用
fegin
服务熔点
hystrix
Mysql
面试题
Mysql事务实现原理
参考下边的 事务 MVVC
Mysql索引原理
参考下边的 数据结构 B+树
索引失效情况
索引失效的7种情况
有or必全有索引
复合索引未用左列字段
like以%开头
需要类型转换
where中索引列有运算
where中索引列使用了函数
如果mysql觉得全表扫描更快时(数据少)
复合索引未用左列字段
like以%开头
需要类型转换
where中索引列有运算
where中索引列使用了函数
如果mysql觉得全表扫描更快时(数据少)
sql优化方案
开启慢查询日志
通过命令查看慢查询最长时间,一般默认10s
SHOWVARIABLESLIKE'long_query_time'
可以看到,slow_query_log 默认是 OFF,我们必须要打开它
SETGLOBALslow_query_log=ON
打开Mysql安装位置,找到my.ini文件并打开,查找datadir对应的目录,日志文件就放在该目录下
showstatuslike'slow_queries'//显示慢查询次数
SHOWVARIABLESLIKE'long_query_time'
可以看到,slow_query_log 默认是 OFF,我们必须要打开它
SETGLOBALslow_query_log=ON
打开Mysql安装位置,找到my.ini文件并打开,查找datadir对应的目录,日志文件就放在该目录下
showstatuslike'slow_queries'//显示慢查询次数
查看sql执行计划EXPLAIN
查看他是否走预期的索引,和sql执行级别
拒接出现 select *
Mysql日志
bin log
redo log
概念
redolog是重做日志,提供前滚操作
意义
redolog通常是物理日志,记录的是数据页的物理修改,而不是某一行或某几行修改成怎样怎样,它用来恢复提交后的物理数据页(恢复数据页,且只能
恢复到最后一次提交的位置)
恢复到最后一次提交的位置)
作用
InnoDB作为MySQL的存储引擎,数据是存放在磁盘中的,但如果每次读写数据都需要磁盘IO,效率会很低。为此,InnoDB提供了缓存(BufferPool)
,BufferPool中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲:当从数据库读取数据时,会首先从BufferPool中读取,如果BufferPool中
没有,则从磁盘读取后放入BufferPool;当向数据库写入数据时,会首先写入BufferPool,BufferPool中修改的数据会定期刷新到磁盘中(这一过程称
为刷脏)
BufferPool的使用大大提高了读写数据的效率,但是也带了新的问题:如果MySQL宕机,而此时BufferPool中修改的数据还没有刷新到磁盘,就会导致
数据的丢失,事务的持久性无法保证
于是,redolog被引入来解决这个问题:当数据修改时,除了修改BufferPool中的数据,还会在redolog记录这次操作;当事务提交时,会调用fsync接
口对redolog进行刷盘。如果MySQL宕机,重启时可以读取redolog中的数据,对数据库进行恢复。redolog采用的是WAL(Write-aheadlogging,预
写式日志),所有修改先写入日志,再更新到BufferPool,保证了数据不会因MySQL宕机而丢失,从而满足了持久性要求
既然redolog也需要在事务提交时将日志写入磁盘,为什么它比直接将BufferPool中修改的数据写入磁盘(即刷脏)要快呢?主要有以下两方面的原因:
(1)刷脏是随机IO,因为每次修改的数据位置随机,但写redolog是追加操作,属于顺序IO。
(2)刷脏是以数据页(Page)为单位的,MySQL默认页大小是16KB,一个Page上一个小修改都要整页写入;而redolog中只包含真正需要写入的部分,
无效IO大大减少
,BufferPool中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲:当从数据库读取数据时,会首先从BufferPool中读取,如果BufferPool中
没有,则从磁盘读取后放入BufferPool;当向数据库写入数据时,会首先写入BufferPool,BufferPool中修改的数据会定期刷新到磁盘中(这一过程称
为刷脏)
BufferPool的使用大大提高了读写数据的效率,但是也带了新的问题:如果MySQL宕机,而此时BufferPool中修改的数据还没有刷新到磁盘,就会导致
数据的丢失,事务的持久性无法保证
于是,redolog被引入来解决这个问题:当数据修改时,除了修改BufferPool中的数据,还会在redolog记录这次操作;当事务提交时,会调用fsync接
口对redolog进行刷盘。如果MySQL宕机,重启时可以读取redolog中的数据,对数据库进行恢复。redolog采用的是WAL(Write-aheadlogging,预
写式日志),所有修改先写入日志,再更新到BufferPool,保证了数据不会因MySQL宕机而丢失,从而满足了持久性要求
既然redolog也需要在事务提交时将日志写入磁盘,为什么它比直接将BufferPool中修改的数据写入磁盘(即刷脏)要快呢?主要有以下两方面的原因:
(1)刷脏是随机IO,因为每次修改的数据位置随机,但写redolog是追加操作,属于顺序IO。
(2)刷脏是以数据页(Page)为单位的,MySQL默认页大小是16KB,一个Page上一个小修改都要整页写入;而redolog中只包含真正需要写入的部分,
无效IO大大减少
undo log
概念
undolog是回滚日志,提供回滚操作
意义
undo用来回滚行记录到某个版本。undolog一般是逻辑日志,根据每行记录进行记录。
总结
redolog保证数据持久性,undo log保证数据原子性
作用
undolog,实现原子性的关键,是当事务回滚时能够撤销所有已经成功执行的sql语句。InnoDB实现回滚,靠的是undolog:当事务对数据库进行
修改时,InnoDB会生成对应的undolog;如果事务执行失败或调用了rollback,导致事务需要回滚,便可以利用undolog中的信息将数据回滚到
修改之前的样子
undolog属于逻辑日志,它记录的是sql执行相关的信息。当发生回滚时,InnoDB会根据undolog的内容做与之前相反的工作:对于每个insert,回
滚时会执行delete;对于每个delete,回滚时会执行insert;对于每个update,回滚时会执行一个相反的update,把数据改回去
当事务执行update时,其生成的undolog中会包含被修改行的主键(以便知道修改了哪些行)、修改了哪些列、这些列在修改前后的值等信息,回滚时
便可以使用这些信息将数据还原到update之前的状态
修改时,InnoDB会生成对应的undolog;如果事务执行失败或调用了rollback,导致事务需要回滚,便可以利用undolog中的信息将数据回滚到
修改之前的样子
undolog属于逻辑日志,它记录的是sql执行相关的信息。当发生回滚时,InnoDB会根据undolog的内容做与之前相反的工作:对于每个insert,回
滚时会执行delete;对于每个delete,回滚时会执行insert;对于每个update,回滚时会执行一个相反的update,把数据改回去
当事务执行update时,其生成的undolog中会包含被修改行的主键(以便知道修改了哪些行)、修改了哪些列、这些列在修改前后的值等信息,回滚时
便可以使用这些信息将数据还原到update之前的状态
redolog与binlog
我们知道,在MySQL中还存在binlog(二进制日志)也可以记录写操作并用于数据的恢复,但二者是有着根本的不同的:
(1)作用不同:redolog是用于crashrecovery的,保证MySQL宕机也不会影响持久性;binlog是用于point-in-timerecovery的,保证服务器可以
基于时间点恢复数据,此外binlog还用于主从复制。
(2)层次不同:redolog是InnoDB存储引擎实现的,而binlog是MySQL的服务器层(可以参考文章前面对MySQL逻辑架构的介绍)实现的,同时支持
InnoDB和其他存储引擎。
(3)内容不同:redolog是物理日志,内容基于磁盘的Page;binlog的内容是二进制的,根据binlog_format参数的不同,可能基于sql语句、基于
数据本身或者二者的混合。
(4)写入时机不同:binlog在事务提交时写入;redolog的写入时机相对多元:
前面曾提到:当事务提交时会调用fsync对redolog进行刷盘;这是默认情况下的策略,修改innodb_flush_log_at_trx_commit参数可以改变该策
略,但事务的持久性将无法保证。
除了事务提交时,还有其他刷盘时机:如masterthread每秒刷盘一次redolog等,这样的好处是不一定要等到commit时刷盘,commit速度大大加快
(1)作用不同:redolog是用于crashrecovery的,保证MySQL宕机也不会影响持久性;binlog是用于point-in-timerecovery的,保证服务器可以
基于时间点恢复数据,此外binlog还用于主从复制。
(2)层次不同:redolog是InnoDB存储引擎实现的,而binlog是MySQL的服务器层(可以参考文章前面对MySQL逻辑架构的介绍)实现的,同时支持
InnoDB和其他存储引擎。
(3)内容不同:redolog是物理日志,内容基于磁盘的Page;binlog的内容是二进制的,根据binlog_format参数的不同,可能基于sql语句、基于
数据本身或者二者的混合。
(4)写入时机不同:binlog在事务提交时写入;redolog的写入时机相对多元:
前面曾提到:当事务提交时会调用fsync对redolog进行刷盘;这是默认情况下的策略,修改innodb_flush_log_at_trx_commit参数可以改变该策
略,但事务的持久性将无法保证。
除了事务提交时,还有其他刷盘时机:如masterthread每秒刷盘一次redolog等,这样的好处是不一定要等到commit时刷盘,commit速度大大加快
事务
四大特性
原子性
要执行的事务是一个独立的操作单元,要么全部执行,要么全部不执行
undo log保证原子性
一致性
事务的一致性是指事务的执行不能破坏数据库的一致性,一致性也称为完整性。一个事务在执行后,数据库必须从一个一致性状态转变为另一个一致性状态
隔离性
多个事务并发执行时,一个事务的执行不应影响其他事务的执行,SQL92规范中对隔离性定义了不同的隔离级别
隔离级别
(ReadUncommitted)读未提交:一个事务可以读取到另一个事务未提交的修改。这会带来脏读、幻读、不可重复读问题。(基本没用)
(ReadCommitted)读已提交:一个事务只能读取另一个事务已经提交的修改。其避免了脏读,但仍然存在不可重复读和幻读问题
(RepeatableRead)可重复读:同一个事务中多次读取相同的数据返回的结果是一样的。其避免了脏读和不可重复读问题,但幻读依然存在
(Serializable)串行化:事务串行执行。避免了以上所有问题
默认的隔离级别是REPEATABLE-READ(可重复读)
脏读,幻读,不可重复读,丢失修改
脏读:事务A查询数据后进行了一次修改且未提交,而事务B这个时候去查询,然后使用了这个数据,因为这个数据还没有被事务A提交到数据库中,所以事务
B的得到数据就是脏数据,对脏数据进行操作可能是不正确的
B的得到数据就是脏数据,对脏数据进行操作可能是不正确的
不可重复读:事务A访问了两次数据,但是这访问第二次之间事务B进行一次并进行了修改,导致事务A访问第二次的时候得到的数据与第一次不同,导致一个
事务访问两次数据得到的数据不相同。因此叫做不可重复读
事务访问两次数据得到的数据不相同。因此叫做不可重复读
幻读:与不可重复读都点相似,只是这次是事务B在事务A访问第二次的之前做了一个新增,导致事务A第二次读取的时候发现了多的记录,这就是幻读。
丢失修改:事务A访问该数据,事务B也访问该数据,事务A修改了该数据,事务B也修改了该数据,这样导致事务A的修改被丢失,因此称为丢失修改;
持久性
redo log保证数据持久性
基本概念
只有InnoDB支持事务,所有这里说的事务隔离级别指的是InnoDB下的事务隔离级别。
数据库事务是构成单一逻辑工作单元的操作集合
数据库事务可以包含一个或多个数据库操作,但这些操作构成一个逻辑上的整体
要么全部执行要么全部不执行要么全部产生影响要么全不产生
要么全部执行要么全部不执行要么全部产生影响要么全不产生
MVCC
自己的总结
概述
MVCC的目的就是多版本并发控制,在数据库中的实现,就是为了解决读写冲突,它的实现原理主要是依赖记录中的3个隐式字段,undo日志,ReadView
来实现的。所以我们先来看看这个三个point的概念
来实现的。所以我们先来看看这个三个point的概念
基本原理
隐藏列:InnoDB中每行数据都有隐藏列,隐藏列中包含了本行数据的事务id、指向undolog的指针等。
基于undolog的版本链:前面说到每行数据的隐藏列中包含了指向undolog的指针,而每条undolog也会指向更早版本的undolog,从而形成一条版本链。
ReadView:通过隐藏列和版本链,MySQL可以将数据恢复到指定版本;但是具体要恢复到哪个版本,则需要根据ReadView来确定。所谓ReadView,
是指事务(记做事务A)在某一时刻给整个事务系统(trx_sys)打快照,之后再进行读操作时,会将读取到的数据中的事务id与trx_sys快照比较,从而
判断数据对该ReadView是否可见,即对事务A是否可见
是指事务(记做事务A)在某一时刻给整个事务系统(trx_sys)打快照,之后再进行读操作时,会将读取到的数据中的事务id与trx_sys快照比较,从而
判断数据对该ReadView是否可见,即对事务A是否可见
trx_sys中的主要内容,以及判断可见性的方法如下:
low_limit_id:表示生成ReadView时系统中应该分配给下一个事务的id。如果数据的事务id大于等于low_limit_id,则对该ReadView不可见。
up_limit_id:表示生成ReadView时当前系统中活跃的读写事务中最小的事务id。如果数据的事务id小于up_limit_id,则对该ReadView可见。
rw_trx_ids:表示生成ReadView时当前系统中活跃的读写事务的事务id列表。如果数据的事务id在low_limit_id和up_limit_id之间,则需要判断事
务id是否在rw_trx_ids中:如果在,说明生成ReadView时事务仍在活跃中,因此数据对ReadView不可见;如果不在,说明生成ReadView时事务已
经提交了,因此数据对ReadView可见
low_limit_id:表示生成ReadView时系统中应该分配给下一个事务的id。如果数据的事务id大于等于low_limit_id,则对该ReadView不可见。
up_limit_id:表示生成ReadView时当前系统中活跃的读写事务中最小的事务id。如果数据的事务id小于up_limit_id,则对该ReadView可见。
rw_trx_ids:表示生成ReadView时当前系统中活跃的读写事务的事务id列表。如果数据的事务id在low_limit_id和up_limit_id之间,则需要判断事
务id是否在rw_trx_ids中:如果在,说明生成ReadView时事务仍在活跃中,因此数据对ReadView不可见;如果不在,说明生成ReadView时事务已
经提交了,因此数据对ReadView可见
如果基础实在太差看不懂请参考
https://baijiahao.baidu.com/s?id=1669272579360136533&wfr=spider&for=pc
https://www.cnblogs.com/kismetv/p/10331633.html
https://www.cnblogs.com/kismetv/p/10331633.html
锁机制
锁的粒度
行锁
表锁
全局锁
分类
共享/排它锁(SharedandExclusiveLocks)
共享锁(ShareLocks,记为S锁),读取数据时加S锁
排他锁(eXclusiveLocks,记为X锁),修改数据时加X锁
使用的语义为:
共享锁之间不互斥,简记为:读读可以并行
排他锁与任何锁互斥,简记为:写读,写写不可以并行
可以看到,一旦写数据的任务没有完成,数据是不能被其他任务读取的,这对并发度有较大的影响。对应到数据库,可以理解为,写事务没有提交,
读相关数据的select也会被阻塞,这里的select是指加了锁的,普通的select仍然可以读到数据(快照读)
排他锁(eXclusiveLocks,记为X锁),修改数据时加X锁
使用的语义为:
共享锁之间不互斥,简记为:读读可以并行
排他锁与任何锁互斥,简记为:写读,写写不可以并行
可以看到,一旦写数据的任务没有完成,数据是不能被其他任务读取的,这对并发度有较大的影响。对应到数据库,可以理解为,写事务没有提交,
读相关数据的select也会被阻塞,这里的select是指加了锁的,普通的select仍然可以读到数据(快照读)
意向锁(IntentionLocks)
记录锁(RecordLocks)
间隙锁(GapLocks)
间隙锁,它封锁索引记录中的间隔,或者第一条索引记录之前的范围,又或者最后一条索引记录之后的范围。依然是上面的例子,InnoDB,RR:
select*fromlock_examplewhereidbetween8and15forupdate;
这个SQL语句会封锁区间(8,15),以阻止其他事务插入id位于该区间的记录。
间隙锁的主要目的,就是为了防止其他事务在间隔中插入数据,以导致“不可重复读”。如果把事务的隔离级别降级为读提交(ReadCommitted,RC),
间隙锁则会自动失效
select*fromlock_examplewhereidbetween8and15forupdate;
这个SQL语句会封锁区间(8,15),以阻止其他事务插入id位于该区间的记录。
间隙锁的主要目的,就是为了防止其他事务在间隔中插入数据,以导致“不可重复读”。如果把事务的隔离级别降级为读提交(ReadCommitted,RC),
间隙锁则会自动失效
临键锁(Next-keyLocks)
插入意向锁(InsertIntentionLocks)
自增锁(Auto-incLocks)
自增锁是一种特殊的表级别锁(table-levellock),专门针对事务插入AUTO_INCREMENT类型的列。最简单的情况,如果一个事务正在往表中插入记录
,所有其他事务的插入必须等待,以便第一个事务插入的行,是连续的主键值。
举个例子(表依然是如上的例子lock_example),但是id为AUTO_INCREMENT,数据库表中数据为:
1,zhangsan
2,lisi
3,wangwu
事务A先执行,还未提交:insertintot(name)values(xxx);
事务B后执行:insertintot(name)values(ooo);
此时事务B插入操作会阻塞,直到事务A提交
,所有其他事务的插入必须等待,以便第一个事务插入的行,是连续的主键值。
举个例子(表依然是如上的例子lock_example),但是id为AUTO_INCREMENT,数据库表中数据为:
1,zhangsan
2,lisi
3,wangwu
事务A先执行,还未提交:insertintot(name)values(xxx);
事务B后执行:insertintot(name)values(ooo);
此时事务B插入操作会阻塞,直到事务A提交
参考博客
子主题
索引
分类
组合索引
最左原则:B+Tree排序规则:按照从左到右字段排序,先比较第一个字段,如果第一个字段相同再比较第二个字段....以此类推
如果不遵守直接从第二个索引条件开始查询,则不走索引,因为第二索引,只有在第一个索引的基础上才会有序
如果不遵守直接从第二个索引条件开始查询,则不走索引,因为第二索引,只有在第一个索引的基础上才会有序
主键
唯一
全文
普通
数据结构
B+树
概念
InnoDB是通过B+Tree结构对主键创建索引,然而叶子节点中存储记录,如果没有主键,那么会选择唯一键,如果没有唯一键,那么会生成一个6位的
row_id作为主键
非叶子节点不存储data数据,只存储索引(冗余),保证可以存放更多的索引,减少树深度
叶子节点包含包含所有索引字段
叶子节点之间用指针连接,提高区间访问的性能
row_id作为主键
非叶子节点不存储data数据,只存储索引(冗余),保证可以存放更多的索引,减少树深度
叶子节点包含包含所有索引字段
叶子节点之间用指针连接,提高区间访问的性能
Hash
利用hash存储的话需要将所有的数据文件添加到内存,比较耗费内存空间,其不太支持范围查询
聚簇索引
包含主键索引和对应的实际数据,索引的叶子节点就是数据节点,也就是说找到了索引也就找到了数据
非聚簇索引
索引的存储和数据的存储是分离的,也就是说找到了索引但没找到数据,需要根据索引上的值(主键)再次回表查询,非聚簇索引也叫做辅助索引
二级索引访问需要两次索引查找,第一次找到主键值,第二次根据主键值找到行数据
二级索引访问需要两次索引查找,第一次找到主键值,第二次根据主键值找到行数据
Mysql架构图
基本架构
架构图
组成
连接器
查询缓存
连接建立完成后,你就可以执行select语句了。执行逻辑就会来到第二步:查询缓存。
MySQL拿到一个查询请求后,会先到查询缓存看看,之前是不是执行过这条语句。之前执行过的语句及其结果可能会以key-value对的形式,被直接
缓存在内存中。key是查询的语句,value是查询的结果。如果你的查询能够直接在这个缓存中找到key,那么这个value就会被直接返回给客户端。
如果语句不在查询缓存中,就会继续后面的执行阶段。执行完成后,执行结果会被存入查询缓存中。你可以看到,如果查询命中缓存,MySQL不需要执
行后面的复杂操作,就可以直接返回结果,这个效率会很高。
**但是大多数情况下我会建议你不要使用查询缓存,为什么呢?因为查询缓存往往弊大于利。**
查询缓存的失效非常频繁,只要有对一个表的更新,这个表上所有的查询缓存都会被清空。因此很可能你费劲地把结果存起来,还没使用呢,就被一个更
新全清空了。对于更新压力大的数据库来说,查询缓存的命中率会非常低。除非你的业务就是有一张静态表,很长时间才会更新一次。比如,一个系统配
置表,那这张表上的查询才适合使用查询缓存。
好在MySQL也提供了这种“按需使用”的方式。你可以将参数query_cache_type设置成DEMAND,这样对于默认的SQL语句都不使用查询缓存。而对于你确
定要使用查询缓存的语句,可以用SQL_CACHE显式指定,像下面这个语句一样:
mysql>selectSQL_CACHE*fromTwhereID=10;
需要注意的是,MySQL8.0版本直接将查询缓存的整块功能删掉了,也就是说8.0开始彻底没有这个功能了
MySQL拿到一个查询请求后,会先到查询缓存看看,之前是不是执行过这条语句。之前执行过的语句及其结果可能会以key-value对的形式,被直接
缓存在内存中。key是查询的语句,value是查询的结果。如果你的查询能够直接在这个缓存中找到key,那么这个value就会被直接返回给客户端。
如果语句不在查询缓存中,就会继续后面的执行阶段。执行完成后,执行结果会被存入查询缓存中。你可以看到,如果查询命中缓存,MySQL不需要执
行后面的复杂操作,就可以直接返回结果,这个效率会很高。
**但是大多数情况下我会建议你不要使用查询缓存,为什么呢?因为查询缓存往往弊大于利。**
查询缓存的失效非常频繁,只要有对一个表的更新,这个表上所有的查询缓存都会被清空。因此很可能你费劲地把结果存起来,还没使用呢,就被一个更
新全清空了。对于更新压力大的数据库来说,查询缓存的命中率会非常低。除非你的业务就是有一张静态表,很长时间才会更新一次。比如,一个系统配
置表,那这张表上的查询才适合使用查询缓存。
好在MySQL也提供了这种“按需使用”的方式。你可以将参数query_cache_type设置成DEMAND,这样对于默认的SQL语句都不使用查询缓存。而对于你确
定要使用查询缓存的语句,可以用SQL_CACHE显式指定,像下面这个语句一样:
mysql>selectSQL_CACHE*fromTwhereID=10;
需要注意的是,MySQL8.0版本直接将查询缓存的整块功能删掉了,也就是说8.0开始彻底没有这个功能了
分析器
如果没有命中查询缓存,就要开始真正执行语句了。首先,MySQL需要知道你要做什么,因此需要对SQL语句做解析。
分析器先会做“词法分析”。你输入的是由多个字符串和空格组成的一条SQL语句,MySQL需要识别出里面的字符串分别是什么,代表什么。
MySQL从你输入的"select"这个关键字识别出来,这是一个查询语句。它也要把字符串“T”识别成“表名T”,把字符串“ID”识别成“列ID”。
做完了这些识别以后,就要做“语法分析”。根据词法分析的结果,语法分析器会根据语法规则,判断你输入的这个SQL语句是否满足MySQL语法。
如果你的语句不对,就会收到“YouhaveanerrorinyourSQLsyntax”的错误提醒,比如下面这个语句select少打了开头的字母“s”。
mysql>elect*fromtwhereID=1;
ERROR1064(42000):YouhaveanerrorinyourSQLsyntax;checkthemanualthatcorrespondstoyourMySQLserverversion
fortherightsyntaxtousenear'elect*fromtwhereID=1'atline1
一般语法错误会提示第一个出现错误的位置,所以你要关注的是紧接“usenear”的内容。
分析器先会做“词法分析”。你输入的是由多个字符串和空格组成的一条SQL语句,MySQL需要识别出里面的字符串分别是什么,代表什么。
MySQL从你输入的"select"这个关键字识别出来,这是一个查询语句。它也要把字符串“T”识别成“表名T”,把字符串“ID”识别成“列ID”。
做完了这些识别以后,就要做“语法分析”。根据词法分析的结果,语法分析器会根据语法规则,判断你输入的这个SQL语句是否满足MySQL语法。
如果你的语句不对,就会收到“YouhaveanerrorinyourSQLsyntax”的错误提醒,比如下面这个语句select少打了开头的字母“s”。
mysql>elect*fromtwhereID=1;
ERROR1064(42000):YouhaveanerrorinyourSQLsyntax;checkthemanualthatcorrespondstoyourMySQLserverversion
fortherightsyntaxtousenear'elect*fromtwhereID=1'atline1
一般语法错误会提示第一个出现错误的位置,所以你要关注的是紧接“usenear”的内容。
优化器
经过了分析器,MySQL就知道你要做什么了。在开始执行之前,还要先经过优化器的处理。
优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。比如你执行下面这样的
语句,这个语句是执行两个表的join:
mysql>select*fromt1joint2using(ID)wheret1.c=10andt2.d=20;
既可以先从表t1里面取出c=10的记录的ID值,再根据ID值关联到表t2,再判断t2里面d的值是否等于20。
也可以先从表t2里面取出d=20的记录的ID值,再根据ID值关联到t1,再判断t1里面c的值是否等于10。
这两种执行方法的逻辑结果是一样的,但是执行的效率会有不同,而优化器的作用就是决定选择使用哪一个方案。
优化器阶段完成后,这个语句的执行方案就确定下来了,然后进入执行器阶段。如果你还有一些疑问,比如优化器是怎么选择索引的,有没有可能选择错
等等,没关系,我会在后面的文章中单独展开说明优化器的内容。
优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。比如你执行下面这样的
语句,这个语句是执行两个表的join:
mysql>select*fromt1joint2using(ID)wheret1.c=10andt2.d=20;
既可以先从表t1里面取出c=10的记录的ID值,再根据ID值关联到表t2,再判断t2里面d的值是否等于20。
也可以先从表t2里面取出d=20的记录的ID值,再根据ID值关联到t1,再判断t1里面c的值是否等于10。
这两种执行方法的逻辑结果是一样的,但是执行的效率会有不同,而优化器的作用就是决定选择使用哪一个方案。
优化器阶段完成后,这个语句的执行方案就确定下来了,然后进入执行器阶段。如果你还有一些疑问,比如优化器是怎么选择索引的,有没有可能选择错
等等,没关系,我会在后面的文章中单独展开说明优化器的内容。
执行器
MySQL通过分析器知道了你要做什么,通过优化器知道了该怎么做,于是就进入了执行器阶段,开始执行语句。
开始执行的时候,要先判断一下你对这个表T有没有执行查询的权限,如果没有,就会返回没有权限的错误,如下所示。
select*fromTwhereID=10;
ERROR1142(42000):SELECTcommanddeniedtouser'b'@'localhost'fortable'T'
如果有权限,就打开表继续执行。打开表的时候,执行器就会根据表的引擎定义,去使用这个引擎提供的接口。
比如我们这个例子中的表T中,ID字段没有索引,那么执行器的执行流程是这样的:
调用InnoDB引擎接口取这个表的第一行,判断ID值是不是10,如果不是则跳过,如果是则将这行存在结果集中;
调用引擎接口取“下一行”,重复相同的判断逻辑,直到取到这个表的最后一行。
执行器将上述遍历过程中所有满足条件的行组成的记录集作为结果集返回给客户端。
至此,这个语句就执行完成了。
对于有索引的表,执行的逻辑也差不多。第一次调用的是“取满足条件的第一行”这个接口,之后循环取“满足条件的下一行”这个接口,这些接口都是
引擎中已经定义好的。
你会在数据库的慢查询日志中看到一个rows_examined的字段,表示这个语句执行过程中扫描了多少行。这个值就是在执行器每次调用引擎获取数据
行的时候累加的。
在有些场景下,执行器调用一次,在引擎内部则扫描了多行,因此**引擎扫描行数跟rows_examined并不是完全相同的。**我们后面会专门有一篇文章来
讲存储引擎的内部机制,里面会有详细的说明。
开始执行的时候,要先判断一下你对这个表T有没有执行查询的权限,如果没有,就会返回没有权限的错误,如下所示。
select*fromTwhereID=10;
ERROR1142(42000):SELECTcommanddeniedtouser'b'@'localhost'fortable'T'
如果有权限,就打开表继续执行。打开表的时候,执行器就会根据表的引擎定义,去使用这个引擎提供的接口。
比如我们这个例子中的表T中,ID字段没有索引,那么执行器的执行流程是这样的:
调用InnoDB引擎接口取这个表的第一行,判断ID值是不是10,如果不是则跳过,如果是则将这行存在结果集中;
调用引擎接口取“下一行”,重复相同的判断逻辑,直到取到这个表的最后一行。
执行器将上述遍历过程中所有满足条件的行组成的记录集作为结果集返回给客户端。
至此,这个语句就执行完成了。
对于有索引的表,执行的逻辑也差不多。第一次调用的是“取满足条件的第一行”这个接口,之后循环取“满足条件的下一行”这个接口,这些接口都是
引擎中已经定义好的。
你会在数据库的慢查询日志中看到一个rows_examined的字段,表示这个语句执行过程中扫描了多少行。这个值就是在执行器每次调用引擎获取数据
行的时候累加的。
在有些场景下,执行器调用一次,在引擎内部则扫描了多行,因此**引擎扫描行数跟rows_examined并不是完全相同的。**我们后面会专门有一篇文章来
讲存储引擎的内部机制,里面会有详细的说明。
0 条评论
下一页
为你推荐
查看更多