MySQL技术内幕--读书笔记

基础:

B树和B+树 参考这里

innodb后台线程

master thread

核心后台线程,负责将数据异步刷新到磁盘,保证数据一致性,包括脏页的刷新、合并插入缓冲

IO thread

负责处理异步IO请求的回调,InnoDB 1.0之前有4个IO Thread,分别是write read insert_buffer 和 log

Purge Thread

事务提交后需要回收undolog,InnoDB 1.1以前,purge操作在Master Thread中进行,1.1版开始,purge操作移到了Purge Thread,InnoDB1.1版本只能配置一个PurgeThread,1.2开始可以配置多个

Page Cleaner Thread

1.2.x版中引入,作用是将之前版本中的脏页的刷新操作放到单独的线程中完成,目的是为了减轻MasterThread工作以及对用户查询线程的阻塞

内存

1 缓冲池

数据库读取页操作时,首先将磁盘读到的页存到缓冲池中,这个过程称为将页“FIX”在缓冲池中,下次再读相同页时,先判断是否在缓冲池中。页从缓冲池刷新回磁盘的操作并不是在每次页发生更新时触发,而是通过一种称为CheckPoint的机制刷新回磁盘。
可以通过配置参数innodb_buffer_pool_size来设置缓冲池大小。
缓冲池中缓存的数据页类型有:索引页、数据页、undo页、插入缓冲、自适应哈希索引、InnoDB存储的锁信息、数据字典信息等。不能简单的认为缓冲池中只是缓存索引页和数据页,它们只是占缓冲池很大的一部分而已。

2 LRU List、 Free List 和 Flush List

数据库的缓冲池是通过LRU算法来进行管理。LRU算法增加了midpoint位置,新读取的页不是放在LRU列表的首部,而是放在midpoint位置,默认情况下midpoint设置为LRU列表长度的5/8处。midpoint位置可以通过innodb_old_blocks_pct(37代表37%)配置。这样操作是为了避免单次的查询大量数据导致LRU数据全部被刷新

以下省略一大堆LRU的优化。。。

3 重做日志缓冲

以下三种情况下会将重做日志缓冲刷新到外部磁盘的重做日志文件中:

  1. Master Thread每秒整一次
  2. 每个事务提交时整一次
  3. 当重做日志缓冲池剩余空间小于1/2时

4 额外的内存池

没看懂,貌似是某些操作需要的内存,需要从额外的内存池中分配。

CheckPoint技术

前提:
1.update或delete操作会改变缓冲池中的记录,此时缓冲池中的页是脏的,需要刷新到磁盘。
2.缓冲池+重做日志用来维系数据的准确

CheckPoint需要解决的问题:

  1. 缩短数据库的恢复时间(重做日志要足够小)
  2. 缓冲池不够用时,将脏页刷新到磁盘
  3. 重做日志不可用时,刷新脏页
    需要CheckPoint的时机:
    数据库宕(dàng)机时, 不需要重做所有日志,因为CheckPoint之前的页都已经刷新回磁盘。
    当缓冲池不够时,回溢出LRU中最近最少使用的页,若此页为脏页,需要强制执行CheckPoint。
    重做日志是循环使用的,当新日志存储的位置的旧日志还没有被刷新时,需要强制CheckPoint

CheckPoint需要决定的是:需要刷新哪些脏页,以及什么时间触发

InnoDB有两种CheckPoint:

  1. Sharp CheckPoint:数据库关闭时刷新所有脏页
  2. Fuzzy CheckPoint:刷新部分页,触发时机为MasterThread定期触发和上面的3个时机

Master Thread工作方式

1.1版本前的工作方式

线程状态有:loop, background loop, flush loop, suspend loop
由多个循环组成:
主循环:

image.png

image.png

即使事务还没有提交,仍会每秒将日志缓冲中的内容刷新到重做日志文件,这也很好的解释了为什么再大的事务提交的时间也很短

后台循环:若当前没有用户活动或者数据库关闭,就会切换到这个循环。
1.删除无用的undo页(总是)
2.合并20个插入缓冲(总是)
3.跳回到主循环(总是)
4.不断刷新100个页直到符合条件(可能,跳转到flash loop中完成)
刷新循环:刷新页
暂停循环:

1.2版本之前的工作方式

主要就是优化了一些参数,以及之前写死的部分参数改为可配置的

1.2版本后

主要就是把刷新操作移到Page Cleaner Thread中了

