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
- 共享锁(S)
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层做了一个过滤,把没有命中记录的锁都提前释放了。