面试发现经常有些重复的面试问题,自己也应该学会记录下来,最好自己能做成笔记,在下一次面的时候说得有条不紊,深入具体,面试官想必也很开心。以下是我个人总结,请参考:

HashSet底层原理:(问了大几率跟HashMap一起面)

HashMap底层原理:(非常大几率问到)

Hashtable底层原理:(问的少,问了大几率问你跟HashMap的区别)

synchronized底层如何实现?锁优化,怎么优化?

ReentrantLock 底层实现;

ConcurrentHashMap 的工作原理,底层原理(谈到多线程高并发大几率会问它)

JVM调优(JVM层层渐进问时大几率问)

JVM内存管理,JVM的常见的垃圾收集器,GC调优,Minor GC ,Full GC 触发条件(像是必考题)

java内存模型

线程池的工作原理(谈到多线程高并发大几率会问它)

ThreadLocal的底层原理(有时问)

voliate底层原理

NIO底层原理

IOC底层实现原理(Spring IOC ,AOP会问的两个原理,面试官经常会问看过源码吗?所以你有所准备吧)

AOP底层实现原理

MyisAM和innodb的有关索引的疑问(容易混淆,可以问的会深入)

HashSet底层原理:(面试过)


HashSet实现Set接口,由哈希表(实际上是一个HashMap实例)支持。它不保证set 的迭代顺序;特别是它不保证该顺序恒久不变。此类允许使用null元素。

2.    HashSet的实现:

对于HashSet而言,它是基于HashMap实现的,HashSet底层使用HashMap来保存所有元素,因此HashSet 的实现比较简单,相关HashSet的操作,基本上都是直接调用底层HashMap的相关方法来完成, (实际底层会初始化一个空的HashMap,并使用默认初始容量为16和加载因子0.75。)

HashSet的源代码

对于HashSet中保存的对象,请注意正确重写其equals和hashCode方法,以保证放入的对象的唯一性。

插入
当有新值加入时,底层的HashMap会判断Key值是否存在(HashMap细节请移步深入理解HashMap),如果不存在,则插入新值,同时这个插入的细节会依照HashMap插入细节;如果存在就不插入

HashMap底层原理:

1.    HashMap概述:

HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

2.    HashMap的数据结构:

HashMap实际上是一个“数组+链表+红黑树”的数据结构

3.    HashMap的存取实现:

(1.8之前的)

当我们往HashMap中put元素的时候,先根据key的hashCode重新计算hash值,根据hash值得到这个元素在数组中的位置(即下标),如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。

1.8:

put():

  1. 根据key计算得到key.hash = (h = k.hashCode()) ^ (h >>> 16);

  2. 根据key.hash计算得到桶数组的索引index = key.hash & (table.length - 1),这样就找到该key的存放位置了:

① 如果该位置没有数据,用该数据新生成一个节点保存新数据,返回null;

② 如果该位置有数据是一个红黑树,那么执行相应的插入 / 更新操作

③ 如果该位置有数据是一个链表,分两种情况一是该链表没有这个节点,另一个是该链表上有这个节点,注意这里判断的依据是key.hash是否一样: 如果该链表没有这个节点,那么采用尾插法新增节点保存新数据,返回null; 如果该链表已经有这个节点了,那么找到該节点并更新新数据,返回老数据。 注意: HashMap的put会返回key的上一次保存的数据。

get():

计算需获取数据的hash值(计算过程跟put一样),计算存放在数组table中的位置(计算过程跟put一样),然后依次在数组,红黑树,链表中查找(通过equals()判断),最后再判断获取的数据是否为空,若为空返回null否则返回该数据

树化与还原

  • 哈希表的最小树形化容量

  • 当哈希表中的容量大于这个值时(64),表中的桶才能进行树形化

  • 否则桶内元素太多时会扩容,而不是树形化

  • 为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD

  • 一个桶的树化阈值

  • 当桶中元素个数超过这个值时(8),需要使用红黑树节点替换链表节点

  • 这个值必须为 8,要不然频繁转换效率也不高

  • 一个树的链表还原阈值

  • 当扩容时,桶中元素个数小于这个值(6),就会把树形的桶元素 还原(切分)为链表结构

  • 这个值应该比上面那个小,至少为 6,避免频繁转换

条件1. 如果当前桶数组为null或者桶数组的长度 < MIN_TREEIFY_CAPACITY(64),则进行扩容处理(见代码片段2:resize());

条件2. 当不满足条件1的时候则将桶中链表内的元素转换成红黑树!!!稍后再详细讨论红黑树。

扩容机制的实现

  1. 扩容(resize)就是重新计算容量。当向HashMap对象里不停的添加元素,而HashMap对象内部的桶数组无法装载更多的元素时,HashMap对象就需要扩大桶数组的长度,以便能装入更多的元素。

  2. capacity 就是数组的长度/大小,loadFactor 是这个数组填满程度的最大比比例。

  3. size表示当前HashMap中已经储存的Node<key,value>的数量,包括桶数组和链表 / 红黑树中的的Node<key,value>。

  4. threshold表示扩容的临界值,如果size大于这个值,则必需调用resize()方法进行扩容。

  5. 在jdk1.7及以前,threshold = capacity * loadFactor,其中 capacity 为桶数组的长度。 这里需要说明一点,默认负载因子0.75是是对空间和时间(纵向横向)效率的一个平衡选择,建议大家不要修改。 jdk1.8对threshold值进行了改进,通过一系列位移操作算法最后得到一个power of two size的值

什么时候扩容

当向容器添加元素的时候,会判断当前容器的元素个数,如果大于等于阈值---即当前数组的长度乘以加载因子的值的时候,就要自动扩容啦。

扩容必须满足两个条件:

