MySQL存储引擎原理深入剖析


1 InnoDB存储引擎原理

以Page为单位存储,每个Page的结构如下:(每个页的大小为16KB)
在这里插入图片描述

  • 页头
    • 记录页面的控制信息,共占56字节,包括页的左右兄弟页面指针、页面空间使用情况等。
  • 虚记录
    • 最大虚记录:比页内最大主键还大
    • 最小虚记录:比页内最小主键还小
  • 记录堆
    • 行记录存储区,分为有效记录和已删除记录两种
  • 自由空间链表
    • 已删除记录组成的链表
  • 未分配空间
    • 页面未使用的存储空间;
  • 页尾
    • 页面最后部分,占8个字节,主要存储页面的校验信息;

结构示意图:
在这里插入图片描述

  • 顺序保证:使用逻辑有序(链表)而非物理有序(数组)
  • 插入策略:自由空间链表+未使用空间。优先使用自由空间链表。
  • 查询策略:
    • 首先找到数据在哪一页;
    • 其次通过二分查找找到需要的那行数据。这里使用了跳表的数据结构:
      在这里插入图片描述
      slot区是一段连续的存储空间,每个slot大小相同,这样就可以使用二分法找到所需要的记录在哪个子链表上。

2 内存管理

在这里插入图片描述
在这里插入图片描述

2.1 内存淘汰算法

正常使用LRU即可,但是单纯的LRU会存在一个问题:假如我做了一次全表扫描,那么所有的数据都会被加载进内存一次,这样就导致热数据失效。

因此,解决热数据失效可以从两方面考虑:

  • 结合LFU(Least Frequent Used 最少频率使用)来淘汰页面
  • 再来一个LRU队列,这样组成双LRU队列:一个保存热数据,一个保存冷数据。热LRU设定一个规则,达到该规则后热的page会转移到冷LRU中;冷LRU页面也同样设定规则,允许冷page转移到热LRU中。

InnoDB的解决方法:双LRU
在这里插入图片描述

2.1.1 移动的规则

  • old到new:
    在这里插入图片描述

  • 从new到old
    直接把new区的尾节点放到old区的头节点即可。
    在这里插入图片描述

  • LRU_new中的操作
    通常来说,LRU_new保存的是最新最近访问的数据,一个数据一旦被访问到,就移动到头部。我们知道链表的插入操作很快,但是这样一直插入头部会存在性能问题吗?需要去优化吗?

在这里插入图片描述
事实上并不是每个page一访问就移动到头部,而是看这个page在访问的时候它所处的位置。
a. 如果当前page在访问的时候已经到了LRU的1/4之后,它会面临被淘汰的危险。这时就把它移动到头部。
在这里插入图片描述

b. 如果当前page在访问的时候还在LRU的前1/4,则不需要移动到头部。

具体当前Page在LRU的哪个部分,是从该Page上次移动到头部,到当前时刻一共发生了多少次页面淘汰而计算的。

3 事务

一般我们会认为 begin/start transaction 是事务开始的时间点,也就是一旦我们执行了 start transaction,就认为事务已经开始了,其实这是错误的。事务开始的真正的时间点(LSN),是 start transaction 之后执行的第一条语句,不管是什么语句,不管成功与否。

但是如果你想要达到将 start transaction 作为事务开始的时间点,那么我们必须使用:

start transaction with consistent snapshot

它的含义是:执行 start transaction 同时建立本事务一致性读的 snapshot . 而不是等到执行第一条语句时,才开始事务,并且建立一致性读的 snapshot .

效果等价于: start transaction 之后,马上执行一条 select 语句(此时会建立一致性读的snapshot)。

3.1 MVCC

MVCC又叫多版本并发控制。用于解决读写冲突。InnoDB在每个表创建的时候都会追加两个隐藏列:事务ID列(DB_TRX_ID)和回滚指针列(DB_ROLL_PTR)。前者的含义是,ID为x的事务将表中的这条数据变为当前状态。后者可以实现从当前数据从一个事务转移到另一个事务。

