MySQL InnoDB底层原理(索引+行锁)实现机制解析

【第一章】innodb行格式

1、我们要查某一条或者某多条数据的时候,是怎么样在计算机里面(innodb)进行查询运算的?

首先我们要知道一条数据显示在屏幕上其实是一个逻辑的一个视界。

我们查询数据,首先是将数据放到我们计算机的内存里面的

然后再根据SQL语句的条件与cpu读到的数据进行对比

就好比

select * from test where a >1 ;

因为存储引擎和文件系统是不会去执行这种大于、小于的操作的

只能够交给CPU去进行计算。也就是说数据是分地方存在我们磁盘的文件系统里面的

MySQL InnoDB底层原理(索引+行锁)实现机制解析_第1张图片

说白了就是我们在表中要查某一部分的数据的时候,表中的每一行数据都要被放到内存中,然后由CPU进行条件筛选。最后得出结果。

上述说明的,其实只是逻辑上的一个原理,其实它只是局部性原理的一个基础

2、计算机的局部性原理

这个是一个通用的原理

因为在实际中我们运行某一条指令的时候,可能只需要用到某一个字节的数据,但是根据计算机的局部性原理,它会认为你接下来会默认用到与刚刚那个字节相邻的数据。取一个字节的时候会把相邻的数据给去到内存中,下次如果用到了以后就会直接在内存中查,而不是去做一次读的I/O。

好了这里做一个引申

我们一次取多少?

答案是一页。(通常的操作系统的页大小为4KB)

如果你取的数据是属于某一页的那么就会直接将这一页的数据取出来

现在的innodb 也是借鉴了这个原理,他有自己的一个innodb 页大小

也就是说,我们的某部分数据是存储在同一页中的,每次我只需要从页中取一页就好了

页结构(这个需要重点记忆)

MySQL InnoDB底层原理(索引+行锁)实现机制解析_第2张图片

这里面record 是记录的意思

MySQL InnoDB底层原理(索引+行锁)实现机制解析_第3张图片

3、innodb行格式

什么是innodb 行格式,行格式其实就是我们insert 一条记录到数据库中(文件系统)的时候,这一行的格式是什么?

一条记录可以以不同的格式存在innodb 中,行格式分别是compact(小型的、紧密的、秀珍的)、redundant(冗余的、多余的)、dynamic(动态的)和compressed(压缩的、浓缩的、精简的)

首先来看:

compact

MySQL InnoDB底层原理(索引+行锁)实现机制解析_第4张图片

其中边长字段长度列表、null标志位、记录头信息是属于数据库记录的额外信息

(这不是废话么?一看就知道)

例子:

我们现在定义一个表结构

MySQL InnoDB底层原理(索引+行锁)实现机制解析_第5张图片

这里的a是主键

bcd是 int 类型的数据,其中int 为4个字节