1、 存放新值的时候   当前已有元素的个数  (size) 必须大于等于阈值

2、 存放新值的时候当前存放数据发生hash碰撞(当前key计算的hash值换算出来的数组下标位置已经存在值)

//如果计算的哈希位置有值(及hash冲突),且key值一样,则覆盖原值value,并返回原值value

      if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {

        V oldValue = e.value;

        e.value = value;

        e.recordAccess(this);

        return oldValue;

      }

resize()方法: 该函数有2种使用情况1.初始化哈希表 2.当前数组容量过小,需扩容

过程:

插入键值对时发现容量不足,调用resize()方法方法,

1.首先进行异常情况的判断,如是否需要初始化,二是若当前容量》最大值则不扩容,

2.然后根据新容量(是就容量的2倍)新建数组,将旧数组上的数据(键值对)转移到新的数组中,这里包括:(遍历旧数组的每个元素,重新计算每个数据在数组中的存放位置(原位置或者原位置+旧容量),将旧数组上的每个数据逐个转移到新数组中,这里采用的是尾插法。)

3.新数组table引用到HashMap的table属性上

4.最后重新设置扩容阙值,此时哈希表table=扩容后(2倍)&转移了旧数据的新table

synchronized底层如何实现?锁优化,怎么优化?

synchronized 是 Java 内建的同步机制,所以也有人称其为 Intrinsic Locking,它提供了互斥的语义和可见性,当一个线程已经获取当前锁时,其他试图获取的线程只能等待或者阻塞在那里。

原理:

synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性

底层实现:

同步代码块是使用monitorenter和monitorexit指令实现的, ,当且一个monitor被持有之后,他将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁;

同步方法(在这看不出来需要看JVM底层实现)依靠的是方法修饰符上的ACC_SYNCHRONIZED实现。  synchronized方法则会被翻译成普通的方法调用和返回指令如:invokevirtual、areturn指令,在VM字节码层面并没有任何特别的指令来实现被synchronized修饰的方法,而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示 Klass 做为锁对象。

Java对象头和monitor是实现synchronized的基础!

synchronized存放的位置:

synchronized用的锁是存在Java对象头里的。

其中, Java对象头包括:

Mark Word(标记字段): 用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。它是实现轻量级锁和偏向锁的关键

Klass Pointer(类型指针): 是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例

monitor:  可以把它理解为一个同步工具, 它通常被描述为一个对象。 是线程私有的数据结构

锁优化,怎么优化?

jdk1.6对锁的实现引入了大量的优化。 锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。 注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。 重量级锁降级发生于STW阶段,降级对象为仅仅能被VMThread访问而没有其他JavaThread访问的对象。( HotSpot JVM/JRockit JVM是支持锁降级的)

偏斜锁:

当没有竞争出现时,默认会使用偏斜锁。JVM 会利用 CAS 操作(compare and swap),在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。

自旋锁:

自旋锁 for(;;)结合cas确保线程获取取锁

就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。怎么等待呢?执行一段无意义的循环即可(自旋)。

轻量级锁:

引入偏向锁主要目的是:为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径。 当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁

重量级锁:

重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。

ReentrantLock 底层实现

AQS原理:

AQS和Condition各自维护了不同的队列,在使用lock和condition的时候,其实就是两个队列的互相移动。如果我们想自定义一个同步器,可以实现AQS。它提供了获取共享锁和互斥锁的方式,都是基于对state操作而言的。

概念+实现:

ReentrantLock实现了Lock接口,是AQS( 一个用来构建锁和同步工具的框架, AQS没有 锁之 类的概念)的一种。加锁和解锁都需要显式写出,注意一定要在适当时候unlock。ReentranLock这个是可重入的。其实要弄明白它为啥可重入的呢,咋实现的呢。其实它内部自定义了同步器Sync,这个又实现了AQS,同时又实现了AOS,而后者就提供了一种互斥锁持有的方式。其实就是每次获取锁的时候,看下当前维护的那个线程和当前请求的线程是否一样,一样就可重入了。

和synhronized相比:

synchronized相比,ReentrantLock用起来会复杂一些。在基本的加锁和解锁上,两者是一样的,所以无特殊情况下,推荐使用synchronized。ReentrantLock的优势在于它更灵活、更强大,增加了轮训、超时、中断等高级功能。

可重入锁。可重入锁是指同一个线程可以多次获取同一把锁。ReentrantLock和synchronized都是可重入锁。

可中断锁。可中断锁是指线程尝试获取锁的过程中,是否可以响应中断。synchronized是不可中断锁,而ReentrantLock则z,dz提供了中断功能。

公平锁与非公平锁。公平锁是指多个线程同时尝试获取同一把锁时,获取锁的顺序按照线程达到的顺序,而非公平锁则允许线程“插队”。synchronized是非公平锁,而ReentrantLock的默认实现是非公平锁,但是也可以设置为公平锁。

lock()和unlock()是怎么实现的呢?

由lock()和unlock的源码可以看到,它们只是分别调用了sync对象的lock()和release(1)方法。而  Sync是ReentrantLock的内部类, 其扩展了AbstractQueuedSynchronizer。

lock():

final void lock() {

if (compareAndSetState(0, 1))

setExclusiveOwnerThread(Thread.currentThread());

else

acquire(1);

}

首先用一个CAS操作,判断state是否是0(表示当前锁未被占用),如果是0则把它置为1,并且设置当前线程为该锁的独占线程,表示获取锁成功。当多个线程同时尝试占用同一个锁时,CAS操作只能保证一个线程操作成功,剩下的只能乖乖的去排队啦。( “非公平”即体现在这里)。

设置state失败,走到了else里面。我们往下看acquire。

  1. 第一步。尝试去获取锁。如果尝试获取锁成功,方法直接返回。