两个重要的概念:

  • 当前读:select for update / insert / update / delete。读取最新提交的数据,并且对读取的记录加锁,阻塞其他事务同时修改相同记录,避免出现安全问题。
  • 快照读:select。读取当前时刻可见的数据。不管提交了多少次,始终读取到的都是当前可见的最新历史版本。实现了RR可重复读。

    Read Committed隔离级别:每次select都生成一个快照读
    Read Repeatable隔离级别:开启事务后第一个select语句才是快照读的地方,而不是一开启事务就快照读

3.1.1 快照读如何实现了RR?

在这里插入图片描述

  • 每个事务创建的时候,会创建一个read view,记录当前所有活跃的事务(已创建但未提交的事务)。
  • 读取(select)一条数据时,查看该数据的事务ID。
    • 如果小于活跃事务的最小ID,说明产生该数据的事务已经提交,该数据可读
    • 如果大于活跃事务的最大ID,说明产生该数据的事务还没创建,自然在当前事务下是不可读的
    • 如果在最小ID-最大ID之间
      • 如果ID在活跃事务列表里,说明产生该数据的事务还没提交,不可读。
      • 如果ID不在活跃事务列表里,说明已经提交,可读。

如下图,事务A在创建的时刻,它只能读到事务B提交的数据,而无法读到事务C和D提交的数据。
在这里插入图片描述

3.2 undolog

  • 回滚日志
  • 保证事务原子性
  • 实现数据多版本
  • delete undo log:用于回滚,提交即清理;
  • update undo log:用于回滚,同时实现快照读,不能随便删除

在这里插入图片描述
如图,第一条数据是保存在存储引擎里的,后面两条保存在undolog里。

思考:undolog如何清理?
答:依照系统活跃的最小活跃事务ID Read view。ID最小的事务可以看到的数据,全局都可以看到,则这些数据以前的版本都可以删除掉。

3.3 redolog

在这里插入图片描述
刷盘时机:通过一个参数指定。推荐2.
在这里插入图片描述
redolog的意义?

  • 体积小,记录页的修改,比写入页代价低
  • 末尾追加,随机写变顺序写,发生改变的页不固定

4 锁

  • 锁粒度
    • 行级锁
    • 间隙锁
    • 表级锁
  • 类型
    • 共享锁(S)
      • 读锁,可以同时被多个事务获取,阻止其他事务对记录的修改;
    • 排他锁(X)
      • 写锁,只能被一个事务获取,允许获得锁的事务修改数据;所有当前读加排他锁,包括SELECT FOR UPDATE、UPDATE、DELETE

4.1 如何加锁?RR/RC * 唯一索引/非唯一索引

探讨为什么RR隔离级别能够解决幻读。

幻读举例:假如数据库中存在三条记录:{a: 1, b: 2, c: 3}。事务A通过select快照读可以读到这三条数据。这时事务B向数据库中insert了一条数据d:4并提交。此时事务A同样想插入数据d:5,但是没有成功,事务A再select还是看到的abc三条数据。

4.1.1 RC & 非唯一索引

在这里插入图片描述

4.1.2 RR & 非唯一索引

RR级别的幻读是怎么产生的?
还是上面那个例子:delete from user where phone = 134。
事务A执行这句话,显示影响行数2。此时事务A并不提交。
事务B此时执行插入insert into user values(134, 106)
事务A再执行delete语句,发现又有一条影响行数
如果事务B反复执行插入,则事务A每次执行delete都会看到新的影响行

在这里插入图片描述

问题关键:记录之间的间隙。
解决方法:间隙锁

间隙锁

  • 解决可重复读模式下的幻读问题;
  • GAP锁不是加在记录上;
  • GAP锁锁住的位置,是两条记录之间的GAP;
  • 保证两次当前读返回一致的记录;

间隙锁保证两次当前读之间,其他事务不会插入新的满足条件的记录!

在这里插入图片描述

4.1.3 表级锁

什么情况下会触发表级锁?全表扫描!
如果phone不是索引,则下面这条语句会全表扫描。扫描时RC隔离级别会把每条记录加锁再释放;RR隔离级别还会给每个Gap加锁再释放。
在这里插入图片描述
理论上只有事务提交后才会释放锁,但server层做了一个过滤,把没有命中记录的锁都提前释放了。

4.1.4 加锁过程

死锁问题

在这里插入图片描述


版权声明:本文为u013165358原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
THE END
< <上一篇
下一篇>>