看技术文章是不是很累呢, 这次来个轻松点的~来谈一谈MySQL最主流的数据库引擎 InnoDB 吧~
序
老王走进一号会议室, 随手打开了灯, 小张紧随其后
"王哥, 找我来干啥啊"
老王挥挥手示意小张坐下, "Boss交给了我们一个艰巨的任务, 不过你不用担心, 最难的部分Boss已经做完了"
小张听完松了口气, "那我们负责干啥?"
"设计一个MySQL的数据库引擎"
"那Boss干了啥"
"Boss把名字起好了, 叫 InnoDB"
"还有呢?" 小张一脸懵逼的看着老王
"没了啊, 做项目最难的不就是起名字嘛" 老王随即说到 "好了, 我们开始定下方案吧, 既然数据库的主要功能是增删改查, 那我们就按顺序一个一个来, 先从增开始吧"
"为了之后讨论方便我们先弄个表吧" 老王起身拿起马克笔在白板上写了一个表结构
Table Name : User
id : primaryKey
name : index
age
存储文件结构
老王 : "我们先从最底层的文件系统开始设计, 如果我要执行一条插入语句, 你觉得应该怎么持久化? 换句话说就是文件系统应该设计成什么样子"
小张 : "嗯 ... 弄个文件一行存一条?"
老王 : "你当做毕业设计呢? 咋不弄个txt都塞进去呢"
小张 : "不是, 王哥你听我说, 可以按照主键排序啊, 这样查找的时候可以二分查找, 插入数据的话先二分查找找到对应的位置, 然后插进去就可以了"
老王 : "这个设计是有问题的, 小时候用铅笔在田字格本上写过作文吧? 在你写完一篇作文之后, 你发现一个字写错了, 可以擦掉这个字然后改成正确的, 但是如果想在中间插一句话, 由于没有地方给你写, 是不是只能把后面的全部擦掉, 重新写一遍? 硬盘就跟田字格本是一样的, 根本不支持在随机位置插入数据, 只能覆盖已经有的数据. 还有一点, 田字格本是一行一行的, 但是硬盘根本没有行这一说, 你可以理解成一页只有一行的田字格, 随机读写只支持按照字节查找, 所谓换行只不过是发现数据里有个换行符才知道你这是要换行了, 本质上换行符对于磁盘来说和其他的数据没有什么区别, 也不知道换行符在哪儿, 所以如果你想知道一条数据是第几行数据只能把前面的数据都扫描一遍, 这个性能就很差了"
小张 : "啊? 那我就有点懵了, 看来只能按照顺序插入了? 那如果主键是UUID, 插入顺序肯定不是有序的, 这怎么办啊? 王哥我没思路了"
老王 : "嗯, 有点难为你了, 你看啊, 虽然硬盘只支持按照字节查找, 但是我们可以人为的分个页, 还是田字格本的例子, 一个本子, 每一页的格子数目都是固定的吧, 比如说一页有300个格子, 那我想读第10页, 是不是只要从第2700个格子开始读300个格子就正好把第10页给读完了?"
小张 : "是这样的, 也就是说一页有固定的大小, 读的时候只要知道页数, 就可以得到页的起始偏移量对吧, 然后呢?"
老王 : "对, 我刚才说的读, 是指读到内存里, 既然读到内存里了, 那么这一页具体有几行, 每一行的 id 是什么是不是就都知道了? 那么现在只需要知道每个 id 所在的页数, 那么这个问题是不是就解决了?"
小张 : "感觉还是没懂, 这个只能在顺序的情况下有用啊, 如果主键是UUID的话, 怎么保持数据的顺序呢"
老王 : "为什么一定要按照顺序保存呢? 我们可以引入索引, 就像你看书都有目录一样. 给你简单的画个图, 就别UUID了, 太长了, 假设我们插入数据的顺序按照 id 排列是 1 3 5 2 4 6 吧, 然后假设一页能存下两行数据, 那么等这几条数据都被插入之后, 磁盘里的数据是这样的"
老王 : "你看看, 这样的话, 数据是按照插入的顺序存储的, 但是即便物理上不延续, 只要在索引上是连续的, 那么是不是无论是指定id查找还是范围查找都很方便了?"
小张 : "是的, 王哥你真厉害, 这样插入数据即便是乱序也不需要擦掉文件重新写了, 只要维护好索引那个页就好了, 那我们就照这个方案走吧"
老王 : "先别急, 你看看这个方案有什么缺点?"
小张 : "嗯 ... 要是非说缺点的话就是, 这里假设的是一页2行数据, 如果实际上一页放了100行数据, 那么要是按照主键 id 进行范围查找的话, 由于每次读都需要读一整页, 那么是不是会浪费很多IO在用不到的数据上呢?"
老王 : "嗯, 总结的非常好, 这个设计是会有这个问题, 这种设计属于堆表的一种, 也就是存储顺序按照插入顺序排放, 和索引完全解耦, 优点就是写入很快, 缺点就是如果按照主键范围查询的话基本都是随机读操作, 而且由于有索引到实际数据页的一个映射过程, 所以往往会多一次硬盘读取"
小张 : "嗯 ... 听你这么说, 应该还有其他更好的设计方案?
老王 : "是的, 这次的设计准备用组织索引表的设计, 不过更好倒是谈不上, 两种设计各有千秋"
小张 : "那那个什么组织索引表和堆表的区别是什么呢?"
老王 : "区别就是组织索引表的数据页上数据存储是有序的"
小张 : "王哥你真的不是在逗我嘛, 最开始用田字格本的例子和我说有序的存储会需要擦掉后面的数据的不就是你嘛!"
老王 : "注意我说的是页上的数据是有序的, 但是页本身可以是乱序的"
小张 : "我这更糊涂了, 王哥你给举个例子吧"
老王 : "那就还是刚才那个乱序插入 1 3 5 2 4 6 的例子吧, 给你画个图"
老王 : "我们来看第一个图, 这是插入完1 3 5 三条数据的时候的结构"
小张 : "嗯, 看起来和之前那个没什么区别, 不过这个为什么索引页只有两条呢"
老王 : "由于数据页的内容都是有序的, 所以只需要标出来每一页第一行数据的索引值就可以了, 比如如果要找 id 为 3 的数据, 先看索引页, 发现 3 < 5 所以就去查看页1就可以了, 如果页1里没有这条数据, 就说明这条数据不存在"
小张 : "嗯, 懂了"
老王 : "现在看第二张图, 这是插入完 id 为 2 的数据的结构, 由于 id 为 2 的数据小于5所以需要插在页1中, 但是页1已经满了, 所以只能将页1从中间分开成两个页, 所以 id 为 1 的数据被留在了原来的页, 2 和 3 被划分到了一个新的页里, 虽然 3 被从页1移动到了页3, 但是后续的数据,比如页2并没有受到影响, 所以说数据移动的开销在可控制的范围内"
小张 : "哦, 那第三个图就是 id 为 4 的数据被插入, 由于页3满了所以新开了一个页4, 然后 id=6 的数据由于页2没有满就直接被放在了页2上?"
老王 : "完全正确"
小张 : "那还有个问题我一至没问, 这个索引本身也是个页, 如果索引页满了怎么办呢?"
老王 : "小伙子, 你知道B+树嘛?"
小张 : "哦, 我明白了, 这个图就是一层的B+树!"
老王 : "行, 还算有点脑子, 索引页满了也需要分裂, 本质上和数据页没什么区别, 所以索引页也不一定是连续的, 实际情况中索引页和数据页往往混在一起, 全靠页指针进行查找, 还有数据页之间应该是有指向下一个页和上一个页的指针的, 不过这里我没画出来, 看看B+树你就明白了"
小张 : "那还有个问题, 就是这个方案好多页都没塞满, 那这些空间不就浪费了吗?"
老王 : "对, 就是浪费了, 这个没有什么好的解决办法, 而且乱序插入会频繁的触发页的分裂, 不光更耗费空间, 性能也会差一些, 所以你知道为什么DBA推荐使用递增主键了吧?"
小张 : "对了, 我看最开始那个User表, name 是个辅助索引吧, 辅助索引怎么设计啊, 也是B+树嘛"
老王 : "嗯, 也是B+树, 但是叶子节点咱就别指向数据页了, 直接指向主键, 也是就 id 吧"
小张 : "那查询的时候不就需要先查辅助索引找到主键, 然后需要按照主键再查一遍了吗"
老王 : "你别光想着查询啊, 你别忘了我们这个设计是以主键id为维度的组织索引表, 看看我刚才画的图, 插入 id 为 2 的数据的时候是不是发生了页的分裂, 从而导致 id 为 3 的数据被移动到了页3? 我这里举的例子一页只有两条数据还好, 按照正常情况一页可能几百甚至几千条数据, 我去, 那页一旦分裂, 还要把所有移动的数据的辅助索引指向的数据页都改一遍, 那你可能最坏情况下插一条数据几十秒, 这谁受得了啊"
小张 : "哦哦, 有道理, 文件结构我弄懂了, 接下来呢?"
老王 : "接下来还没想好呢, 今儿先到这儿吧, 回头我再想想, 走, 吃饭去!"