innodb是如何存数据的?yyds
前言
如果你使用过mysql数据库,对它的存储引擎:innodb
,一定不会感到陌生。
众所周知,在mysql8以前,默认的存储引擎是:myslam。但mysql8之后,默认的存储引擎已经变成了:innodb,它是我们建表的首选存储引擎。
那么,问题来了:
- innodb的底层是如何存储数据的?
- 表中有哪些隐藏列?
- 用户记录之间是如何关联起来的?
如果你想知道上面三个问题的答案,那么,请继续往下面看。
本文主要包含如下内容:
1.磁盘or内存?
1.1 磁盘
数据对系统来说是非常重要的东西,比如:用户的身份证、手机号、银行号、会员过期时间、积分等等。一旦丢失,会对用户造成很大的影响。
那么问题来了,如何才能保证这些重要的数据不丢呢?
答案:把数据存在磁盘上。
当然有人会说,如果磁盘坏了怎么办?
那就需要备份,或者做主从了。。。
好了,打住,这不是今天的重点。
言归正传。
大家都知道,从磁盘上读写数据,至少需要两次IO请求才能完成。一次是读IO,另一次是写IO。
而IO请求是比较耗时的操作,如果频繁的进行IO请求势必会影响数据库的性能。
那么,如何才能解决数据库的性能问题呢?
1.2 内存
把数据存在寄存器?
没错,操作系统从寄存器中读取数据是最快的,因为它离CPU最近。
但是寄存器有个非常致命的问题是:它只能存储非常少量的数据,设计它的目的主要是用来暂存指令和地址,并非存储大量用户数据的。
这样看来,只能把数据存在内存中了。
因为内存同样能满足我们,快速读取和写入数据的需求,而且性能是非常可观的,只是比较寄存器稍稍慢了一丢丢而已。
不过有个让人讨厌的地方是,内存相对于磁盘来说,是更加昂贵的资源。通常情况下,500G或者1T的磁盘,是很常见的。但你有听说过有500G的内存吗?别人会以为你疯了。内存大小讨论的数量级一般是16G或32G。
内存可以存储一些用户数据,但无法存储所有的用户数据,因为如果数据量太大了,它可能还是存不下。
此外,即使用户数据能刚好存在内存,以后万一有一天,数据库服务器或者部署节点挂了,或者重启了,数据不就丢了?
怎么做,才能不会因为异常情况,而丢数据。同时,又能保证数据的读写速度呢?
2.数据页
我们可以把一批数据放在一起。
写操作时,先将数据写到内存的某个批次中,然后再将该批次的数据一次性刷到磁盘上。如下图所示:
读操作时,从磁盘上一次读一批数据,然后加载到内存当中,以后就在内存中操作。如下图所示:
将内存中的数据刷到磁盘,或者将磁盘中的数据加载到内存,都是以批次为单位,这个批次就是我们常说的:数据页
。
当然innodb中存在多种不同类型的页,数据页只是其中一种,我们在这里重点介绍一下数据页。
那么问题来了,什么是数据页?
数据页主要是用来存储表中记录的,它在磁盘中是用双向链表相连的,方便查找,能够非常快速得从一个数据页,定位到另一个数据页。
很多时候,由于我们表中的数据比较多,在磁盘中可能存放在多个数据页当中。
有一天,我们要根据某个条件查询数据时,需要从一个数据页找到另一个数据页,这时候的双向链表就派上大用场了。磁盘中各数据页的整体结构如下图所示:
通常情况下,单个数据页默认的大小是16kb
。当然,我们也可以通过参数:innodb_page_size
,来重新设置大小。不过,一般情况下,用它的默认值就够了。
好吧,数据页的整体结构已经搞明白了。
那么,单个数据页包含哪些内容呢?
从上图中可以看出,数据页主要包含如下几个部分:
- 文件头部
- 页头部
- 最大和最小记录
- 用户记录
- 空闲空间
- 页目录
- 文件尾部
3.用户记录
对于新申请的数据页,用户记录是空的。当插入数据时,innodb会将一部分空闲空间
分配给用户记录。
用户记录是innodb的重中之重,我们平时保存到数据库中的数据,就存储在它里面。那么,它里面又包含哪些内容呢?你不好奇吗?
其实在innodb支持的数据行格式有四种:
- compact行格式
- redundant行格式
- dynamic行格式
- compressed行格式
我们以compact行格式为例:
一条用户记录主要包含三部分内容:
- 记录额外信息,它包含了变长字段、null值列表和记录头信息。
- 隐藏列,它包含了行id、事务id和回滚点。
- 真正的数据列,包含真正的用户数据,可以有很多列。
下面让我们一起了解一下这些内容。
3.1 额外信息
额外信息并非真正的用户数据,它是为了辅助存数据用的。
3.1.1 变长字段列表
有些数据如果直接存会有问题,比如:如果某个字段是varchar或text类型,它的长度不固定,可以根据存入数据的长度不同,而随之变化。
如果不在一个地方记录数据真正的长度,innodb很可能不知道要分配多少空间。假如都按某个固定长度分配空间,但实际数据又没占多少空间,岂不是会浪费?
所以,需要在变长字段中记录某个变长字段占用的字节数,方便按需分配空间。
3.1.2 null值列表
数据库中有些字段的值允许为null,如果把每个字段的null值,都保存到用户记录中,显然有些浪费存储空间。
有没有办法只简单的标记一下,不存储实际的null值呢?
答案:将为null的字段保存到null值列表。
在列表中用二进制的值1,表示该字段允许为null,用0表示不允许为null。它只占用了1位,就能表示某个字符是否为null,确实可以节省很多存储空间。
3.1.3 记录头信息
记录头信息用于描述一些特殊的属性。
它主要包含:
- deleted_flag: 即删除标记,用于标记该记录是否被删除了。
- min_rec_flag: 即最小目录标记,它是非叶子节点中的最小目录标记。
- n_owned:即拥有的记录数,记录该组索引记录的条数。
- heap_no:即堆上的位置,它表示当前记录在堆上的位置。
- record_type:即记录类型,其中:0表示普通记录,1表示非叶子节点,2表示Infrimum记录, 3表示Supremum记录。
- next_record:即下一条记录的位置。
3.2 隐藏列
数据库在保存一条用户记录时,会自动创建一些隐藏列。如下图所示:
目前innodb自动创建的隐藏列有三种:
- db_row_id,即行id,它是一条记录的唯一标识。
- db_trx_id,即事务id,它是事务的唯一标识。
- db_roll_ptr,即回滚点,它用于事务回滚。
如果表中有主键,则用主键做行id,无需额外创建。如果表中没有主键,假如有不为null的unique唯一键,则用它做为行id,同样无需额外创建。
如果表中既没有主键,又没有唯一键,则数据库会自动创建行id。
也就是说在innodb中,隐藏列中事务id
和回滚点
是一定会被创建的,但行id要根据实际情况决定。
3.3 真正数据列
真正的数据列中存储了用户的真实数据,它可以包含很多列的数据。这个比较简单,没有什么好多说的。
3.4 用户记录是如何相连的?
通过上面介绍的内容,大家对一条用户记录是如何存储的,应该有了一定的认识。
但问题来了,一条用户记录和另一条用户记录是如何相连的,innodb是怎么知道,某条记录的下一条记录是谁?
答案是:用前面提到过的, 记录额外信息 》 记录头信息 》下一条记录的位置。
多条用户记录之间通过下一条记录的位置
,组成了一个单向链表。这样就能从前往后,找到所有的记录了。
4.最大和最小记录
从上面可以得知,在一个数据页当中,如果存在多条用户记录,它们是通过下一条记录的位置
相连的。
不过有个问题:如果才能快速找到最大的记录和最小的记录呢?
这就需要在保存用户记录的同时,也保存最大和最小记录了。
最大记录保存到Supremum记录中。
最小记录保存在Infimum记录中。
在保存用户记录时,数据库会自动创建两条额外的记录:Supremum 和 Infimum。它们之间的关系,如下图所示:
从图中可以看出用户数据是从最小记录开始,通过下一条记录的位置,从小到大,一步步查找,最后找到最大记录为止。
5.页目录
从上面可以看出,如果我们要查询某条记录的话,数据库会从最小记录开始,一条条查找所有记录。如果中途找到了,则直接返回该记录。如果一直找到最大记录,还没有找到想要的记录,则返回空。
咋一看,没有问题。
但如果仔细想想。
效率会不会有点低?
这不是要对整页用户数据进行扫描吗?
有没有更高效的方法?
这就需要使用页目录
了。
说白了,就是把一页用户记录分为若干组,每一组的最大记录都保存到一个地方,这个地方就是页目录
。每一组的最大记录叫做槽
。
由此可见,页目录是有多个槽组成的。所下图所示:
假设一页的数据分为4组,这样在页目录中,就对应了4个槽,每个槽中都保存了该组数据的最大值。
这样就能通过二分查找,比较槽中的记录跟需要找到的记录的大小。如果用户需要查找的记录,小于当前槽中的记录,则向上查找上一个槽。如果用户需要查找的记录,大于当前槽中的记录,则向下查找下一个槽。
如此一来,就能通过二分查找,快速的定位需要查找的记录了。
so easy
6.文件头部和尾部
6.1 文件头部
通过前面介绍的行记录中下一条记录的位置
和页目录
,innodb能非常快速的定位某一条记录。但有个前提条件,就是用户记录必须在同一个数据页当中。
如果用户记录非常多,在第一个数据页找不到我们想要的数据,需要到另外一页找该怎么办呢?
这时就需要使用文件头部
了。
它里面包含了多个信息,但我只列出了其中4个最关键的信息:
- 页号
- 上一页页号
- 下一页页号
- 页类型
顾名思义,innodb是通过页号、上一页页号和下一页页号来串联不同数据页的。如下图所示:
不同的数据页之间,通过上一页页号和下一页页号构成了双向链表。这样就能从前向后,一页页查找所有的数据了。
此外,页类型也是一个非常重要的字段,它包含了多种类型,其中比较出名的有:数据页、索引页(目录项页)、溢出页、undo日志页等。
6.2 文件尾部
我之前提过,数据库的数据是以数据页为单位,加载到内存中,如果数据有更新的话,需要刷新到磁盘上。
但如果某一天比较倒霉,程序在刷新到磁盘的过程中,出现了异常,比如:进程被kill掉了,或者服务器被重启了。
这时候数据可能只刷新了一部分,如何判断上次刷盘的数据是完整的呢?
这就需要用到文件尾部
。
它里面记录了页面的校验和
。
在数据刷新到磁盘之前,会先计算一个页面的校验和。后面如果数据有更新的话,会计算一个新值。文件头部中也会记录这个校验和,由于文件头部在前面,会先被刷新到磁盘上。
接下来,刷新用户记录到磁盘的时候,假设刷新了一部分,恰好程序出现异常了。这时,文件尾部的校验和,还是一个旧值。数据库会去校验,文件尾部的校验和,不等于文件头部的新值,说明该数据页的数据是不完整的。
7.页头部
通过上面介绍的内容,数据页之间能够轻松访问了,但剩下还有个比较重要的问题,就是记录的状态信息。
比如一页数据到底保存了多条记录,或者页目录到底使用了多个槽等。这些信息是实时统计,还是事先统计好了,保存到某个地方?
为了性能考虑,上面的这些统计数据,当然是先统计好,保存到一个地方。后面需要用到该数据时,再读取出来会更好。这个保存统计数据的地方,就是页头部
。
当然页头部不仅仅只保存:槽的数量、记录条数等信息。
它还记录了:
- 已删除记录所占的字节数
- 最后插入记录的位置
- 最大事务id
- 索引id
- 索引层级
其实还有很多,在这里就不一一列举了,有兴趣的朋友可以找我私聊。
总结
多个数据页之间通过页号
构成了双向链表。而每一个数据页的行数据之间,又通过下一条记录的位置
构成了单项链表。整体架构图如下:
好了,本文内容先到这里。如果小伙伴们有任何疑问的话,欢迎找我私聊。
顺便预告一下,在innodb的存储结构中,还有一个非常重要的内容没讲,它就是:索引
。敬请期待,我们下期见。
最后说一句(求关注,别白嫖我)
如果这篇文章对您有所帮助,或者有所启发的话,帮忙扫描下发二维码关注一下,您的支持是我坚持写作最大的动力。
求一键三连:点赞、转发、在看。
关注公众号:【苏三说技术】,在公众号中回复:面试、代码神器、开发手册、时间管理有超赞的粉丝福利,另外回复:加群,可以跟很多BAT大厂的前辈交流和学习。
innodb是如何存数据的?yyds的更多相关文章
- Hbase写数据,存数据,读数据的详细过程
Client写入 -> 存入MemStore,一直到MemStore满 -> Flush成一个StoreFile,直至增长到一定阈值 -> 出发Compact合并操作 -> 多 ...
- ElasticSearch 学习记录之 分布式文档存储往ES中存数据和取数据的原理
分布式文档存储 ES分布式特性 屏蔽了分布式系统的复杂性 集群内的原理 垂直扩容和水平扩容 真正的扩容能力是来自于水平扩容–为集群添加更多的节点,并且将负载压力和稳定性分散到这些节点中 ES集群特点 ...
- Package设计1:选择数据类型、暂存数据和并发
SSIS 设计系列: Package设计1:选择数据类型.暂存数据和并发 Package设计2:增量更新 Package 设计3:数据源的提取和使用暂存 一,数据类型的选择 对于SSIS的数据类型,容 ...
- python,java操作mysql数据库,数据引擎设置为myisam时能够插入数据,转为innodb时无法插入数据
今天想给数据库换一个数据引擎,mysiam转为 innodb 结果 python 插入数据时失败,但是自增id值是存在的, 换回mysiam后,又可以插入了~~ 想换php插入试试,结果php数据引擎 ...
- R读数据stringsAsFactors=F,存数据时row.names = F
stringsAsFactors=F 以前在r里读数据,经常把character读成factor,还得费半天劲把它转回来,尤其是把factor转成numeric还没有那么直接.例如: dat< ...
- InnoDB的行溢出数据,Char的行结构存储
行溢出数据 InnoDB存储引擎可以将一条记录中的某些数据存储在真正的数据页面之外,即作为行溢出数据.一般认为BLOB.LOB这类的大对象列类型的存储会把数据存放在数据页面之外.但是,这个理解有点偏差 ...
- Mysql之InnoDB行格式、数据页结构
Mysql架构图 存储引擎负责对表中的数据的进行读取和写入,常用的存储引擎有InnoDB.MyISAM.Memory等,不同的存储引擎有自己的特性,数据在不同存储引擎中存放的格式也是不同的,比如Mem ...
- NSUserDefaults存数据相关的问题
NSUserDefaults存储数据的类型是有限制的!NSUserDefaults里面只能存储property list objects.具体的内容请看下面的链接.特别的,对于NSDictionary ...
- 【Mysql】InnoDB 引擎中的数据页结构
InnoDB 是 mysql 的默认引擎,也是我们最常用的,所以基于 InnoDB,学习页结构.而学习页结构,是为了更好的学习索引. 一.页的简介 页是 InnoDB 管理存储空间的基本单位,一个页的 ...
随机推荐
- 序-WEB方向指南
WEB 这个方向其实是目前从业人员最多的方向,也是学习安全门槛最低的方向,当然也是最容易恰饭的方向. 我从入行到现在也依旧没有脱离它,毕竟在我这个小城市.小圈子里,不干这个好像就要没饭吃了,但是你说它 ...
- ECMAScript6.0
ECMAScript6.0相当于JavaScript的标准,它规定了浏览器脚本语言的标准,发布于2015年,目标是使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言 N ...
- Python基础4--数据类型
一.数据类型是什么鬼? 计算机顾名思义就是可以做数学计算的机器,因此,计算机程序理所当然地可以处理各种数值.但是,计算机能处理的远不止数值,还可以处理文本.图形.音频.视频.网页等各种各样的数据,不同 ...
- 『动善时』JMeter基础 — 55、使用非GUI模式运行JMeter(命令行模式)
目录 1.JMeter的非GUI模式说明 2.为什么使用非GUI模式运行JMeter 3.使用非GUI模式运行JMeter (1)非GUI模式运行JMeter步骤 (2)其它参数说明 4.CLI模式运 ...
- PYTHON 错误提示:ModuleNotFoundError: No module named 'cv2'
ModuleNotFoundError: No module named 'cv2' 解决方法: pip install -i https://pypi.tuna.tsinghua.edu.cn/si ...
- [刘阳Java]_SpringMVC文件上传第1季_第10讲
今天来介绍一个关于SpringMVC框架的文件上传功能.首先我个人感觉SpringMVC框架的文件上传还是要比Struts2框架要好用一些,灵活性更强.因为SpringMVC框架的文件上传有几种不同的 ...
- C++:常量
/** https://www.runoob.com/cplusplus/cpp-constants-literals.html * 常量: 固定值,一旦定义不能被修改 * 整数常量:可以是十进制.八 ...
- JAVA-Scaneer对象
Scanner对象 我们可以通过scanner来获取用户的输入 基本语法 Scanner s = new Scanner(System.in); nextLine():输入 import java.u ...
- 微信小程序账号注册
想要开发微信小程序,先注册账号申请APPID. 第一步:百度搜索"微信公众平台" 第二步:立即注册 进入注册页面 区别: 订阅号: 订阅号在文件夹里,订阅号消息 一天只能推送一次, ...
- mapGetters 的作用__为什么mapGetter前面有...(三个点是去掉{}的作用)
参考:vuex 中关于 mapGetters 的作用 mapGetters 工具函数会将 store 中的 getter 映射到局部计算属性中.它的功能和 mapState 非常类似,我们来直接看它的 ...