e为可变长的字符串(20表示,最长不能超过20个字符//MySQL5.0.3之前是字节)

(char表示固定长度的类型)

变长字段长度列表

MySQL中支持一些变长的数据类型,比如varchar(x)、varbinary(x)、text类型、blob类型,这些数据类型修饰列成为变成长字段,变长字段中存储多少字节的数据不是固定的,所以我们在存储真实数据的时候需要顺便把这些数据占用的字节数也存起来。在compact行格式中,把所有变长字段的真实数据占用的字节长度都存放在记录的开头中,从而形成一个变成字段长度列表

另外的,如果表里面没有定义可边长的字段,就可以去掉变长字段的列表

NULL标志位

还是上面说的那张表MySQL InnoDB底层原理(索引+行锁)实现机制解析_第6张图片

这里假设b c d 为空

我们正常在数据库视界中看到的数据存储格式如下:

1   1    1    1    1

2 null null null e

上面这样子就有点浪费空间的感觉

那么我们现在就想这么存

1 1 1 1 1

2 e

这里就需要使用null 标志位,他其实是在记录的前面做了数据的占位符,我们可以假定它的规则为:0表示null ;1表示非null。如果我们的记录为2e,则在null标志位中可以对应的表示为0001

另外的,如果表中定义了,所有的字段都不可以为空(不可以为null),相应的我们可以去掉NULL标志位

MySQL InnoDB底层原理(索引+行锁)实现机制解析_第7张图片

记录头信息

MySQL InnoDB底层原理(索引+行锁)实现机制解析_第8张图片

行溢出数据

MySQL InnoDB底层原理(索引+行锁)实现机制解析_第9张图片

上述的意思是,因为有可边长的属性存在,并且要考虑到null 标志位,我们的一行的数据除开BLOBS以外不能够加起来超过65535,这个是MySQL innodb 的一个限制

MySQL innodb 的一页为16385个字节。假设我们的行最长定位了65535个字节,这个时候会导致“分多页存储”“行溢出”的情况

意味着,我们找一行的信息的时候需要跨页去找了。

我们可以这样做:

第一页存一页的数据+下一页的地址

第二页存一页的数据+下一页的地址...以此类推

这样的存法就是我们说的compact(小型的)这种innodb行格式的存法。

这里直接引申到dynamic 和 compressed 行格式

MySQL InnoDB底层原理(索引+行锁)实现机制解析_第10张图片

MySQL 默认是使用了动态的行格式

dynamic 这种行格式其实和 compact 这种存储方式差不多(不同的是dynamic 增强了可变较长列的存储能力以及更大的索引前缀)

【第二章】索引

【聚簇索引】

1、通过innodb 引擎建好的表,是排好序的(聚集:索引的感觉),如果是用myisam引擎建出来的表,其实是没有排序的(堆表:数据一行一行的累计到表里面)

2、innodb 存储数据的时候是怎么排序的

例子:1_111a 等等的都是就一条记录

MySQL InnoDB底层原理(索引+行锁)实现机制解析_第11张图片

箭头代表指针,没插入一次就按照第一个字符(主键进行排序)

指针是写在记录头里面的信息,指向的是下一个记录的地址

我们针对上面的例子作一个解析

(1)像innodb 这种已经排好序的引擎,我们查找的时候例如我们查找3索引开头的记录,我们只需要查找到4_311d 就可以了,因为这是一个递增序列,我们只需要查到4_311d就可以断定没有3_这个记录在这种表中。这个是这种排序对查找的好处。

(2)我们做一个假设,就是说假设每一行记录都非常小,意味着每一页存储的记录数就非常的多,这个链表也就非常的长,我们都知道链表对数据的插入是很快的,但是对数据的查询是很慢的,这个时候我们就要用到“页目录”

MySQL InnoDB底层原理(索引+行锁)实现机制解析_第12张图片

页目录存得是每一页开头第一行记录的值

我们可以想想查找5_索引开头的记录是如何查找的

(3)假设我们一页只能存4个记录,我们一共有8个记录,我们现在要存储他们,则需要2页

MySQL InnoDB底层原理(索引+行锁)实现机制解析_第13张图片

我们要查找5_索引开头的记录,我们需要使用“目录页”

MySQL InnoDB底层原理(索引+行锁)实现机制解析_第14张图片

目录页里面存每页页号以及对应的那一页的最小索引值

那么这里就很像一个东西~~

B+树

MySQL InnoDB底层原理(索引+行锁)实现机制解析_第15张图片

就是在插入数据的过程中直接就生成了这个B+树,贼牛逼

MySQL InnoDB底层原理(索引+行锁)实现机制解析_第16张图片

这也就是innodb 里面的聚集索引

这里请注意

页数越多B+树越深

那么真实的innodb 还有些补充的地方

就是我们新开辟一张表,就是新开辟一页,当我们的页没有空间继续存储数据的时候,我们将第一页copy 然后将原来的第一页改造成为一个目录页,以此类推,这个目录页是从一开始创建到后续是永远都不会变化的。所以我们可以把它缓存起来。缓存到内存中。

MySQL InnoDB底层原理(索引+行锁)实现机制解析_第17张图片

一般建表会用一个自增主键做聚簇索引,没有的话MySQL会默认创建,但是这个主键如果更改代价较高,故建表时要考虑自增ID不能频繁update这点。

优点:

1.数据访问更快,因为聚簇索引将索引和数据保存在同一个B+树中,因此从聚簇索引中获取数据比非聚簇索引更快

2.聚簇索引对于主键的排序查找和范围查找速度非常快(树的深度很小,查询效率很高;范围查询的时候效率很高。)

缺点:

1.插入速度严重依赖于插入顺序,按照主键的顺序插入是最快的方式,否则将会出现页分裂,严重影响性能。因此,对于InnoDB表,我们一般都会定义一个自增的ID列为主键

2.更新主键的代价很高,因为将会导致被更新的行移动。因此,对于InnoDB表,我们一般定义主键为不可更新。

3.二级索引访问需要两次索引查找,第一次找到主键值,第二次根据主键值找到行数据。

【辅助索引】

我们所说的辅助索引,就是除开主键索引以后的索引,例如上面的BCD

我们都知道建立主键索引的时候,我们其实强以来索引的排序

那么BCD的排序也一样,先对BCD进行排序,然后按照与主键索引一样的方法进行构建B+树。

辅助索引其实是二级索引,需要回表:辅助索引+主键回表

【原则】

一个SQL是否会用到索引

最小前缀原则:

第一条会用;因为缩小了查询范围

第二条不会用到,因为无法比较BCD里面的B

第三条也不行。只能走全表扫描。

MySQL InnoDB底层原理(索引+行锁)实现机制解析_第18张图片

【索引使用的一些技巧(开发和测试的时候需要注意下)】

索引是建立在数据库表中的某些列上面的,因此,在创建索引的时候,应该仔细考虑在哪些列上可以创建索引,在哪些列上不能创建索引

应该创建索引的列:

在经常需要搜索的列上

在作为主键的列上,强制该列的唯一性和组织表中数据的排列结构

在经常用在连接(join)的列上,这些列主要是一外键,可以加快连接的速度

在经常需要根据范围进行搜索的列上面创建索引,因为索引已经排序,其指定的范围也是连续的(in between < <= = > >=)

在经常需要排序(order by) 的列上创建索引,因为索引已经排序,这样查询可以利用索引的排序,加快排序查询时间

在经常使用在where 子句中的列上面建立索引,这样可以加快条件的判断

不应该创建索引的列:

在查询中很少使用的列

对于那些只有很少数据值或者重复值多的列

对于那些定义为text image bit 数据类型的列不应该增加索引(这些列的数据量要么相当大,要么取值很少)

当该列修改性能要求远远高于检索性能的时候(取舍,取修改,舍去查询)的时候

【第三章】事务

1、事务是什么

值得是SQL要做的或者是所要做的事

2、原子性(atomicity)

要么做要么都不做

3、隔离性(lsolation)【重点】

事务间相互隔离,互不影响

4、一致性(consistency)

数据的一致性

5、持久性

数据的持久性,数据会被持久化

6、事务有自动提交的属性(autocommit)

这是个参数,需要默认是开启的,想关闭就直接OFF

可以手动开启事务,使用关键字begin ... commit ... end

【隐式提交】

MySQL InnoDB底层原理(索引+行锁)实现机制解析_第19张图片

7、保存点

MySQL InnoDB底层原理(索引+行锁)实现机制解析_第20张图片

8、修改事务的隔离级别语句

set session transaction isolation level xxx (隔离级别);

【第一个隔离级别:读未提交】

read uncommitted

MySQL InnoDB底层原理(索引+行锁)实现机制解析_第21张图片

【第二个隔离级别:读已提交】

read comitted

目的:为了解决第一种隔离级别产生读脏数据的问题

T1事务没有提交之前的数据是不会被T2查询到的,所以就解决了第一种事务隔离级别的问题

这个事务隔离界别是oracle 数据库默认的

会产生幻读和不可重复读MySQL InnoDB底层原理(索引+行锁)实现机制解析_第22张图片

MySQL InnoDB底层原理(索引+行锁)实现机制解析_第23张图片

【第三个隔离级别:可重复读】

还是会出现幻读(其他的数据库,除了MySQL以外)、但是不会出现不可重复读

MySQL就是这个隔离级别

这玩意其实就是,你T1就算提交了修改,我T2还是查不到

【第四种隔离级别:串行化】

排队,上一个操作没有完成的时候我们后续的操作就只能排队

MySQL InnoDB底层原理(索引+行锁)实现机制解析_第24张图片

【如何实现-读已提交】

首先我们要说明

有这么一个数据a = 1

过程:

T1 此时读a ,a = 1

T2 此时修改a , a = 2 (未提交)

T1 此时读的时候首先在readview中找到活跃的事务T2(以及其他)的事务ID

在readview中命中非活跃的版本链

T2 此时commit

T1 此时读的时候首先在readview中找到活跃的事务(不包括T2,因为T2已经提交)的事务ID。在readview中命中T2(因为在时间轴中,T2是最新修改了数据的事务)

MySQL InnoDB底层原理(索引+行锁)实现机制解析_第25张图片

 

 MySQL InnoDB底层原理(索引+行锁)实现机制解析_第26张图片

这里注意m_ids,这个参数

MySQL InnoDB底层原理(索引+行锁)实现机制解析_第27张图片

【如何实现可重复读】

根据上述列子

首先我们要说明

有这么一个数据a = 1

过程:

T1 此时读a ,a = 1

T2 此时修改a , a = 2 (未提交)

T1 此时读的时候首先在readview中找到活跃的事务T2(以及其他)的事务ID

在readview中命中非活跃的版本链

T2 此时commit

T1 此时读的时候直接复用上一次获取到的readview,包括里面的所有参数,很容易立即这里就不会再出现读T2已经commit的数据的情况了

【多版本并发控制MVCC】

其实就是上述所说的【可重复度】+【读已提交】的结合

MySQL InnoDB底层原理(索引+行锁)实现机制解析_第28张图片

这样有什么好处

1、这种机制没有锁的存在。提高并发效率。

【第四章】数据库锁

1、首先,select 这种SQL语句是可以绕过记录锁(杭行锁)进行操作的,它不会被阻塞,不会发生锁冲突。说白了就是select 操作是不加锁的(至少innodb里面是这样)

2、锁的分类:读锁(S锁、共享锁,加上锁以后,读出数据后,别人是可以读的,但是写不行;对于枷锁的那个事务,他也不一定可以修改,是因为别人也可以对这个记录加上一个S锁);写锁(X锁,排它锁,读出数据后,select 可以操作,但是其他的操作是都不行了,因为其他的操作实际上都会给它加X锁)

3、

MySQL InnoDB底层原理(索引+行锁)实现机制解析_第29张图片

4、

insert 插入记录的时候会先加上“隐式锁”

隐式锁:一个事务插入一条记录后,还未提交,这条记录会保存本次事务的ID,而其他事务如果想要对这个记录进行枷锁时会发现事务id不对,这时候会产生X锁,所以相当于在插入一条记录时,隐式的给这条记录加上了一把X锁。

delete 删除一条数据的时候,先对其进行X锁(排他锁),在执行删除操作

update

如果被更新的列,修改前后没有导致存储空间变化,那么会先给记录加上X锁,再直接对记录进行修改。

如果被更新的列,修改前后导致存储空间变化了,那么会先给记录加上X锁,在insert一条新纪录。

事务结束之后,锁都会被释放掉。(commit)

5、行锁和表锁

LOCK_REC_NOT_GAP 单个行记录上的锁

LOCK_GAP 间隙锁,锁定一个范围,但不包括记录本身。GAP锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况

LOCK_ORDINARY 锁定一个范围,并且锁定记录本身,对于行的查询,都是采用该方法,主要目的是解决幻读的问题。

【行锁1:隔离级别:读已提交】//对于查出来的行才能锁住,但是没有查出来的不会锁住【记住这个隔离级别是:读已提交】

唯一索引的列、主键索引的列、没有索引的列都是一样的。只有查出来的数据才枷锁了。

【行锁2:隔离级别:可重复读】【记住这个隔离级别 是:可重复读】

会对附近的间隙进行枷锁

为了防止幻读,直接在附近枷锁了。不让你继续插入数据。

如果使用的是全表扫描的话,就会把整个表以及所有的间隙都锁住了

你可能感兴趣的:(MySQL InnoDB底层原理(索引+行锁)实现机制解析)