2. 第二步,入队。( 自旋+CAS组合来实现非阻塞的原子操作)

3. 第三步,挂起。 让已经入队的线程尝试获取锁,若失败则会被挂起

public final void acquire(int arg) {

if (!tryAcquire(arg) &&

acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

selfInterrupt();

}

unlock():
流程大致为先尝试释放锁,若释放成功,那么查看头结点的状态是否为SIGNAL,如果是则唤醒头结点的下个节点关联的线程,

如果释放失败那么返回false表示解锁失败。这里我们也发现了,每次都只唤起头结点的下一个节点关联的线程。

public void unlock() {

sync.release(1);

}

public final boolean release(int arg) {

if (tryRelease(arg)) {

Node h = head;

if (h != null && h.waitStatus != 0)

unparkSuccessor(h);

return true;

}

return false;

}

ConcurrentHashMap 的工作原理

概念:

ConcurrentHashMap的目标是实现支持高并发、高吞吐量的线程安全的HashMap。

1.8之前:

数据结构:

ConcurrentHashMap是由Segment数组结构和 多个HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap相似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。

put和get的时候,都是现根据key.hashCode()算出放到哪个Segment中: ConcurrentHashMap中默认是把segments初始化为长度为16的数组

面试

问:数据库中最常见的慢查询优化方式是什么?答:加索引。问:为什么加索引能优化慢查询?答1:...不知道答2:因为索引其实就是一种优化查询的数据结构,比如Mysql中的索引是用B+树实现的,而B+树就是一种数据结构,可以优化查询速度,可以利用索引快速查找数据,所以能优化查询。问:你知道哪些数据结构可以提高查询速度?(听到这个问题就感觉此处有坑...)答:哈希表、完全平衡二叉树、B树、B+树等等。问:那这些数据结构既然都能优化查询速度,那Mysql种为何选择使用B+树?答:...不知道

提问

SHOW INDEX FROM employees.titles;

有一个titles表,主键由emp_no,title,from_date三个字段组成。那么以下几个语句会用到索引吗:

  1. select * from employees.titles where emp_no = 1
  2. select * from employees.titles where title = '1'
  3. select * from employees.titles where emp_no = '1' and title = 1;
  4. select * from employees.titles where title = '1' and emp_no = 1;

索引(Index)

到底什么是索引(Index)?大学老师是这么定义的:索引就像书的目录Mysql官网是这么定义的:Indexes are used to find rows with specific column values quickly我是这么定义的:索引是一种优化查询的数据结构

为什么哈希表、完全平衡二叉树、B树、B+树都可以优化查询,为何Mysql独独喜欢B+树?

哈希表是什么?

哈希表(Hash table,也叫散列表),是根据键值(Key value)而直接进行访问的数据结构。也就是说,它通过把键值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。哈希表的做法其实很简单,就是把Key通过一个固定的算法函数既所谓的哈希函数转换成一个整型数字,然后就将该数字对数组长度进行取余,取余结果就当作数组的下标,将value存储在以该数字为下标的数组空间里。而当使用哈希表进行查询的时候,就是再次使用哈希函数将key转换为对应的数组下标,并定位到该空间获取value,如此一来,就可以充分利用到数组的定位性能进行数据定位。

哈希表的特点是什么?

假如有这么一张表(表名:sanguo):

现在对name字段建立哈希索引:注意字段值所对应的数组下标是哈希算法随机算出来的,所以可能出现哈希冲突。那么对于这样一个索引结构,现在来执行下面的sql语句:select * from sanguo where name = '周瑜'可以直接对‘周瑜’按哈希算法算出来一个数组下标,然后可以直接从数据中取出数据并拿到锁对应那一行数据的地址,进而查询那一行数据。那么如果现在执行下面的sql语句:select * from sanguo where name > '周瑜'则无能为力,因为哈希表的特点就是可以快速的精确查询,但是不支持范围查询

如果用完全平衡二叉树呢?

还是上面的表数据用完全平衡二叉树表示如下图(为了简单,数据对应的地址就不画在图中了。):图中的每一个节点实际上应该有四部分:

  1. 左指针,指向左子树
  2. 键值
  3. 键值所对应的数据的存储地址
  4. 右指针,指向右子树

另外需要提醒的是,二叉树是有顺序的,简单的说就是“左边的小于右边的”假如我们现在来查找‘周瑜’,需要找2次(第一次曹操,第二次周瑜),比哈希表要多一次。而且由于完全平衡二叉树是有序的,所以也是支持范围查找的。

如果用B树呢?

还是上面的表数据用B树表示如下图(为了简单,数据对应的地址就不画在图中了。):我们可以发现同样的元素,B树的表示要比完全平衡二叉树要“矮”,原因在于B树中的一个节点可以存储多个元素。

如果用B+树呢?

还是上面的表数据用B+树表示如下图(为了简单,数据对应的地址就不画在图中了。):我们可以发现同样的元素,B+树的表示要比B树要“胖”,原因在于B+树中的非叶子节点会冗余一份在叶子节点中,并且叶子节点之间用指针相连。

那么B+树到底有什么优势呢?

这里我们用“反证法”,假如我们现在就用完全平衡二叉树作为索引的数据结构,我们来看一下有什么不妥的地方。实际上,索引也是很“大”的,因为索引也是存储元素的,我们的一个表的数据行数越多,那么对应的索引文件其实也是会很大的,实际上也是需要存储在磁盘中的,而不能全部都放在内存中,所以我们在考虑选用哪种数据结构时,我们可以换一个角度思考,哪个数据结构更适合从磁盘中读取数据,或者哪个数据结构能够提高磁盘的IO效率。回头看一下完全平衡二叉树,当我们需要查询“张飞”时,需要以下步骤

  1. 从磁盘中取出“曹操”到内存,CPU从内存取出数据进行笔记,“张飞”<“曹操”,取左子树(产生了一次磁盘IO)
  2. 从磁盘中取出“周瑜”到内存,CPU从内存取出数据进行笔记,“张飞”>“周瑜”,取右子树(产生了一次磁盘IO)
  3. 从磁盘中取出“孙权”到内存,CPU从内存取出数据进行笔记,“张飞”>“孙权”,取右子树(产生了一次磁盘IO)
  4. 从磁盘中取出“黄忠”到内存,CPU从内存取出数据进行笔记,“张飞”=“张飞”,找到结果(产生了一次磁盘IO)

同理,回头看一下B树,我们发现只发送三次磁盘IO就可以找到“张飞”了,这就是B树的优点:一个节点可以存储多个元素,相对于完全平衡二叉树所以整棵树的高度就降低了,磁盘IO效率提高了。而,B+树是B树的升级版,只是把非叶子节点冗余一下,这么做的好处是为了提高范围查找的效率

所以,到这里,我们可以总结出来,Mysql选用B+树这种数据结构作为索引,可以提高查询索引时的磁盘IO效率,并且可以提高范围查询的效率,并且B+树里的元素也是有序的。

那么,一个B+树的节点中到底存多少个元素合适呢?

这里有必要先来了解一下磁盘IO的原理。

磁盘I/O的本质

磁盘分类

机械硬盘

固态硬盘

从上面的原理我们也能知道,固态硬盘比机械硬盘快的最根本最简单的原因就是:固态硬盘使用的电路进行读写,而机械硬盘使用的机械运动其实不管是机械硬盘还是固态硬盘都是存储介质,真正控制读写的是操作系统

机械硬盘存储原理

磁盘立体结构图

一个磁盘由大小相同且同轴的圆形盘片组成,磁盘可以转动(各个磁盘必须同步转动)。在磁盘的一侧有磁头支架,磁头支架固定了一组磁头,每个磁头负责存取一个磁盘的内容。磁头不能转动,但是可以沿磁盘半径方向运动(实际是斜切向运动),每个磁头同一时刻也必须是同轴的,即从正上方向下看,所有磁头任何时候都是重叠的(不过目前已经有多磁头独立技术,可不受此限制)。

磁盘片结构图

盘片被划分成一系列同心环,圆心是盘片中心,每个同心环叫做一个磁道,所有半径相同的磁道组成一个柱面。磁道被沿半径线划分成一个个小的段,每个段叫做一个扇区,每个扇区是磁盘的最小存储单元,大小一般为521字节。

磁盘读取数据逻辑

当需要从磁盘读取数据时,系统会将数据逻辑地址传给磁盘,磁盘的控制电路按照寻址逻辑将逻辑地址翻译成物理地址,即确定要读的数据在哪个磁道,哪个扇区。为了读取这个扇区的数据,需要将磁头放到这个扇区上方,为了实现这一点,磁头需要移动对准相应磁道,这个过程叫做寻道,所耗费时间叫做寻道时间,然后磁盘旋转将目标扇区旋转到磁头下,这个过程耗费的时间叫做旋转时间。

固态硬盘存储原理

固态硬盘(Solid State Drives),用固态电子存储芯片阵列而制成的硬盘,由控制单元和存储单元(FLASH芯片、DRAM芯片)组成。固态硬盘在接口的规范和定义、功能及使用方法上与普通硬盘的完全相同,在产品外形和尺寸上也完全与普通硬盘一致。

控制单元

每个SSD都有一个控制器(controller)将存储单元连接到电脑,主控器可以通过若干个通道(channel)并行操作多块存储单元

存储单元

一个Flash Page由两个或者多个Die(又称chips组成),这些Dies可以共享I/0数据总线和一些控制信号线。一个Die又可以分为多个Plane,而每个Plane又包含多个多个Block,每个Block又分为多个Page。以Samsung 4GB Flash为例,一个4GB的Flash Page由两个2GB的Die组成,共享8位I/0数据总线和一些控制信号线。每个Die由4个Plane组成,每个Plane包含2048个Block,每个Block又包含64个4KB大小的Page

访问SSD的原理

Host是通过LBA(Logical BlockAddress,逻辑地址块)访问SSD的,每个LBA代表着一个Sector(一般为512B大小),操作系统一般以4K为单位访问SSD,我们把Host访问SSD的基本单元叫用户页(Host Page)。而在SSD内部,SSD主控与Flash之间是Flash Page为基本单元访问Flash的,我们称Flash Page为物理页(Physical Page)。Host每写入一个Host Page, SSD主控会找一个Physical Page把Host数据写入,SSD内部同时记录了这样一条映射(Map)。有了这样一个映射关系后,下次Host需要读某个Host Page 时,SSD就知道从Flash的哪个位置把数据读取上来。

局部性原理与磁盘预读

计算机科学中著名的局部性原理:当一个数据被用到时,其附近的数据也通常会马上被使用。所以操作系统为了提高效率,读取数据时往往不是严格按需读取,而是每次都会预读,即使只需要一个字节,操作系统也会从这个位置开始,顺序向后读取一定长度的数据放入内存。这里的一定长度叫做,也就是操作系统操作磁盘时的基本单位。一般操作系统中一页的大小是4Kb。

总结

所以,回到我们的问题,B+树中一个节点到底存多少个元素合适?,其实也可以换个角度来思考B+树中一个节点到底多大合适?答案是:B+树中一个节点为一页或页的倍数最为合适。因为如果一个节点的大小小于1页,那么读取这个节点的时候其实也会读出1页,造成资源的浪费;如果一个节点的大小大于1页,比如1.2页,那么读取这个节点的时候会读出2页,也会造成资源的浪费;所以为了不造成浪费,所以最后把一个节点的大小控制在1页、2页、3页、4页等倍数页大小最为合适。

那么,Mysql中B+树的一个节点大小为多大呢?

这个问题的答案是“1页”,这里说的“页”是Mysql自定义的单位(其实和操作系统类似),Mysql的Innodb引擎中一页的默认大小是16k(如果操作系统中一页大小是4k,那么Mysql中1页=操作系统中4页),可以使用命令SHOW GLOBAL STATUS like 'Innodb_page_size';查看。并且还可以告诉你的是,一个节点为1页就够了。

为什么一个节点为1页(16k)就够了?

解决这个问题,我们先来看一下Mysql中利用B+树的具体实现。

Mysql中MyISAM和innodb使用B+树

通常我们认为B+树的非叶子节点不存储数据,只有叶子节点才存储数据;而B树的非叶子和叶子节点都会存储数据,会导致非叶子节点存储的索引值会更少,树的高度相对会比B+树高,平均的I/O效率会比较低,所以使用B+树作为索引的数据结构,再加上B+树的叶子节点之间会有指针相连,也方便进行范围查找。上图的data区域两个存储引擎会有不同。

MyISAM中的B+树

MYISAM中叶子节点的数据区域存储的是数据记录的地址

主键索引

辅助索引

总结

MyISAM存储引擎在使用索引查询数据时,会先根据索引查找到数据地址,再根据地址查询到具体的数据。并且主键索引和辅助索引没有太多区别。

InnoDB中的B+树

InnoDB中主键索引的叶子节点的数据区域存储的是数据记录,辅助索引存储的是主键值

主键索引

辅助索引

总结

Innodb中的主键索引和实际数据时绑定在一起的,也就是说Innodb的一个表一定要有主键索引,如果一个表没有手动建立主键索引,Innodb会查看有没有唯一索引,如果有则选用唯一索引作为主键索引,如果连唯一索引也没有,则会默认建立一个隐藏的主键索引(用户不可见)。另外,Innodb的主键索引要比MyISAM的主键索引查询效率要高(少一次磁盘IO),并且比辅助索引也要高很多。所以,我们在使用Innodb作为存储引擎时,我们最好:

  1. 手动建立主键索引
  2. 尽量利用主键索引查询

回到我们的问题:为什么一个节点为1页(16k)就够了?

对着上面Mysql中Innodb中对B+树的实际应用(主要看主键索引),我们可以发现,B+树中的一个节点存储的内容是:

  • 非叶子节点:主键+指针
  • 叶子节点:数据

那么,假设我们一行数据大小为1K,那么一页就能存16条数据,也就是一个叶子节点能存16条数据;再看非叶子节点,假设主键ID为bigint类型,那么长度为8B,指针大小在Innodb源码中为6B,一共就是14B,那么一页里就可以存储16K/14=1170个(主键+指针),那么一颗高度为2的B+树能存储的数据为:1170*16=18720条,一颗高度为3的B+树能存储的数据为:1170*1170*16=21902400(千万级条)。所以在InnoDB中B+树高度一般为1-3层,它就能满足千万级的数据存储。在查找数据时一次页的查找代表一次IO,所以通过主键索引查询通常只需要1-3次IO操作即可查找到数据。所以也就回答了我们的问题,1页=16k这么设置是比较合适的,是适用大多数的企业的,当然这个值是可以修改的,所以也能根据业务的时间情况进行调整。

最左前缀原则

我们模拟数据建立一个联合索引select *, concat(right(emp_no,1), "-", right(title,1), "-", right(from_date,2)) from employees.titles limit 10;

那么对应的B+树为

我们判断一个查询条件能不能用到索引,我们要分析这个查询条件能不能利用某个索引缩小查询范围对于select * from employees.titles where emp_no = 1是能用到索引的,因为它能利用上面的索引所有查询范围,首先和第一个节点“4-r-01”比较,1<4,所以可以直接确定结果在左子树,同理,依次按顺序进行比较,逐步可以缩小查询范围。对于select * from employees.titles where title = '1'是不能用到索引的,因为它不能用到上面的所以,和第一节点进行比较时,没有emp_no这个字段的值,不能确定到底该去左子树还是右子树继续进行查询。对于select * from employees.titles where title = '1' and emp_no = 1是能用到索引,按照我们的上面的分析,先用title='1'这个条件和第一个节点进行比较,是没有结果的,但是mysql会对这个sql进行优化,优化之后会将emp_no=1这个条件放到第一位,从而可以利用索引。


只有学习底层原理才能变得更强

1. InnoDB页结构
InnoDB是用于将表中的数据存储到磁盘上的一款存储引擎。

更大维度了解MySQL内部组成结构

MySQL其实就是一款软件,可以分为客户端、服务器端以及引擎层。服务器端分为了一下几部分:

服务端组成结构

连接器

连接器用于客户端和服务端进行连接、用户权限校验以及拦截。

缓存

客户端连接服务端成功后,即可去查询数据。但是MySQL中首先会去查询缓存,如果缓存中查询出了结果,则直接返回缓存的查询结果。否则往下执行。但是缓存在MySQL中比较的鸡肋,因为其缓存命中率非常低,
对于一些静态表以及读多不怎么写的表才适合缓存。在MYSQL8.0移除了缓存功能。

词法分析器

词法分析器用于去分析SQL语句是否合法以及去识别SQL语句的关键字等。

优化器

优化器决定了在表里面有多个索引的时候决定使用哪一个;还有在范围查找的时候是否要使用索引还是不去回表直接全表扫描更快都是优化器所做的。

执行器

调用引擎结构,获取查询结果集。

引擎层

对于MySQL引擎包含了InnoDB,MyISAM,Memory等引擎,而MySQL默认使用的是InnoDB引擎。

具体更深入的就不去了解了。下面用一张图来展示MySQL内部组成结构。

回到InnoDB页的简介,来看下InnoDB页结构以及原理。

在MySQL底层,需要通过InnoDB引擎将数据从磁盘中读取到内存中,或者是将内存中的数据持久化到磁盘中,那么在InnoDB中,规定是:将数据分为若干个页,然后以页为单位作为磁盘和内存交互的单位。在InnoDB中一个页单位大小一般为16KB。

需要额外提出的地方:

一个页一般是16KB,当记录中的数据太多,当前页放不下的时候,会把多余的数据存储到其他页中,这种现象称为行溢出。

2. InnoDB行格式
InnoDB将以16KB大小的页来作为内存和磁盘的交互基本单位,那么MySQL中的数据存储到磁盘中,又是以什么格式存储呢?在InnoDB中,设计者设计出了MySQL数据存储在磁盘的格式,也被称为:行格式

在InnoDB中有4中行格式:

Compact行格式
Redundant行格式
Dynamic行格式
Compressed行格式
每个行格式都分为两部分:记录的额外信息、记录的真实信息。

记录的额外信息

在Compact行格式中,记录的额外信息包括了:变长字段长度列表、NULL值列表、记录头信息等信息,然后其他行格式也是差不多的,有些许区别,可通过查资料来确认。

记录的真实信息

这个部分就是存储MySQL数据的真实的地方了。

3. InnoDB数据页结构
都知道了InnoDB中是以页作为内存和磁盘交互的基本单位,一个页的大小一般为16KB。在InnoDB中为了不同的目的而设计了许多不同类型的页,下面简单列出了几项:

存放表头空间头部信息的页
存放Insert Buffer信息的页
存放INODE信息的页
存放undo日志信息的页
我们详细介绍下存放数据的页,官方称作为:索引(INDEX)页。

下面看下索引页,也成为数据页(下文统一称呼为数据页)。

在InnoDB数据页结构中,被划分为了7个部分

详细看MySQL 是怎样运行的:从根儿上理解 MySQL

最重要的就是:

用户记录(User Records)
页面目录(Page Directory)
页面头部(File Header)
空闲空间(Free Space)
通常,MySQL真实数据是存储在用户记录部分的。但是在一开始生成页的时候,是不存在用户记录部分的,每当插入一条记录时,都会从Free Space分配部分空间给User Records,待Free Space空间分配完了,也就意味着
当前页使用完了,数据以及填充满了,如果还有新的记录插入的话,就需要去申请新的页了,这就是页分裂过程。

在InnoDB页中的用户记录部分

在用户记录部分中,当存有用户数据记录时,都会规定两个伪记录:最小记录(Infimum)和最大记录(supremum),而其他记录都存在最大记录和最小记录中间,通过一个next_record来连接起来的一个:单链表。

不管怎么对页中的记录做CRUD,InnoDB都会维护一条记录的单链表,链表中的各个节点是按照主键值小到大的顺序连接起来。注意是从主键开始从大到小!!!

总结一下:InnoDB用户记录中,每个记录的头信息都会有一个next_record属性来使得页中所有记录串联成一个单链表。

InnoDB的页目录(Page Directory)

在InnoDB中,会把页中的记录划分为若干个组,每个组中最后一条记录的偏移量作为一个槽,然后把这个槽存放在页目录中。因此InnoDB在页中通过查询页目录来查询数据是非常快的,具体可以分为两步:

通过二分法确定该记录所在的槽
通过记录的next_record属性遍历该槽所在的组中的各个记录
需要额外注意的!! 页目录是为主键列创建的!!非主键列是不存在页目录的!!

InnoDB的页面头部(File Header)

在InnoDB中,每个页面头部都会存放上一页和下一页的编号用于连接上一个数据页(索引页)和下一个数据页(索引页)。

对于InnoDB数据页,总结一下:

在InnoDB中,各个数据页可以组成一个双向链表,每个数据页中的记录头部有一个next_record来根据主键值从小到大的顺序组成一个单向链表。每个数据页都会为存储到它边上的记录生成一个"页目录",然后通过主键查找某条记录的时候,
可以在页目录中使用二分法快速定位到对应的槽,然后再遍历槽对应分组中的记录即可快速找到指定的记录。

4. B+树索引
学过MySQL的开发者都知道,根据索引来查找数据查询效率会非常高,查询速度也会非常快。下面来看看没有走索引的条件,底层是个什么原理。

首先对于查询可以分为两种类型:以主键为搜索条件、以其他列作为搜索条件

以主键作为搜索条件

对于以主键来搜索条件,在单数据也中,首先是可以在页目录中通过二分查找快速定位到对应的槽,然后遍历槽中分组中的用户记录即可快速查找到记录。

以其他列作为搜索条件

对于其他列就没那么幸运了,由于非主键列是没有页目录这一项的,所以就无法通过二分法来快速定位到具体的槽。只能从页中的最小记录开始遍历页中的单链表来查询每条记录。

然而!!MySQL中用户记录是非常多的,真实线上的数据页也不可能为一个,而是非常多的数据页,所以对于按主键查找或者是非主键查找数据,都无法绕开要先确定数据在哪个页记录上。在多数据页查找的过程可以分为如下几步:

在多个数据页中确定记录所在的页
根据是主键还是非主键的条件来查找对应的记录
在没有索引的情况下,无论条件是根据主键列还是非主键列来查找数据,都需要从头开始遍历由双向链表连接的数据页,然后在进入数据页中去具体的记录(如果是主键列,则可以对通过二分法来确定记录在哪个槽范围,然后再到槽中去遍历记录组的记录),然而非主键列就更惨了,上层遍历了双向链表的数据页,下层还有继续去遍历单链表连接的记录!!!实在是苦逼!!

可想而知,没有索引时,在数据量巨大的情况下,查找效率是多么的低下。

4.1 InnoDB中B+树索引原理
因此在InnoDB中,提供了索引用于快速查询数据。那么在InnoDB底层中索引是如何运行的呢?什么是索引呢?

在InnoDB中,会将B+树叶子节点中一个页记录链表头对应的最小主键值记录在一个目录项中,一条目录项包含了最小主键值以及该主键值对应的页数,而在B+树叶子节点中会存有多个数据页,并且是作为双向链表连接起来的数据页。

当目录项一多,则InnoDB会为其分配一个用于存储目录项的页,在前面已经说到,在InnoDB中页是MySQL内存和磁盘交互的基本单位,且页分为了很多类型,而在InnoDB中通过record_type来区分页类型。

0:普通的用户记录
1:目录项记录
2:最小记录
3:最大记录
所以用于存储数据页的页record_type类型为0,而用于存储目录项的页类型为1。这点需要大家记住!

先看下图目录项和叶子节点的数据页的关系:

而将多个目录项存放在一个页中时,B+树结构如下图所示:

并且,如果存放由目录项记录的页也变得过多时,可以再为存有目录项的页们建一个目录, 这个目录其实和目录项一样存放由两个关键数据:

存放着目录项页中最小的主键值
对应的目录项页的页数
说起来有点抽象,看下下图就能理解了:

小结:就是这样,在B+树叶子节点中存放着众多的数据页,而叶子节点中数据页与数据页之间是通过双向链表来连通的。数据页中的用户真实记录之间是通过单链表来关联起来的。
InnoDB为了加快数据查询而建立了一个存储目录项的页,而又继续为存储目录项的页建立了又一个目录项页,依次建立起了InnoDB的索引机制!而B+树就是这样一个层数低,但又节点以及叶子节点
众多的一棵树。

4.2 聚镞索引
对于聚镞索引有下列两个特点:

在页和记录中,是通过记录的主键值来进行排序的,这包括三个方面的含义:
页内的记录是按照主键的大小顺序排成了一个单向链表
各个存放用户记录的页是根据用户记录中的主键值大小排序成一个双向链表
存放目录项记录的页分为了不同层此,在同一层次中不同目录项页之间也是根据目录项中的主键值大小顺序排列成的一个双向链表
B+树的叶子节点存储的是完整的用户记录
所谓完整的用户记录,就是指这个记录中存储了所有列的值(包括隐藏列)。
我们把具有这两种特性的B+树称为聚簇索引,所有完整的用户记录都存放在这个聚簇索引的叶子节点处。这种聚簇索引并不需要我们在MySQL语句中显式的使用INDEX语句去创建(后边会介绍索引相关的语句),InnoDB存储引擎会自动的为我们创建聚簇索引。另外有趣的一点是,在InnoDB存储引擎中,聚簇索引就是数据的存储方式(所有的用户记录都存储在了叶子节点),也就是所谓的索引即数据,数据即索引。

4.3 如何正确使用索引呢?
深入InnoDB索引底层原理后,也要浅出总结下B+树索引适用于哪些情况:

全值匹配:与索引中的所有列进行匹配,也就是条件字段与联合索引的字段个数与顺序相同;
匹配最左前缀:只使用联合索引的前几个字段;
匹配列前缀:比如like 'xx%'可以走索引;
匹配范围值:范围查询,比如>,like等;
匹配某一列并范围匹配另外一列:精确查找+范围查找;
只访问索引查询:索引覆盖,select的字段为主键;
那哪些情况是不走索引的呢?

那么SQL语句什么情况下是不走索引的呢?

SQL语句中包含了or的不走索引;
SQL语句中模糊查询like使用了前缀’%xx’不走索引;
SQL语句中包含了运算或者函数;
SQL语句中列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不使用索引;
4.4 浅谈InnoDB的二级索引(辅助索引)以及联合索引等概念
在聚镞索引中,叶子节点是存储这用户记录的数据页,用户记录包含了主键,还有其他列值,但是数据页内是按照主键大小顺序排列成一个单链表。在InnoDB中,如果需要将其他非主键列定义为索引,则会定义成辅助索引。

辅助索引定义如下:

辅助索引结构和聚镞索引类似,都是包含着目录项页以及数据页,其中叶子节点就是数据页;
相比于聚镞索引,辅助索引数据页中的用户记录只存储两个值,一个是指定作为索引的列值,另外一个是主键值,数据页中数据是按照这个指定的列值来排列而成的单向链表,同样的数据页之间也是按照数据列值的顺序排列成一个双向链表;
对于查询走辅助索引的情况,是需要回表,这个回表指的是虽然查询走的是辅助索引,但是查询到指定辅助索引数据页记录中的主键值,然后再按照这个主键值去走一遍聚镞索引;
对于辅助索引的回表操作,是需要消耗性能的,所以查询走辅助索引最终还是要走聚镞索引的。

联合索引

对于联合索引,比如主键是:a,其他列:b、c

此时需要建立联合索引:b、c

其实对于联合索引,和辅助索引底层是相似的,即联合索引数据页中存储的是a、b、c散列数据。

4.5 InnoDB中的页分裂
对于InnoDB底层中,一个页大小为16KB,一个页总有用完的时候,当一个页中内存使用完时,即会发生页分裂操作,即将一个页中装不完的用户数据分到另外一个新开的数据页中,并且下一个数据页中用户记录的主键值必须大于上一个页中用户记录的主键值。这个过程我们也可以称为页分裂。(注意和文章开头指出的行溢出的区别)

而对于页分裂操作,也是需要消耗性能的,所以在InnoDB的聚镞索引中,建议让主键拥有AUTO_INCREMENT属性,这样可以避免频繁的页分裂操作。

面试 innodb底层原理相关推荐

  1. mysql引擎层存储层_MySQL存储底层技术:InnoDB底层原理解读

    原标题:MySQL存储底层技术:InnoDB底层原理解读 存储引擎 很多文章都是直接开始介绍有哪些存储引擎,并没有去介绍存储引擎本身.那么究竟什么是存储引擎?不知道大家有没有想过,MySQL是如何存储 ...

  2. MySQL innoDB底层基础原理总结

    MySQL innoDB底层基础原理 前言:由于正在准备之后的实习面试,故总结了一部分MYSQL innoDB基础的问题,回答全为自己组织的语言,若有错各位大佬可及时指出,大家共同进步,谢谢. 1.i ...

  3. 嘿嘿,我就知道面试官接下来要问我 ConcurrentHashMap 底层原理了,看我怎么秀他...

    来自:烟雨星空 前言 上篇文章介绍了 HashMap 源码后,在博客平台广受好评,让本来己经不打算更新这个系列的我,仿佛被打了一顿鸡血.真的,被读者认可的感觉,就是这么奇妙. 原文:面试官再问你 Ha ...

  4. 面试官再问你 HashMap 底层原理,就把这篇文章甩给他看

    来自:烟雨星空 前言 HashMap 源码和底层原理在现在面试中是必问的.因此,我们非常有必要搞清楚它的底层实现和思想,才能在面试中对答如流,跟面试官大战三百回合.文章较长,介绍了很多原理性的问题,希 ...

  5. 关于Spring底层原理面试的那些问题,你是不是真的懂Spring?

    转载自  关于Spring底层原理面试的那些问题,你是不是真的懂Spring? 1.什么是 Spring 框架?Spring 框架有哪些主要模块? Spring 框架是一个为 Java 应用程序的开发 ...

  6. 面试必备:synchronized的底层原理?

    最近更新的XX必备系列适合直接背答案,不深究,不喜勿喷. 你能说简单说一下synchronize吗? 可别真简单一句话就说完了呀~ 参考回答: synchronize是java中的关键字,可以用来修饰 ...

  7. cgblib 代理接口原理_Java开发者你还不知道?告诉你Dubbo 的底层原理,面试不再怕...

    前言 平常我们在构建分布式系统的时候,一般都是基于 Dubbo 技术栈或者是SpringCloud 技术栈来做.早期其实最先比较流行的是Dubbo,我记得我们当时有个部分的老大就是用的是Dubbo 来 ...

  8. 查询已有链表的hashmap_面试官再问你 HashMap 底层原理,就把这篇文章甩给他看...

    前言 HashMap 源码和底层原理在现在面试中是必问的.因此,我们非常有必要搞清楚它的底层实现和思想,才能在面试中对答如流,跟面试官大战三百回合.文章较长,介绍了很多原理性的问题,希望对你有所帮助~ ...

  9. 面试突然问Java多线程底层原理,我哭了!

    兄弟们,不要踩坑啊,我原本打算在金九银十之前换份工作,结果出去第一面就被干懵了! 面试官上来就问我了解不了解多线程,我感觉我还可以,我就和他说:必须的! 不过,他直接问了多线程的底层原理,这我都是一知 ...

  10. 拜托,面试请不要再问我 Spring Cloud Alibaba 底层原理

    大家好,今天给大家介绍一个非常热门的技术,同时也是面试的时候面试官特别喜欢问的一个话题,那就是SpringCloudAlibaba的底层原理. 现在大家都知道,SpringCloudAlibaba 风 ...

最新文章

  1. 软件本地化,软件本地化公司
  2. java html提取_2020年全新Java学习路线,含配套资料,更易上手 - 打不过就跑吧
  3. ubuntu14.04下安装qt4.8.6 +qt creator
  4. Pyalgotrade量化交易回测框架
  5. dockerfile arg_解读三组容易混淆的 Dockerfile 指令
  6. Java中对象的直接赋值、浅拷贝及深拷贝的理解和应用场景及其实现方式
  7. ZYNQ研究----(2)基于开发板制作串口测试程序
  8. 威金Worm.Viking病毒分析及处理
  9. 网络分析仪自动化测试软件,高效矢量网络分析仪自动测试方法
  10. 并发请求:统计数据收集模式
  11. linux上centos镜像磁盘,VirtualBox中配置linuxCentOS的本地磁盘镜像iso作为其软件源
  12. IC EMC(集成电路电磁兼容)测试标准介绍
  13. ACP相比AWS哪个更具有优势
  14. SSO(single sign on)模式 单点登录
  15. 推荐系统引擎——模型(1)
  16. matlab打开笔记本摄像头_如何利用MATLAB实现摄像头视频获取和保存
  17. 网络空间测绘国内外发展及现状
  18. 史上最强《Java 开发手册》泰山版王者归来!
  19. 母亲节html页面,css3母亲节主题文字动画特效
  20. 表面粗糙度的基本评定参数是_表面粗糙度最常用评定参数是什么?

热门文章

  1. 阿里云物联网平台基础
  2. 定理在数学中的简写形式_初中数学定义、定理汇总
  3. 2021届的Java后端应届生面试总结
  4. MongoDB文件服务器搭建
  5. java 注解报错_java-注解篇Annotation
  6. 苹果未能与恢复服务器取得联系解决
  7. MAR位数反映存储单元的个数笔记
  8. auto-cpufreq安装及配置过程
  9. windows 截图软件——sharex 截图软件的天花板 并且是免费开源的。
  10. 使用两个FBO互相绑定实现PS液化效果