InnoDB关键特性

Insert Buffer

Insert Buffer和数据页一样,也是物理页的一个组成部分。(啥叫物理页的一个组成部分??我的理解是:一个物理页上会有些数据信息,而这些信息里面除了数据表,也会有InsertBuffer和其他数据)插入数据时,如果索引是辅助索引不唯一,就会使用Insert Buffer,将索引异步同步,以此提高Insert性能

Change Buffer

1.0.x版本开始引入,是InsertBuffer的升级,用来缓冲insert delete update??

Insert Buffer的内部实现

Insert Buffer是一个B+树,4.1及以后的版本,全局只有一个Insert Buffer B+树,负责对所有表的辅助索引更新。所以在通过表的bid文件恢复表数据时可能会失败,因为辅助索引还需要通过全局的Insert Buffer更新,所以还需要进行Repire Table操作重建表的辅助索引。

B+树的非叶子节点中记录了辅助索引所在的以及所在的辅助索引所在的

Insert Buffer Bitmap用来存储辅助索引页的信息包括:
1.是否需要更新(有记录在Insert Buffer中)
2.剩余空间(0无可用,1剩余大于1/32,2剩余大于1/16,3剩余大于1/8)
每个记录占用4个字节,所以每个Insert Buffer Bitmap页可以用来记录16384个辅助索引页的信息(256个区)

Mege Insert Buffer

合并辅助索引的时机:

  1. 辅助索引所在的页被加载到内存中
  2. 辅助索引所在的页剩余空间小于1/32
  3. Master Thread定期刷新

2.6.2 doublewrite

将脏页同步到磁盘时,如果同步了某页的一半时发生宕机,那么通过重做日志无法恢复(因为页被写了一半,相当于被破坏了,而重做日志需要基于原页进行恢复)。
doublewirte中记录了需要刷新的数据(共2M),同时在磁盘上维护2M的备份空间,刷新double的数据时,先把2M内存memcopy到磁盘(分两次,每次1M,然后马上fync同步,因为是连续地址所以开销不大)。然后再把doublewirte中的脏页逐个的进行刷新。
宕机恢复时,先把磁盘的备份空间中的页逐个替换,然后再通过重做日志进行恢复。

2.6.3 自适应哈希(Adaptive Hash Index)

InnoDB通过监控对索引页的查询情况,满足一定规则后,构造AHI来提高查询速度,不过仅对等值查找起作用,对范围查找等无效

2.6.4 异步IO(AIO)

除了异步执行IO操作,AIO还会合并(地址)连续IO操作
Linux 和 Window中可以用Native AIO

2.6.5 刷新邻接页

某个页需要刷新时,如果相邻的页也是脏页,就一起刷新了。
不过两个问题:
相邻的页是频繁改的,而且没那么“脏”,没*必要现在刷新
SSD硬盘IO效率高,无需这个优化

2.7 启动关闭恢复 2.8 小结

感觉不重要,因为猜得到。。。

3 文件

3.1 参数文件

静态参数、动态参数、会话有效参数、生命周期有效参数。。

3.2 日志文件

错误日志

SHOW VARIABLES LIKE 'log_error' \G

慢查询日志

默认不启动,可以通过设置
long_query_time //5.1以后精确到微秒
mysqldumpslow用来查询满查询SQL

查询日志 (略)

二进制日志

记录SELECT 和 SHOW以外的操作
二进制日志主要用来恢复和同步
同步可能会有各种问题,比如宕机时日志还没有刷新到磁盘,比如宕机时事务执行了一半等等,感觉不太重要,懒得看了。。。

socket文件, PID文件

SHOW VARLIABLES LIKE 'socket' \G
SHOW VARLIABLES LIKE 'pid_file' \G

表结构文件

table_name.frm, 也会用来存储视图结构

InnoDB存储引擎文件

3.6.1表空间文件

通过设置innodb_file_per_table可以设置每个表一个单独的文件
单独的表文件存储记录、索引、插入缓冲、BITMAP等数据,其余信息还是放在默认的表空间中

3.6.2 重做日志文件

略。。。。。

索引组织表

根据主键顺序存放
如果表没有主键,使用第一个定义的非空唯一索引,没有非空唯一索引则生成一个6字节的主键

4.2 InnoDB存储结构

表空间 > 段(segment) > 区(extent) > 页(page) > 行(row)

4.2.1 表空间

