InnoDB实现原理
- 它是MySQL从5.5版本开始的默认的存储引擎,是第一份支持ACID特性的MySQL存储引擎,特点是行锁设计,支持MVCC(多版本并发控制),支持外键,提供一致性非锁定读,同时尽可能高效的利用计算机硬件资源.
MVCC
MVCC (Multiversion Concurrency Control),即多版本并发控制技术,它使得大部分支持行锁的事务引擎,不再单纯的使用行锁来进行数据库的并发控制,取而代之的是把数据库的行锁与行的多个版本结合起来,只需要很小的开销,就可以实现非锁定读,从而大大提高数据库系统的并发性能
InnoDB体系架构
image.png- 后台线程
Master Thread:将内存数据异步刷新到磁盘, InnoDB 的主要⼯作都是该线程
完成. 该线程具有最⾼的优先级, 在 DB 运⾏过程中主要进⾏两部分操作: 每秒将⽇志缓冲刷新到磁盘, 合并插⼊缓冲, 将脏⻚刷新到磁盘等. - IO Thread
处理IO请求回调 - Purge Thread
在事务提交的时候回收Undo Log - InnoDB 存储引擎是基于磁盘的, 并将其中所有的记录按照⻚的⽅式进⾏管理. 由于
CPU 和磁盘速度的鸿沟, 采⽤缓冲池技术来提⾼数据库的性能. - 缓存池是⼀块内存区域, 在 DB 读取⻚的时候, ⾸先从磁盘读到缓存池中, 成为将⻚
FIX 在缓冲池中, 下次再读相同⻚的时候, 先判断是否在缓冲池中. - 对于写操作, ⾸先修改缓冲池中的⻚, 再以⼀定的频率刷新到磁盘上
- InnoDB 中缓冲池⻚的⼤⼩默认为 16KB, 使⽤ LRU 算法进⾏管理. InnoDB 对新读
取的⻚会放在 midpoint (默认5/8⻓度处). -
重做⽇志缓冲: InnoDB ⾸先将 Redo Log 放⼊这个缓冲区, 然后以⼀定的频率刷新
到 Redo Log ⽂件中
image.png
数据页
数据页,操作系统通过机械磁头读取硬盘中的文件,读取指定的某数据之后,自动读取大约4KB的页数据,将与此数据有关的一并读取,为了机器性能考虑,因为在磁盘,磁道,扇区中机械查找数据并不容易,直接读取的数据页为了提升机器效率
索引页
索引页,MySQL的InnoDB引擎通过B+实现索引页,相比B树,大大减小了树深,在磁盘中读取数据,减小读取次数,有效提升速度,B+树搜索之后并不能直接找到数据,只能找到索引页,将该页加载入内存,通过key在其中进行二分查找,找到key对应的slot,在slot管理的记录中查找最终的数据
插入缓冲
学长在讲解的时候,只是提到了插入缓冲对于提高数据的插入和修改有很大的帮助,对于非聚集索引的插人或更新操作,不是每一次直接插入到索引页中,而是先判断插入的非聚集索引页是否在缓冲池中,若在,则直接插人;若不在,则先放入到一个 Insert Buffer 对象中,好似欺骗。数据库这个非聚集的索引已经插到叶子节点,而实际并没有,只是存放在另一个位置。然后再以一定的频率和情况进行 Insert Buffer 和辅助索引页子节点的 merge(合并)操作,这时通常能将多个插人合并到一个操作中(因为在一个索引页中),这就大大提高了对于非聚集索引插人的性能。
自适应哈希索引
InnoDB存储引擎会监控对表上各索引页的查询。如果观察到建立哈希索引可以带来速度提升,则建立哈希索引,称之为自适应哈希索引(Adaptive Hash Index, AHI)。AHI是通过缓冲池的B+树页构造而来,因此建立的速度很快,而且不需要对整张表构建哈希索引。InnoDB存储引擎会自动根据访问的频率和模式来自动地为某些热点页建立哈希索引。
日志文件
错误日志(ErrLog)
错误⽇志对 MySQL 的启动, 运⾏, 关闭过程进⾏了记录, 不仅记录了所有错误信息, 也记录了⼀些警告信息, 遇到问题是应该⾸先查看
该⽂件以便定位问题. 当 MySQL 不能正常启动, 或者 MySQL 在运⾏期间遇到的内存不⾜等问题都可以在其中找到详细记录.
慢查询日志(SlowLog)
慢查询⽇志能够定位到可能存在问题的 SQL 语句, 可以设定⼀个阈值, 将运⾏时间超过该值的 SQL 语句都记录到慢查询⽇志⽂件中.
另⼀个可⽤的参数是 log_queries_not_using_indexes, 如果SQL 语句没有使⽤索引, 同样会将这条 SQL 语句记录到慢查询⽇志⽂件.
查询日志(QueryLog)
查询⽇志记录了所有对 MySQL 请求的信息, 不论是否得到了正确的执⾏. 默认⽂件名: 主机名.log.
二进制日志
BinLog 记录了对 MySQL 执⾏更改的 (不包括SELECT 和 SHOW 等对数据本身没有修改) 的操作. 不过若⼀个修改操作本身没有导致
数据库发⽣变化也会被写⼊ BigLog 中(如修改了⼀条不存在的记录). 此外, ⼆进制⽇志还包括了 DB 修改操作的时间以及其他额外信
息, BinLog 主要的⽤途有:
- 数据恢复: 某些数据的恢复需要 BinLog.
- 集群同步: 通过复制和执⾏ BinLog, 使⼀台远程的 MySQL 从库与当前主库进⾏实时同步.
- 数据同步: 不同于集群同步, 业务场景中经常需要其他组件(搜索引擎, 业务报表等)需要感知数据库的修改, 此时可以通过同步
BinLog 实现.
InnoDB表存储
在 InnoDB 中, 表都是根据主键顺序组织存放的, 这种存储⽅式成为索引组织表. 每张表都有⼀个主键, 如果没有显
式指定, InnoDB 会使⽤第⼀个⾮空的唯⼀索引, 如果没有唯⼀索引, InnoDB 会⾃动创建⼀个 6 字节⼤⼩的指针作
为主键.
所有的数据被存放在⼀个表空间 (tablespace) 中, 表空间⼜由 段(segment), 区(extent), ⻚(page) 组成.
• 表空间(Tablespace), 是 InnoDB 存储引擎逻辑存储的顶层对象, 所有数据都存放在表空间. 其中包括数据, 索引,
插⼊缓冲, 回滚信息, 系统事务信息等.
• 段(Segment), 表空间时由多个段组成的, 常⻅的段有数据段, 索引段, 回滚段等. InnoDB 由于索引组织表的特点,
数据即索引, 索引即数据. 因此数据段是 B+ 树的叶⼦节点, 索引段是 B+ 树的⾮叶⼦节点. 回滚段
• 区(Extent), 区是由连续⻚组成的空间, 在任何情况下每个区的⼤⼩都为 1MB, 为了保证区中⻚的连续性, InnoDB
⼀次从磁盘申请 4-5 个区, 默认情况下 InnoDB ⻚⼤⼩为 16KB, ⼀个区中⼀共有 64 个连续的⻚.
• ⻚(Page): 是 InnoDB 磁盘管理的最⼩单位, 默认 16KB, 常⻅的⻚有: 数据⻚ / undo log⻚ / 系统⻚等.
• ⾏(Row): InnoDB 中数据按⾏进⾏存放, 每⻚最多允许存放 16KB / 2 - 200 = 7992 ⾏记录
InnoDB 中⻚是管理数据库的最⼩磁盘单位, ⻚类型为 B-tree Node 的⻚存放的就是
表中的实际数据.InnoDB 数据⻚由⼀下 7 部分组成:
• File Header(⽂件头), 固定 38 字节, ⽤来记录头信息, 以及相邻⻚的指针.
• Page Header(⻚头), 固定 56 字节, ⽤来记录数据⻚的状态信息.
• Infinum 和 Suprenum Records, 虚拟的⾏记录, ⽤来限定记录的边界
• User Recodes(⽤户记录, 即⾏记录)
• Free Space(空闲记录)
• Page Directory(⻚⽬录), 存放记录的相对位置, 找到 B+ 树叶⼦节点后, 再通过
Page Directory 再进⾏⼆分查找.
• File Trailer(⽂件结尾信息), 固定 8 字节, ⽤于检测⻚是否已经完整地写⼊
B+树
image.png⼀条索引的命运啊, 当然要靠查询复杂度, 但也要考虑到与硬件的兼容.
B+ 树结构本身⽐较复杂, 是为磁盘或其他直接存取设备设计的⼀种平衡查找树, 所有记录节
点都是按照键值⼤⼩顺序放在同⼀层叶⼦节点上, 由各叶⼦指针进⾏连接.
B+ 树的插⼊必须保证插⼊后叶⼦节点依然有序, 同时不同的情况可能会导致不同的插⼊算法.
索引
image.png- 聚集索引
InnoDB 存储引擎是索引组织表, 表中数据按照主键顺序存放. 聚集索引就是按照每张表的主键构造⼀棵 B+
树, 同时叶⼦节点中存放整张表的记录数据(数据⻚), 聚集索引的特性决定了索引组织表中数据也是索引的⼀
部分, 每个数据⻚都通过⼀个双向链表来进⾏连接.
每张表只能拥有⼀个聚集索引, 并且多数情况下查询优化器都倾向于采⽤聚集索引, 因为可以直接在其叶⼦节
点上找到数据. - 辅助索引
但是实际⽣产中, 往往需要通过不同的⽅式去查询, 此时⼀个聚集索引是远远不够的, 就引出了辅助索引, 叶
⼦节点不包括⾏记录的全部数据, 叶⼦节点除了包含键值以外, 每个叶⼦节点中还包含了⼀个指针, 指向聚集
索引中的主键, 然后再通过主键索引来找到⼀个完整的记录 - 联合索引
联合索引也是⼀棵 B+ 树, 不同的是联合索引的键值数
量⼤于2.
此外, 逻辑上和单键值的 B+ 树并没有什么区别, 键值依
旧是有序的, 只不过这个有序是⼀个前提的:
⾸先会按照 a 列进⾏排序, 此外 b 列的排序规则是在 a
相同的前提下, b 才有序. - 覆盖索引
InnoDB ⽀持直接从辅助索引中查询记录并返回(如果有
的话), ⽽不需要查询聚集索引中的记录. 使⽤覆盖索引
的好处是辅助索引不包含整⾏记录的所有信息, 故其⼤
⼩要远⼩于聚集索引, 因此可以减少⼤量的 IO 操作.
由于 InnoDB 的辅助索引包含了主键信息, 因此其叶⼦
节点数据为 (pk1-k1) 的组合. 因此下列语句都可以仅仅
通过⼀次辅助联合索引来完成查询 - 索引失效
有时候在执⾏ EXPLAIN 进⾏ SQL 语句分析的时候, 会
发现优化器并没有选择索引, ⽽是执⾏全表扫描. 这种
情况多发⽣于范围查找, JOIN 连接等场景.
典型的场景是如果辅助索引不能覆盖所有要查询的信
息, 就需要再⾛⼀次主索引, ⽽主索引上的访问是离散
的, 离散的读操作如果数量较⼤, 优化器就会选择通过
聚集索引来查找数据.
因此在优化的时候尽量通过辅助索引查询到全部数据,
避免⼤量的回表.
• OR 条件连接的多个条件中有不⾛索引的字段;
• 复合索引的使⽤不符合最左匹配原则;
• LIKE 查询前缀模糊匹配;
• InnoDB 出现隐式类型转换(varchar -> bigint);
• InnoDB 评估使⽤全表扫描⽐⾛索引更快;
• ⽆法通过索引获取全部数据, 需要从主索引中进⾏⼤
量的随机读取
锁
锁是数据库系统区别于⽂件系统的⼀个关键特性, ⽤于管理对共享资源的并发访问.
InnoDB 会在⾏记录上加锁. 同时也会在内部其他地⽅使⽤, 如 LRU 列表. InnoDB 提
供⼀致性的⾮锁定读, ⾏锁, 可以同时得到并发性和⼀致性.
InnoDB 实现了⼀下两种标准的⾏级锁:
• 共享锁, 允许事务读⼀⾏数据.
• 排他锁, 允许事务删除或更新⼀⾏数据.
此外, InnoDB ⽀持多粒度锁定, 允许事务在⾏级别和表级别同时存在, 因此引出了意
向锁(Intention Lock), 意向锁意味着事务希望在更细粒度上进⾏加锁
⾮⼀致性锁定读(consistent nonblocking read) 是值 InnoDB 通过多⾏版本控制的
⽅式来读取当前执⾏时间数据中的数据. 如果当前数据正在执⾏写操作, 这是读取操
作不会因此阻塞, ⽽是去读取⼀个快照数据.
快照数据就是当前⾏数据的历史版本, 每⾏记录可能⼜多个版本, READ
COMMITTED 和 REPEATABLE READ 下, InnoDB 都会使⽤⼀致性⾮锁定读, 但⽅式
不同:
• READ COMMITTED 下, ⼀致性⾮锁定读总是读取被锁定⾏的最新⼀份快照.
• REPEATABLE READ 下, ⼀致性⾮锁定读总是读取事务开始时的⾏版本数据.
锁带来的问题
- 脏读(读到了中间状态)
本质上是写操作只加
共享锁带来的问题,
会让其他事务读取到
该事物的中间状态。
⼀般不会去使⽤,因
为读到了中间状态(脏
数据) - 不可重复读(读到了最终的数据 尽管与刚开始的不一样)
本质上是读不加锁带
来的,因此会让同⼀
事务的两次读操作取
回不⼀致的结果。
⼀般来说可以接受,
因为读到的已经是提
交后的数据 - 丢失更新(两事务同时操作 导致更新的覆盖)
本质上是写不加锁带
来的后果,两个事务
可以同时对同⼀数据
进⾏操作,必然导致
先提交的被覆盖。
会带来⾮常严重的问
题,应该尽可能避免
死锁
多个事务, 以不同的顺序去获取相同的资源, 就容易引发死锁.
image.png
解决
- 超时检测: 设置⼀个阈值, 当任意⼀⽅等待时间超过预设的阈值时, 其中⼀个事务回滚.
- Wait-for-graph 主动检测: 通过 “等待获取的锁” 和 “等待获取该锁的事务”, 构造出⼀张有向图,
如果图中存在回路, 就代表存在死锁.
InnoDB 将各个事务看成⼀个个节点, 资源就是各个事务占⽤的锁,
当事务1需要等待事务2的锁时, 就⽣成⼀条有向边从1指向2, 最终
形成⼀个有向图.
InnoDB 会定时遍历该图, ⼀旦发现回路, 就将其中⼀个回滚, 另⼀个
事务就得以继续执⾏. 被回滚的事务会返回 “dead lock
事务
- 原子性
事务的全部操
作要么全部执⾏, 要么
全部不执⾏. 将多个操
作绑定为同⼀个操作 - 一致性
将数据库从⼀
个⼀致状态改变为另
⼀种⼀致状态. ⽽不破
坏数据库完整性约束 - 隔离性
将数据库从⼀
个⼀致状态改变为另
⼀种⼀致状态. ⽽不破
坏数据库完整性约束 - 持久性
事务⼀旦提
交, 其结果就会永久保
存, 即便数据库发⽣宕
机, 也能将其恢复
注意原子性指的是执行的过程,在进程角度看的,一旦过程中发生异常则回滚到之前的状态.一致性,则站在系统的角度,双方转账之前总额200,转账之后总额还是200
事务的实现
Redo Log
重做⽇志⽤来实现事务中的持久性, 由两部分组成:
• 内存中的重做⽇志缓冲(redo log buffer)
• 磁盘中的重做⽇志⽂件(redo log file)
当事务提交的时候, 必须先将该事务的所有⽇志写⼊到磁盘中的重做⽇志⽂件进⾏持久化, 待事务
提交结束才算完成.
Undo Log
重做⽇志记录了事务的⾏为, 可以很好的对⻚进⾏”重做”操作, 但事务有时候需要进⾏回滚, 此时
就需要 undo log, redo log 存放在数据库内部的回滚段中, 位于共享表空间.
重做⽇志的主要⼯作是将数据库逻辑地恢复到原来的样⼦, 但数据结构和⻚本身在回滚之后可能
和事务开始前不太相同, 因为与此同时有⼤量的并发事务存在, 不能简单的将⼀个⻚回滚到事务开
始时的样⼦, 否则会影响其他事务.
Undo Log 的另⼀个功能是 MVCC, 当需要读取的记录已经被其他事务加锁的时候, 当前事务可以
通过 undo 读取之前的版本, 以此实现⼀致性⾮锁定读