1.MySQL架构

架构

最近复习MySQL,先从整体架构层面开始。
客户端发送连接请求,MySQL后台有一个线程监听请求,然后SQL接口获取SQL语句,由SQL解析器进行语法解析,解析完成以后查询优化器生成最优查询路径,最后交给执行器来执行,执行器会调用存储引擎从内存或磁盘访问数据。


MySQL架构

更新语句执行流程

MySQL默认存储引擎InnoDB支持事务,执行update语句,首先从磁盘加载数据到MySQL的内存buffer pool,然后生成undo log,用于数据回滚,然后在内存中完成数据的更新,再写入redo log,事务提交时redo log刷盘,并写入binlog,后台线程定时将内存数据刷盘。
undo log记录的是逻辑日志,包括日志文件的开始位置、结束位置、表id、主键值、日志类型(update还是insert)、日志编号。回滚时做反向操作就可以了。
redo log记录的是物理日志,包括表空间号、数据页号、偏移量、修改了几个字节的值、具体的值。当事务提交后,缓存页还没有刷到磁盘时,MySQL宕机了,重启后还能从redo log中恢复数据,如果是MySQL所在的机器宕机了,数据是否可以恢复要看redo log刷盘机制,因为redo log先是写入redo log buffer,为了防止数据丢失,要设置成当事务提交时将redo log buffer刷入磁盘,且写入binlog后,redo log文件与binlog文件同步,redo log写入commit标记,才算事务提交成功,否则,就算redo log已经刷盘,但是binlog还没有和redo log同步,redo log还没有写入commit标记,这个事务仍然是失败的,MySQL重启后,依然会执行回滚操作。
redo log和binlog区别:既然已经有了redo log用来做数据恢复,为什么还要有binlog呢,是因为redo log是存储引擎层的日志,binlog是MySQL server层的日志,主要用来做主从同步。


update语句执行流程

数据页

如果每次查询都从磁盘读取,那性能也太低了,而MySQL有buffer pool结构,每次查询先从内存取,如果内存没有,再从磁盘读取,然后保存到buffer pool。现在查询一条数据,具体是怎么查的呢,如果buffer pool中没有,查询一次就从磁盘读取一次吗?MySQL的数据在磁盘上是以数据页为单位存储的,默认每个数据页是16KB,存储的是一行一行的数据,每行数据以单链表连接,如下图所示:


数据页

然后每个数据页还有一个对应的页目录,记录着主键和槽位,类似稀疏索引。


页目录

查找数据时,可以先从页目录二分查找主键对应的槽位,然后再从数据页从具体槽位开始一行一行遍历,最终找到数据。
具体每一行数据是怎么存储的呢?

我们创建表结构时,有varchar类型,和允许为null的字段,比如下面一个例子:

CREATE TABLE `sys_user` (
  `id` bigint(20) NOT NULL,
  `usercode` varchar(255) NOT NULL COMMENT '用户编码',
  `password` varchar(255) DEFAULT NULL COMMENT '密码',
  `name` varchar(255) DEFAULT NULL COMMENT '用户名'
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

MySQL大概是按照以下结构存储的:


一行数据

假如现在有一行数据:1 100001 5bg7b89rf NULL
NULL值列表:一共有3个变长字段,其中第一个字段usercode定义为NOT NULL,NULL值列表只表示那些允许为NULL的字段,password和name字段允许为NULL,这行数据中password不为null,name为null,为null用1表示,不为null用0表示,且逆序存放,所以这两个字段就是01,逆序为10,且NULL值列表是以8个bit位的倍数存放的,不够的高位补0,也就是00000010。
变长字段长度列表:usercode字段值为100001,长度为6,password字段值为5bg7b89rf,长度为9,用16进制表示为0X06 0x09,逆序存放就是0x09 0X06 。
所以,最终这一行数据大概就是这样存储的:
0x09 0X06 00000010 数据头信息 1 100001 5bg7b89rf
取数据时,首先看数据头后面的数据,id不为null,那就是第一个数字1,usercode是变长字段类型且不为null,就找前面的16进制表示的变成字段长度列表,逆序找到第一个0X06,表示长度是6,也就是100001,然后password字段也是变长字段类型且可以为null,就先找null值列表看这个字段值是否为null,找到00000010,逆序找第一位是0,表示不是null,那就逆序找变成字段长度列表的第二位0x09,表示这个字段长度是9,也就是5bg7b89rf,最后name字段也是变长字段且可以为null,先找null值列表逆序找到第二位是1,表示这个字段值是null,就不用再找变长字段长度列表了。

buffer pool

磁盘上是以数据页为单位存储的,而buffer pool中是和数据页一样,只不过在buffer pool中叫缓存页,数据页和缓存页是一一对应的。
如果buffer pool设置的过小,那么存储的数据就少,从磁盘读数据的概率就大,从而影响性能,所以一般buffer pool设置为机器内存的50%到60%。一个buffer pool中除了存储缓存页,还有free链表、flush链表和lru链表,以及一个hash结构,用于表示缓存页和数据页的映射关系。
MySQL启动时,就已经划分好了一个一个的缓存页,一个缓存页对应一个描述数据,记录这个缓存页表示的表空间、数据页编号、缓存页地址,其中free链表表示空闲的缓存页,读取一条数据时,首先从hash结构中查看对应的数据页有没有在buffer pool中,如果没有,再从free链表中找一个空闲缓存页存储该数据页,然后该缓存页从free链表移除,加入到lru链表,因为buffer pool大小是有限的,对于一些经常不使用的数据,会被淘汰,这里有个优化点,lru链表进行了冷热数据分离,其中热数据占比63%,剩下的冷数据占比37%,第一次访问会放到冷数据头部,如果1s内再次访问只会从冷数据区尾部移动到冷数据区头部,1s后再次访问才会从冷数据区转移到热数据区头部,这种情况一般是全表扫描,将数据存储到缓存页以后,之后再也不访问了,另外一种是计算机的局部性原理,每次读取数据会将临近的数据页也一并读取,存放到buffer pool中,可能这些临近的数据页压根都没有被访问,每次内存不足时,淘汰冷数据区尾部的缓存页,也就是将冷数据区尾部缓存页上的数据刷新到磁盘,再从lru链表冷数据区删除。
当数据更新时,buffer pool中的缓存页数据和磁盘上的数据不一致时,称为脏页,flush链表用来表示这些脏页。
MySQL启动时,所有的缓存页都存在于free链表,随着访问数据,free链表减少,lru链表增加,更新数据后,flush链表增加,另外后台有一个线程,定时将lru链表尾部的缓存页刷新到磁盘,lru链表和flush链表减少,free链表增加;后台线程定时将flush链表缓存页刷新到磁盘,flush链表和lru链表减少,free链表增加。
每个链表由描述数据组成双向链表,有一个基础节点表示链表数量。
多个线程同时访问同一个buffer pool,修改数据,修改flush链表、free链表、lru链表,必然要加锁,但是这些都是基于内存的,原则上速度很快,但是如果并发量很大,还是会影响性能,所以可以设置多个多个buffer pool,来分摊并发量,每个buffer pool又可以设置多个chunk,进一步缩小锁的范围,每一个buffer pool共用一套free链表、flush链表和lru链表。


buffer pool结构

关于MySQL还有很多其他的知识点,比如索引、事务、锁等,限于篇幅,这些另起一篇再写吧。如有错误,还望大佬指出,谢谢!

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
禁止转载,如需转载请通过简信或评论联系作者。

推荐阅读更多精彩内容