表空间存储:记录,索引,插入缓冲BitMap页
其他信息存储在共享表空间:回滚信息、插入缓冲页、系统事务信息、二次写缓冲
事务提交以后并不会马上释放共享表空间,而是留着下次重用

4.2.2 段

数据段、索引段、回滚段。
数据段在B+树的叶子节点,索引段是非叶子节点

4.2.3 区

默认每个区1MB,每个页16KB,则一个区包含64个连续的页

4.2.4 页

image.png

InnoDB行记录格式

image.png

变长字段长度列表的记录顺序是逆序的
NULL标志位标明哪些字段的值是NULL(2进制形式)
每行记录中,除了用户定义的字段,还有6字节的事务ID和7字节的回滚指针列,若没有定义主键,还有自动生成的6字节rowid列

VARCHAR列的最大长度并不是65535,因为还有别的开销
VARCHAR(N)中,N标示字符的长度,而65535指的是字节的长度,所以charset=utf8或gbk时,长度也不同

InnoDB需要保证每个页(16KB)中至少有两行记录,所以VARCHAR并不一定会写在页里,有可能只保存了前n位,后面的存放在Uncompressed BLOB Page中。 同理,BLOB 和 Text数据也不是一定存放在BLOB页

4.3.4 Compressed 和 Dynamic 行记录格式

InnoDB 1.0.x版本开始引入了新的文件格式Barracuda,包含Compressed和Dynamic两种格式
BLOB数据会全部保存在BLOB页中,在数据页只存放20个字节的指针

4.3.5 CHAR的行结构存储

Mysql4.1版本开始 char(N)中的N不再标示字节长度,而是字符长度,也就是在不同字符集下,cahr类型的字节长度是不同的。
SELECT a, CHAR_LENGTH(a), LENGTH(a), HEX(a) FROM test \G;
CHAR_LENGTH : 字符长度
LENGTH: 字节长度
HEX: 16进制表示

