一、字符集
- 字符集:某个字符范围的编码规则
- 将一个字符映射成一个二进制数据的过程也叫做编码,将一个二进制数据映射到一个字符的过程叫做解码
- 常用字符集
- ASCII字符集:128个字符,包括空格、标点符号、数字、大小写字母和一些不可见字符。1个字节。
- ISO 8859-1字符集:共收录256个字符,是在ASCII字符集的基础上又扩充了128个西欧常用字符。1个字节。
- ASCII字符集:128个字符,包括空格、标点符号、数字、大小写字母和一些不可见字符。1个字节。
- GB2312字符集:其中收录汉字6763个,其他文字符号682个。如果该字符在ASCII字符集中,则采用1字节编码。否则采用2字节编码。称为变长编码方式
- GBK字符集:GBK字符集只是在收录字符范围上对GB2312字符集作了扩充。
- utf8字符集:采用变长编码方式,编码一个字符需要使用1~4个字节。
- utf8mb3:阉割过的utf8字符集,只使用1~3个字节表示字符。utf8mb4:正宗的utf8字符集,使用1~4个字节表示字符。
| 字符集名称 | Maxlen |
|---|---|
| ascii | 1 |
| latin1 | 1 |
| gb2312 | 2 |
| gbk | 2 |
| utf8 | 3 |
| utf8mb4 | 4 |
二、InnoDB记录结构
1. InnoDB采取的方式是:将数据划分为若干个页,以页作为磁盘和内存之间交互的基本单位,InnoDB中页的大小一般为 16 KB
2. 行格式
COMPACT行格式
- 记录的额外信息
- 变长字段长度列表:把所有变长字段的真实数据占用的字节长度都存放在记录的开头部位,从而形成一个变长字段长度列表。VARCHAR(M)、VARBINARY(M)、各种TEXT类型
- NULL值列表:每个允许存储NULL的列对应一个二进制位。二进制位值为1代表该列的值为NULL
- 记录头信息:标记删除位、当前记录在记录堆的位置信息、下一条记录的相对位置等
- 真实数据
- MySQL会为每个记录默认的添加一些列(也称为隐藏列):row_id(行ID,唯一标识一条记录)、transaction_id(事务ID)、roll_pointer(回滚指针)
- char除真实数据以外的字节的统统都用空格字符填充
- 对于 CHAR(M) 类型的列来说,当列采用的是定长字符集时,该列占用的字节数不会被加到变长字段长度列表,而如果采用变长字符集时,该列占用的字节数也会被加到变长字段长度列表。
Redundant行格式
- Redundant行格式会把该条记录中所有列(包括隐藏列)的长度信息都按照逆序存储到字段长度偏移列表。按照两个相邻数值的差值来计算各个列值的长度
- 该列对应的偏移量的NULL比特位是不是为1,如果为1,那么该列的值就是NULL
- Redundant行格式中十分干脆,不管该列使用的字符集是什么,只要是使用CHAR(M)类型,占用的真实数据空间就是该字符集表示一个字符最多需要的字节数和M的乘积。
行溢出
- 存储一个VARCHAR(M)类型的列,其实需要占用3部分存储空间:
- 真实数据
- 真实数据占用字节的长度
- NULL值标识,如果该列有NOT NULL属性则可以没有这部分存储空间
- 如果VARCHAR(M)类型的列使用的不是ascii字符集,那M的最大取值取决于该字符集表示一个字符最多需要的字节数。
- 在Compact和Reduntant行格式中,对于占用存储空间非常大的列,在记录的真实数据处只会存储该列的一部分数据,把剩余的数据分散存储在几个其他的页中,然后记录的真实数据处用20个字节存储指向这些页的地址(当然这20个字节中还包括这些分散在其他页面中的数据的占用的字节数)
- MySQL中规定一个页中至少存放两行记录
Dynamic和Compressed行格式
- 在处理行溢出数据时有点儿分歧,它们不会在记录的真实数据处存储字段真实数据的前768个字节,而是把所有的字节都存储到其他页面中,只在记录的真实数据处存储其他页面的地址
- Compressed行格式和Dynamic不同的一点是,Compressed行格式会采用压缩算法对页面进行压缩,以节省空间。
三、InnoDB数据页结构
简介
- 页是InnoDB管理存储空间的基本单位,一个页的大小一般是16KB。存放记录的页为索引(INDEX)页
| 名称 | 中文名 | 占用空间大小 | 简单描述 |
|---|---|---|---|
| File Header | 文件头部 | 38字节 | 页的一些通用信息 |
| Page Header | 页面头部 | 56字节 | 数据页专有的一些信息 |
| Infimum + Supremum | 最小记录和最大记录 | 26字节 | 两个虚拟的行记录 |
| User Records | 用户记录 | 不确定 | 实际存储的行记录内容 |
| Free Space | 空闲空间 | 不确定 | 页中尚未使用的空间 |
| Page Directory | 页面目录 | 不确定 | 页中的某些记录的相对位置 |
| File Trailer | 文件尾部 | 8字节 | 校验页是否完整 |
Records
- 存储的记录会按照我们指定的行格式存储到User Records部分
- 所有被删除掉的记录都会组成一个所谓的垃圾链表,在这个链表中的记录占用的空间称之为所谓的可重用空间,之后如果有新记录插入到表中的话,可能把这些被删除的记录占用的存储空间覆盖掉。
Page Directory(页目录)
- 将所有正常的记录(包括最大和最小记录,不包括标记为已删除的记录)划分为几个组。
- 每个组的最后一条记录(也就是组内最大的那条记录)的头信息中的n_owned属性表示该记录拥有多少条记录,也就是该组内共有几条记录。
- 将每个组的最后一条记录的地址偏移量单独提取出来按顺序存储到靠近页的尾部的地方,这个地方就是所谓的Page Directory,也就是页目录(此时应该返回头看看页面各个部分的图)。页面目录中的这些地址偏移量被称为槽(英文名:Slot),所以这个页面目录就是由槽组成的。.
- 对于最小记录所在的分组只能有 1 条记录,最大记录所在的分组拥有的记录条数只能在 1-8 条之间,剩下的分组中记录的条数范围只能在是 4-8 条之间。所以分组是按照下面的步骤进行的:
- 初始情况下一个数据页里只有最小记录和最大记录两条记录,它们分属于两个分组。
- 之后每插入一条记录,都会从页目录中找到主键值比本记录的主键值大并且差值最小的槽,然后把该槽对应的记录的n_owned值加1,表示本组内又添加了一条记录,直到该组中的记录数等于8个。
- 在一个组中的记录数等于8个后再插入一条记录时,会将组中的记录拆分成两个组,一个组中4条记录,另一个5条记录。这个过程会在页目录中新增一个槽来记录这个新增分组中最大的那条记录的偏移量。
- 一个数据页中查找指定主键值的记录的过程分为两步:
- 通过二分法确定该记录所在的槽,并找到该槽中主键值最小的那条记录。
- 通过记录的next_record属性遍历该槽所在的组中的各个记录。
Page Header(页面头部)
| 名称 | 占用空间大小 | 描述 |
|---|---|---|
| PAGE_N_DIR_SLOTS | 2字节 | 在页目录中的槽数量 |
| PAGE_HEAP_TOP | 2字节 | 还未使用的空间最小地址,也就是说从该地址之后就是Free Space |
| PAGE_N_HEAP | 2字节 | 本页中的记录的数量(包括最小和最大记录以及标记为删除的记录) |
| PAGE_FREE | 2字节 | 第一个已经标记为删除的记录地址(各个已删除的记录通过next_record也会组成一个单链表,这个单链表中的记录可以被重新利用) |
| PAGE_GARBAGE | 2字节 | 已删除记录占用的字节数 |
| PAGE_LAST_INSERT | 2字节 | 最后插入记录的位置 |
| PAGE_DIRECTION | 2字节 | 记录插入的方向 |
| PAGE_N_DIRECTION | 2字节 | 一个方向连续插入的记录数量 |
| PAGE_N_RECS | 2字节 | 该页中记录的数量(不包括最小和最大记录以及被标记为删除的记录) |
| PAGE_MAX_TRX_ID | 8字节 | 修改当前页的最大事务ID,该值仅在二级索引中定义 |
| PAGE_LEVEL | 2字节 | 当前页在B+树中所处的层级 |
| PAGE_INDEX_ID | 8字节 | 索引ID,表示当前页属于哪个索引 |
| PAGE_BTR_SEG_LEAF | 10字节 | B+树叶子段的头部信息,仅在B+树的Root页定义 |
| PAGE_BTR_SEG_TOP | 10字节 | B+树非叶子段的头部信息,仅在B+树的Root页定义 |
File Header(文件头部)
| 名称 | 占用空间大小 | 描述 |
|---|---|---|
| FIL_PAGE_SPACE_OR_CHKSUM | 4字节 | 页的校验和(checksum值) |
| FIL_PAGE_OFFSET | 4字节 | 页号,InnoDB通过页号来可以唯一定位一个页 |
| FIL_PAGE_PREV | 4字节 | 上一个页的页号 |
| FIL_PAGE_NEXT | 4字节 | 下一个页的页号 |
| FIL_PAGE_LSN | 8字节 | 页面被最后修改时对应的日志序列位置(英文名是:Log Sequence Number) |
| FIL_PAGE_TYPE | 2字节 | 该页的类型 |
| FIL_PAGE_FILE_FLUSH_LSN | 8字节 | 仅在系统表空间的一个页中定义,代表文件至少被刷新到了对应的LSN值 |
| FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID | 4字节 | 页属于哪个表空间 |
File Trailer
如果该页中的数据在内存中被修改了,那么在修改后的某个时间需要把数据同步到磁盘中
- 前4个字节代表页的校验和
这个部分是和File Header中的校验和相对应的。每当一个页面在内存中修改了,在同步之前就要把它的校验和算出来,因为File Header在页面的前面,所以校验和会被首先同步到磁盘,当完全写完时,校验和也会被写到页的尾部,如果完全同步成功,则页的首部和尾部的校验和应该是一致的。如果写了一半儿断电了,那么在File Header中的校验和就代表着已经修改过的页,而在File Trialer中的校验和代表着原先的页,二者不同则意味着同步中间出了错。
- 后4个字节代表页面被最后修改时对应的日志序列位置(LSN)
四、B+树索引
目录项记录
- 目录项记录的record_type值是1,而普通用户记录的record_type值是0。
- 目录项记录只有主键值和页的编号两个列,而普通的用户记录的列是用户自己定义的,可能包含很多列,另外还有InnoDB自己添加的隐藏列。
- min_rec_mask的属性,只有在存储目录项记录的页中的主键值最小的目录项记录的min_rec_mask值为1,其他别的记录的min_rec_mask值都是0。
聚簇索引
- 使用记录主键值的大小进行记录和页的排序,这包括三个方面的含义:
- 页内的记录是按照主键的大小顺序排成一个单向链表。
- 各个存放用户记录的页也是根据页中用户记录的主键大小顺序排成一个双向链表。
- 存放目录项记录的页分为不同的层次,在同一层次中的页也是根据页中目录项记录的主键大小顺序排成一个双向链表。
- B+树的叶子节点存储的是完整的用户记录。
二级索引
- 使用记录索引列的大小进行记录和页的排序
- B+树的叶子节点存储的并不是完整的用户记录,而只是索引列+主键这两个列的值。
- 目录项记录中不再是主键+页号的搭配,而变成了索引列+页号的搭配。
- 我们想根据索引列的值查找到完整的用户记录的话,仍然需要到聚簇索引中再查一遍,这个过程也被称为回表
联合索引
- 每条目录项记录都由c2、c3、页号这三个部分组成,各条记录先按照c2列的值进行排序,如果记录的c2列相同,则按照c3列的值进行排序。
- B+树叶子节点处的用户记录由c2、c3和主键c1列组成
B+树索引的注意事项
- 根页面万年不动窝
- 最开始表中没有数据的时候,每个B+树索引对应的根节点中既没有用户记录,也没有目录项记录。
- 随后向表中插入用户记录时,先把用户记录存储到这个根节点中
- 当根节点中的可用空间用完时继续插入记录,此时会将根节点中的所有记录复制到一个新分配的页,比如页a中,然后对这个新页进行页分裂的操作,得到另一个新页,比如页b。这时新插入的记录根据键值(也就是聚簇索引中的主键值,二级索引中对应的索引列的值)的大小就会被分配到页a或者页b中,而根节点便升级为存储目录项记录的页。
- 对某个表建立一个索引,那么它的根节点的页号便会被记录到某个地方,然后凡是InnoDB存储引擎需要用到这个索引的时候,都会从那个固定的地方取出根节点的页号,从而来访问这个索引。
- 内节点中目录项记录的唯一性
- 把主键值也添加到二级索引内节点中的目录项记录了,这样就能保证B+树每一层节点中各条目录项记录除页号这个字段外是唯一的
- 如果c2列的值相同的话,可以接着比较主键值,因为B+树同一层中不同目录项记录的c2列 + 主键的值肯定是不一样的
- 一个页面最少存储2条记录
- MyISAM中的索引方案
- 将表中的记录按照记录的插入顺序单独存储在一个文件中,称之为数据文件
- 使用MyISAM存储引擎的表会把索引信息另外存储到一个称为索引文件的另一个文件中。MyISAM会单独为表的主键创建一个索引,只不过在索引的叶子节点中存储的不是完整的用户记录,而是主键值 + 行号的组合。也就是先通过索引找到对应的行号,再通过行号去找对应的记录!
- MyISAM中建立的索引相当于全部都是二级索引
五、B+树索引的使用
索引的代价
- 空间上的代价。每建立一个索引都要为它建立一棵B+树,每一棵B+树的每一个节点都是一个数据页
- 时间上的代价。每次对表中的数据进行增、删、改操作时,都需要去修改各个B+树索引。
要想使用索引进行排序操作,必须保证索引列是以单独列的形式出现,而不是修饰过的形式
挑选索引
- 只为用于搜索、排序或分组的列创建索引
- 考虑列的基数
列的基数指的是某一列中不重复数据的个数。在记录行数一定的情况下,列的基数越大,该列中的值越分散,列的基数越小,该列中的值越集中。最好为那些列的基数大的列建立索引,为基数太小列的建立索引效果可能不好。
- 索引列的类型尽量小
在表示的整数范围允许的情况下,尽量让索引列使用较小的类型,能使用INT就不要使用BIGINT
- 数据类型越小,在查询时进行的比较操作越快
- 数据类型越小,索引占用的存储空间就越少,在一个数据页内就可以放下更多的记录,从而减少磁盘I/O带来的性能损耗,也就意味着可以把更多的数据页缓存在内存中,从而加快读写效率。
- 索引字符串值的前缀
- 主键插入顺序
页面分裂和记录移位意味着:性能损耗
- 冗余和重复索引
六、InnoDB的表空间
区(extent)的概念
对于16KB的页来说,连续的64个页就是一个区,也就是说一个区默认占用1MB空间大小。每256个区被划分成一组。
段(segment)的概念
- 引入区(extent)为了尽可能地消除随机IO
- 存放叶子节点的区的集合就算是一个段(segment),存放非叶子节点的区的集合也算是一个段。也就是说一个索引会生成2个段,一个叶子节点段,一个非叶子节点段。
七、单表访问方法
const
通过主键或者唯一二级索引列与常数的等值比较来定位一条记录
ref
采用普通二级索引来执行查询的访问方法称为:ref
range
利用索引进行范围匹配的访问方法称之为:range
index
采用全部遍历二级索引记录的执行方式称之为:index
all
全表扫描
注意事项
- 一般情况下只能利用单个二级索引执行查询,优化器一般会根据表的统计数据来判断到底使用哪个条件到对应的二级索引中查询扫描的行数会更少,选择那个扫描行数较少的条件到对应的二级索引中查询
八、连接
连接的概念
- 对于内连接的两个表,驱动表中的记录在被驱动表中找不到匹配的记录,该记录不会加入到最后的结果集。
- 对于外连接的两个表,驱动表中的记录即使在被驱动表中没有匹配的记录,也仍然需要加入到结果集。
- 左外连接:选取左侧的表为驱动表。右外连接:选取右侧的表为驱动表。
- ON子句中的过滤条件
- 对于外连接的驱动表的记录来说,如果无法在被驱动表中找到匹配ON子句中的过滤条件的记录,那么该记录仍然会被加入到结果集中,对应的被驱动表记录的各个字段使用NULL值填充。
- 一般情况下,我们都把只涉及单表的过滤条件放到WHERE子句中,把涉及两表的过滤条件都放到ON子句中,我们也一般把放到ON子句中的过滤条件也称之为连接条件。
连接的原理
- 这种驱动表只访问一次,但被驱动表却可能被多次访问,访问次数取决于对驱动表执行单表查询后的结果集中的记录条数的连接执行方式称之为嵌套循环连接(Nested-Loop Join) ,这是最简单,也是最笨拙的一种连接查询算法。
- 扫描一个表的过程其实是先把这个表从磁盘上加载到内存中,然后从内存中比较匹配条件是否满足。
- join buffer就是执行连接查询前申请的一块固定大小的内存,先把若干条驱动表结果集中的记录装在这个join buffer中,然后开始扫描被驱动表,每一条被驱动表的记录一次性和join buffer中的多条驱动表记录做匹配,因为匹配的过程都是在内存中完成的,所以这样可以显著减少被驱动表的I/O代价。
九、MySQL基于成本的优化
什么是成本
- I/O成本
我们的表经常使用的MyISAM、InnoDB存储引擎都是将数据和索引都存储到磁盘上的,当我们想查询表中的记录时,需要先把数据或者索引加载到内存中然后再操作。这个从磁盘到内存这个加载的过程损耗的时间称之为I/O成本。
- CPU成本
读取以及检测记录是否满足对应的搜索条件、对结果集进行排序等这些操作损耗的时间称之为CPU成本
- 读取一个页面花费的成本默认是1.0,读取以及检测一条记录是否符合搜索条件的成本默认是0.2
基于成本的优化步骤
- 基本步骤
- 根据搜索条件,找出所有可能使用的索引
- 计算全表扫描的代价
- 计算使用不同索引执行查询的代价
- 对比各种执行方案的代价,找出成本最低的那一个
- 不论某个范围区间的二级索引到底占用了多少页面,查询优化器粗暴的认为读取索引的一个范围区间的I/O成本和读取一个页面是相同的。
- 二级索引记录条数:沿着区间最左记录向右读10个页面,计算平均每个页面中包含多少记录,然后用这个平均值乘以区间最左记录和区间最右记录之间的页面数量就可以了。计算页b和页c之间有多少页面就相当于计算它们父节点(也就是页a)中对应的目录项记录之间隔着几条记录。
- 每次回表操作都相当于访问一个页面,也就是说二级索引范围区间有多少记录,就需要进行多少次回表操作,也就是需要进行多少次页面I/O
基于索引统计数据的成本计算
MySQL会为表中的每一个索引维护一份统计数据,使用统计数据来估算这些参数需要单点区间对应的记录条数
| 属性名 | 描述 |
|---|---|
| Table | 索引所属表的名称。 |
| Non_unique | 索引列的值是否是唯一的,聚簇索引和唯一二级索引的该列值为0,普通二级索引该列值为1。 |
| Key_name | 索引的名称。 |
| Seq_in_index | 索引列在索引中的位置,从1开始计数。比如对于联合索引idx_key_part,来说,key_part1、key_part2和key_part3对应的位置分别是1、2、3。 |
| Column_name | 索引列的名称。 |
| Collation | 索引列中的值是按照何种排序方式存放的,值为A时代表升序存放,为NULL时代表降序存放。 |
| Cardinality | 索引列中不重复值的数量。估计值 |
| Sub_part | 对于存储字符串或者字节串的列来说,有时候我们只想对这些串的前n个字符或字节建立索引,这个属性表示的就是那个n值。如果对完整的列建立索引的话,该属性的值就是NULL。 |
| Packed | 索引列如何被压缩,NULL值表示未被压缩。这个属性我们暂时不了解,可以先忽略掉。 |
| Null | 该索引列是否允许存储NULL值。 |
| Index_type | 使用索引的类型,我们最常见的就是BTREE,其实也就是B+树索引。 |
| Comment | 索引列注释信息。 |
| Index_comment | 索引注释信息。 |
连接查询的成本
- 对驱动表进行查询后得到的记录条数称之为驱动表的扇出
- 连接查询的成本计算公式是这样的:
连接查询总成本 = 单次访问驱动表的成本 + 驱动表扇出数 x 单次访问被驱动表的成本
十、Explain详解
| 列名 | 描述 |
|---|---|
| id | 在一个大的查询语句中每个SELECT关键字都对应一个唯一的id |
| select_type | SELECT关键字对应的那个查询的类型 |
| table | 表名 |
| partitions | 匹配的分区信息 |
| type | 针对单表的访问方法 |
| possible_keys | 可能用到的索引 |
| key | 实际上使用的索引 |
| key_len | 实际使用到的索引长度 |
| ref | 当使用索引列等值查询时,与索引列进行等值匹配的对象信息 |
| rows | 预估的需要读取的记录条数 |
| filtered | 某个表经过搜索条件过滤后剩余记录条数的百分比 |
| Extra | 一些额外的信息 |
select_type
| 名称 | 描述 |
|---|---|
| SIMPLE | Simple SELECT (not using UNION or subqueries) |
| PRIMARY | Outermost SELECT |
| UNION | Second or later SELECT statement in a UNION |
| UNION RESULT | Result of a UNION |
| SUBQUERY | First SELECT in subquery |
| DEPENDENT SUBQUERY | First SELECT in subquery, dependent on outer query |
| DEPENDENT UNION | Second or later SELECT statement in a UNION, dependent on outer query |
| DERIVED | Derived table |
| MATERIALIZED | Materialized subquery |
| UNCACHEABLE SUBQUERY | A subquery for which the result cannot be cached and must be re-evaluated for each row of the outer query |
| UNCACHEABLE UNION | The second or later select in a UNION that belongs to an uncacheable subquery (see UNCACHEABLE SUBQUERY) |
Json格式的执行计划
在EXPLAIN单词和真正的查询语句中间加上FORMAT=JSON。包含该计划花费的成本
十一、InnoDB的Buffer Pool
Buffer Pool
- 为了缓存磁盘中的页,在MySQL服务器启动的时候就向操作系统申请了一片连续的内存
- 每个页对应的控制信息占用的一块内存称为一个控制块,控制块和缓存页是一一对应的,它们都被存放到 Buffer Pool 中,其中控制块被存放到 Buffer Pool 的前面,缓存页被存放到 Buffer Pool 后边
free链表的管理
把所有空闲的缓存页对应的控制块作为一个节点放到一个链表中,这个链表也可以被称作free链表
缓存页的哈希处理
可以用表空间号 + 页号作为key,缓存页作为value创建一个哈希表,在需要访问某个页的数据时,先从哈希表中根据表空间号 + 页号看看有没有对应的缓存页,如果有,直接使用该缓存页就好,如果没有,那就从free链表中选一个空闲的缓存页,然后把磁盘中对应的页加载到该缓存页的位置。
flush链表的管理
- 如果我们修改了Buffer Pool中某个缓存页的数据,那它就和磁盘上的页不一致了,这样的缓存页也被称为脏页
- 凡是修改过的缓存页对应的控制块都会作为一个节点加入到一个链表中,因为这个链表节点对应的缓存页都是需要被刷新到磁盘上的,所以也叫flush链表
LRU链表的管理
简单的LRU链表
- Least Recently Used
- 只要我们使用到某个缓存页,就把该缓存页调整到LRU链表的头部,这样LRU链表尾部就是最近最少使用的缓存页喽
划分区域的LRU链表
- 所谓预读,就是InnoDB认为执行当前的请求可能之后会读取某些页面,就预先把它们加载到Buffer Pool中
- 加载到Buffer Pool中的页不一定被用到。如果非常多的使用频率偏低的页被同时加载到Buffer Pool时,可能会把那些使用频率非常高的页从Buffer Pool中淘汰掉
- 把LRU链表按照一定比例分成两截,分别是:
- 一部分存储使用频率非常高的缓存页,所以这一部分链表也叫做热数据,或者称young区域。
- 另一部分存储使用频率不是很高的缓存页,所以这一部分链表也叫做冷数据,或者称old区域。
- 按照某个比例将LRU链表分成两半,随着程序的运行,某个节点所属的区域也可能发生变化。
- 当磁盘上的某个页面在初次加载到Buffer Pool中的某个缓存页时,该缓存页对应的控制块会被放到old区域的头部。
- 在对某个处在old区域的缓存页进行第一次访问时就在它对应的控制块中记录下来这个访问时间,如果后续的访问时间与第一次访问的时间在某个时间间隔内,那么该页面就不会被从old区域移动到young区域的头部,否则将它移动到young区域的头部。
更进一步优化LRU链表
只有被访问的缓存页位于young区域的1/4的后边,才会被移动到LRU链表头部,这样就可以降低调整LRU链表的频率,从而提升性能
刷新脏页到磁盘
- 从LRU链表的冷数据中刷新一部分页面到磁盘。
- 从flush链表中刷新一部分页面到磁盘。
多个Buffer Pool实例
- 在Buffer Pool特别大的时候,我们可以把它们拆分成若干个小的Buffer Pool,每个Buffer Pool都称为一个实例
- 当innodb_buffer_pool_size的值小于1G的时候设置多个实例是无效的,InnoDB会默认把innodb_buffer_pool_instances 的值修改为1
十二、事务简介
事务是一个抽象的概念,它其实对应着一个或多个数据库操作
原子性(Atomicity)
要么全做,要么全不做的规则称之为原子性
隔离性(Isolation)
隔离性(isolation)指的是不同事务在提交的时候,最终呈现出来的效果是串行的,换句话说,既是不同事务,按照提交的先后顺序执行。数据库对多事务并发执行的一个控制,描述了多个事务并发执行时,事务与事务之间操作数据的可见性。
一致性(Consistency)
保证事务只能把数据库从一个有效(正确)的状态“转移”到另一个有效(正确)的状态。譬如,银行系统内部转账转来转去,金额总和是不变的,也没有负数余额的账户等等。数据库某些操作的原子性和隔离性都是保证一致性的一种手段,在操作执行完成后保证符合所有既定的约束则是一种结果。
持久性(Durability)
当现实世界的一个状态转换完成后,这个转换的结果将永久的保留,这个规则称为持久性
十三、redo日志
redo日志格式
redo日志占用的空间少,顺序写
- type:该条redo日志的类型。
- space ID:表空间ID。
- page number:页号。
- data:该条redo日志的具体内容。
row_id隐藏列赋值的方式
- 服务器会在内存中维护一个全局变量,每当向某个包含隐藏的row_id列的表中插入一条记录时,就会把该变量的值当作新记录的row_id列的值,并且把该变量自增1。
- 每当这个变量的值为256的倍数时,就会将该变量的值刷新到系统表空间的页号为7的页面中一个称之为Max Row ID的属性处(我们前面介绍表空间结构时详细说过)。
- 当系统启动时,会将上面提到的Max Row ID属性加载到内存中,将该值加上256之后赋值给我们前面提到的全局变量(因为在上次关机时该全局变量的值可能大于Max Row ID属性值)。
redo日志刷盘时机
- log buffer空间不足时
- 事务提交时
- 后台线程,大约每秒都会刷新一次log buffer中的redo日志到磁盘
- 正常关闭服务器时
checkpoint
- flush链表中的脏页按照修改发生的时间顺序进行排序,也就是按照oldest_modification代表的LSN值进行排序,被多次更新的页面不会重复插入到flush链表中,但是会更新newest_modification属性的值。
- 判断某些redo日志占用的磁盘空间是否可以覆盖的依据就是它对应的脏页是否已经刷新到磁盘里。
- 凡是在系统lsn值小于该节点的oldest_modification值时产生的redo日志都是可以被覆盖掉的
十四、undo日志
undo日志的格式
为了实现事务的原子性,InnoDB存储引擎在实际进行增、删、改一条记录时,都需要先把对应的undo日志记下来。
十五、事务的隔离级别与MVCC
事务并发执行遇到的问题
- 脏写(Dirty Write)。如果一个事务修改了另一个未提交事务修改过的数据,那就意味着发生了脏写。
- 脏读(Dirty Read)。如果一个事务读到了另一个未提交事务修改过的数据,那就意味着发生了脏读
- 不可重复读(Non-Repeatable Read)。 如果一个事务只能读到另一个已经提交的事务修改过的数据,并且其他事务每对该数据进行一次修改并提交后,该事务都能查询得到最新值,那就意味着发生了不可重复读
- 幻读(Phantom)如果一个事务先根据某些条件查询出一些记录,之后另一个事务又向表中插入了符合这些条件的记录,原先的事务再次按照该条件查询时,能把另一个事务插入的记录也读出来,那就意味着发生了幻读
四个隔离级别
- 未提交读(read uncommitted) :读取其他事务还未提交的数据,也就是脏读
- 提交读(read committed) :读取其他事务已经提交的数据。但是会出现的问题是不可重复读,事务A select了一条数据,接着事务B update 这条数据,然后commit,这时候A还未提交,A再回来读这条数据,发现数据居然变了
- 可重复读(repeatable read) :在上一个级别的基础上,保证不会在一个事务内两次select同一条数据会出现变化。事务A开启事务,并select一段有范围的数据,然后事务B开启事务,在先前A事务select的那段有范围的数据中insert一条数据,然后提交事务,接着事务A再select出来这段数据,发现数据多了一条,这种情况叫幻读。
- 序列化读(serializable) :这也就是最高级别,保证事务之间不会有任何踩踏,每个事务都可以认为只有它自己在操作数据库。
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED | Possible | Possible | Possible |
| READ COMMITTED | Not Possible | Possible | Possible |
| REPEATABLE READ | Not Possible | Not Possible | Possible |
| SERIALIZABLE | Not Possible | Not Possible | Not Possible |
MVCC
MVCC(Multi-Version Concurrency Control ,多版本并发控制)指的就是在使用READ COMMITTD、REPEATABLE READ这两种隔离级别的事务在执行普通的SEELCT操作时访问记录的版本链的过程,这样子可以使不同事务的读-写、写-读操作并发执行,从而提升系统性能。READ COMMITTD、REPEATABLE READ这两个隔离级别的一个很大不同就是:生成ReadView的时机不同,READ COMMITTD在每一次进行普通SELECT操作前都会生成一个ReadView,而REPEATABLE READ只在第一次进行普通SELECT操作前生成一个ReadView,之后的查询操作都重复使用这个ReadView就好了。
十六、锁
共享锁和独占锁
- 共享锁,简称S锁。在事务要读取一条记录时,需要先获取该记录的S锁。
- 独占锁,也常称排他锁,简称X锁。在事务要改动一条记录时,需要先获取该记录的X锁。
假如事务T1首先获取了一条记录的S锁之后,事务T2接着也要访问这条记录:
如果事务T2想要再获取一个记录的S锁,那么事务T2也会获得该锁,也就意味着事务T1和T2在该记录上同时持有S锁。如果事务T2想要再获取一个记录的X锁,那么此操作会被阻塞,直到事务T1提交之后将S锁释放掉。如果事务T1首先获取了一条记录的X锁之后,那么不管事务T2接着想获取该记录的S锁还是X锁都会被阻塞,直到事务T1提交。
多粒度锁
- 给表加S锁
- 别的事务可以继续获得该表的S锁
- 别的事务可以继续获得该表中的某些记录的S锁
- 别的事务不可以继续获得该表的X锁
- 别的事务不可以继续获得该表中的某些记录的X锁
- 给表加X锁
- 别的事务不可以继续获得该表的S锁
- 别的事务不可以继续获得该表中的某些记录的S锁
- 别的事务不可以继续获得该表的X锁
- 别的事务不可以继续获得该表中的某些记录的X锁