多字节字符集(utf8、gbk等)中,CHAR和VARCHAR一样被当作变长字符类型处理。(但我不理解的是,对于char(2)中存储的'a',为什么要设置变长为2,然后第二位填0x20??

InnoDB数据页结构

image.png

4.6 约束

set sql_mode='STRICT_TRANS_TABLES' //严格严查约束,不自动转换(error,而不是warning)
哪些是约束:uniq key, not null, enum('a','b') 等等
可以通过触发器创建自定义约束

4.7 视图

视图除了用来查询数据,还可以用来更新数据
Insert 时加上WITH CHECK OPTION,可以在insert0行时报错
视图只是保存一条SQL, 并不包含真实数据,物化视图包含真实数据,mysql不支持物化视图,不过可以通过创建新表配合触发器实现

分区表

水平分区:按行分
垂直分区:按列分

mysql支持水平分区

局部分区索引:分区中既存放数据又存放索引
全局分区索引:分区中存放数据,索引放在一个对象中

mysql使用局部分区索引

如果有唯一索引和主键,分区列必须是其中的一部分。如果没有唯一索引或主键,可以指定任意一列为分区列

RANGE分区,区间分布

image.png
RANGE分区例子

mysql优化器只能对YEAR()、TO_DAYS()//转换成具体的天数、TO_SECONDS()、UNIX_TIMESTAMP()这类函数进行优化

LIST分区, 枚举分布,如:PARTITION BY LIST(b) PARTITION p0 VALUES in(1,3,5,7,9)
如果insert一堆记录时,某个记录的value不在枚举中,MyISAM引擎会将之前的数据插入,后面的不会。InnoDB会将其视为一个事务,直接返回失败。

HASH分区
需要指定分区数量
SQL:CREATE TABLE t_hash(a INT, b DATETIME) ENGINE=InnoDB PARTITION BY HASH(YEAR(b)) PARTITIONS 4;
上面的HASH相当于通过余数分表,LINEAR HASH通过二进制位分表,优点是方便增、删、合并和拆分分区,缺点是分区区间数据可能不均衡

KEY分区
SQL:CREATE TABLE t_hash(a INT, b DATETIME) ENGINE=InnoDB PARTITION BY KEY (b) PARTITIONS 4;
LINEAR KEY 类似 LINEAR HASH

COLUMNS分区
上面的RANGE、LIST、HASH、KEY四种分区方式只支持整数,COLUMNS分区支持各种INT(不支持FLOAT和DECIMAL)、DATE、DATETIME、CHAR、VARCHAR、BINARY、VARRBINARY。

子分区(感觉有点朝纲了,略。。)

4.8.4 分区中的NULL值

NULL值小于任何非NULL值,所以RANGE分区时,在最小的分区。
ORDER BY时,NULL值也被认为是最小的

HASH 和 KEY 会对NULL值返回0

4.3.5 分区和性能

OLTP:在线事务处理,如Blog、电子商务
OLAP:在线分析处理,如数据仓库,统计系统

对于OLTP应用,分区并不一定能提高性能,反而会影响性能。
我的理解是,主要是看查询时,分区能不能带来一定的优化效果。因为InnoDB是B+树索引,所以1000W的数据拆分成100W,很可能树的层级只少了一个。如果查询涉及了多个分区的查找,那么每个分区都进行一次B+树搜索的时间很可能比搜索一个多了一层的B+树耗时更大。

这里插入了一个TIPS:即使根据自增主键进行HASH,分区也不能保证均匀,因为回滚时,自增值不会回滚,会发生跳跃

4.8.6 在表和分区间交换数据

索引和算法

5.1 InnoDB 存储引擎索引

索引类型:

  1. B+树索引
  2. 全文索引
  3. 哈希索引(之前介绍过,由引擎自动创建)

B+树索引并不指向具体的行,而是指向行所在的页,而后数据库把页读如到内存,再在内存中查找。

5.2 数据结构与算法

参考这里

5.3.1 B+树的插入操作

B+树插入的3种情况

当叶子节点满了,但是左右兄弟节点未满时会进行旋转操作,即只修改了一个父节点的值和对应的指针

5.3.2 B+树的删除操作

PS:填充因子最小可设的值时50%


B+树删除节点时的3种情况

5.4 B+树索引

聚集索引和辅助索引的区别时:叶子节点存放的是否是一整行的信息

5.4.1 聚集索引

聚集索引是跟记录存在一起的,表实际使用的页包括:非叶子节点所在的页(用来存储聚集索引),实际记录页(存储对应的行数据)。非页中的B+树节点标示下一层树节点所在的页序号,通过非叶子节点进行二分查找,定位所在页区间,然后把对应区间的页读到内存中进行查找

5.4.2 非聚集索引

跟聚集索引类似,不过叶子节点存储的是对应行的聚集索引键,所以通过非聚集索引查询到聚集索引键以后,还需要通过聚集索引键来查询行的实际数据

5.4.3 B+树索引的分裂

即保持数据页中的数据是顺序的算法。
简单来说就是,聚集索引随机插入时,将目标页以页中间的记录进行分页。
顺序插入时,如果插入位置后面有3条以上时,插入位置后3条进行分页
一般自增的插入是,如果目标页满了,新的记录另起一页

5.4.4 B+树索引的管理

SHOW INDEX FROM table_name \G
Cardinality用来标示表中,这个索引对应的列中的distinct count,这个数据不是实时更新的,可以通过ANALYZE TABLE table_name来刷新

Mysql5.5之前(不包括5.5),添加和删除索引是新建一个表,然后数据剪切过去的方式来实现的,效率很差
InnoDB 1.0.x版本开始支持Fast Index Creation(FIC)快速创建索引的方式

FIC删除索引,只需要删除索引所在的页(将页标记为可用),然后再删除相关视图中对索引的定义即可。
FIC增加索引时,需要对表加S锁(只读)
FIC方式只能用来创建辅助索引

5.5 什么是Cardinality

某一列的可选值越多,越适合添加索引
查看和刷新Cardinalty看上一段

Caridinaly更新时机:改动的行数超过1/16 或者 距离上次更新超过2百万次以上
Caridinaly更新策略:随机抽样8个叶子节点

5.6 B+树索引的使用

联合索引(a,b) 在单独以b为查询条件时无法使用,单独a时则可以,因为索引是以a为高位,b为低位排序的
联合索引(某些情况下)可以省掉排序操作

image.png

覆盖索引:即通过辅助索引就得到查询结果,而不用再通过聚集索引查询记录
一般情况下,如果只通过条件b查询,将不会使用(a,b)索引,但如果查询count(*)这种只需要辅助索引就能完成的查询,则会采用辅助索引,因为相比使用聚集索引,辅助索引空间更小,需要的IO操作更少。

如果查询的内容没有索引覆盖,即需要通过聚集索引查询行内容,优化器会判断,通过辅助索引查找到的数据量是否超过20%,如果超过20%则不实用辅助索引。因为辅助索引查询到的结果还需要通过聚集索引进行多次随机IO操作,反而直接使用聚集索引进行顺序的IO操作可能更快。不过针对SSD,随机IO和顺序IO没什么差别,则可以使用FORCE INDEX来强制使用某个索引: SELECT * FROM order FORCE INDEX(orderId) WHERE orderId>10000 and orderId<10200;

5.6.6 Multi-Range Read优化

MRR 对 SELECT * FROM salaries WHERE salary>10000 AND salary<40000是如何优化的这里我没看懂,难道不启用MRR时,是把所有 saraly > 10000salary < 40000的记录取出来再取交集? 而MRR是先吧交集之后的聚集索引ID取出来,然后再通过聚集索引去查询?

第二个例子也是分情况吧,要看是不是key_part2 != 10000的行有很多

  1. 共享锁(S Lock),允许事务读一行数据
  2. 排他锁(X Lock),允许事务更新或删除一行数据

6.3.3 一致性非锁定读

即读数据时,读的实际内容是该行的历史版本,从而不需要等待该行的X锁释放。在隔离级别READ COMMITED时,读到的是当前行最新的版本,在REPEATABLE READ(默认的隔离级别)下,读到的是事务开始时行的版本。

读时加锁的方法:
SELECT ... FOR UPDATE ; 加X锁
SELECT ... LOCK IN SHARE MODE ; 加S锁
对于一致性非锁定读,即使行已经执行了SELECT ... FOR UPDATE,也是可以读取的(因为根本不care任何锁...),上面两个加锁的SQL必须运行在一个事务中,务必加上BEGIN, START TRANSACTION 或者SET AUTOCOMMIT=0

6.4 锁的算法

Record Lock:单个行记录上的锁
Gap Lock:间隙锁,锁定一个范围,但不包含记录本身
Next-Key Lock:Gap Lock + Record Lock,锁定一个范围,并且锁住记录本身

这里有点不太理解的是,为什么对于辅助索引,Next-key Lock要锁住相邻的区域,想到的可能的解释是,当b的值有(1,3,6,10),查询b==3时,因为B+树的叶子组成了一个链表,所以查询得到的结果实际是(1.next, 6.next),所以需要把区间的两个端点锁住

对于uniq索引的锁定,SELECT ... FROM table WHERE b=3 FOR UPDATE 时,Next-Key Lock会降级为Record Lock。若uniq索引是多个列组成的,那么==的查询相当于还是范围查询,所以还是会使用Next-Key Lock

Phantom Problem是指在同一事物下,连续执行两次同样的SQL语句可能导致不同的结果,第二次的SQL语句可能返回之前不存在的行

READ COMMIT隔离级别使用的是Record Lock,在a=(1,2,5)中,SELECT * FROM t WHERE a > 2仅会锁定a=5的行,如果后面INSERT了a=4,则会导致Phantom Problem,REPEATABLE READ隔离级别则是用的Next-Key Lock,锁定了返回(2,+∞)

6.5 锁问题

脏读:一个事务读到了另一个事务还没提交的数据。READ UNCOMMITTED隔离级别下才会出现
不可重复读:同一个查询条件,同一个row两次返回结果不一样,再通俗一点就是另一个事务针对这个row执行了update操作(注意,这里是针对同一行,所以锁住这一行就不会有问题了)
幻读:同一个查询条件,返回的结果不一样(有一个隐含条件,同一个row返回的结果是一样的)。再通俗一点就是另一个事务针对这个表做了delete或者insert操作。

脏读 不可重复读 幻读
Serializable(锁表) 不会 不会 不会
REPEATABLE READ 不会 不会
READ COMMITTED 不会
Read Uncommitted

7事务

原子性:Atomicity
一致性:Consistency
隔离型:isolation
持久性:durability

binlog:逻辑日志,在事务提交时写如磁盘,用来做主从同步或按时间点恢复
redulog: 物理日志,在事务开始后即写到日志,记录物理数据页的修改信息,用于在重启时恢复未写入到硬盘的修改
undolog:逻辑日志,记录事务开始前的状态,而不是数据页,用于事务回滚,以及前面提到的非锁定不一致读

你可能感兴趣的:(MySQL技术内幕--读书笔记)