Mysql 细节
MySql 架构
大体来说,MySQL
可以分为 Server
层和存储引擎层两大部分。
Server 层
Server
层包括连接器、查询缓存、分析器、优化器、执行器等,涵盖 MySQL
的大多数核心服务功能,以及所有的内置函数
(如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。
-
连接器
:负责处理与客户端的连接,包含了认证等模块。在完成TCP
三次握手,且认证通过,一个连接就建立起来。需要注意的是,以及成功建立连接的用户,其权限在连接建立成功后就已经确定了,不会受到权限的的更改,需要flush privileges
。连接建立后,受到wait_timeout
参数的影响,超过该时间的连接(默认为8 小时
),会被断开。顺便提一下,redis
也有类似的设置timeout 10
,单位秒,其实提供TCP
服务的一般都会这么处理。 -
查询缓存
:以k-v
形式保存的sql
结果,对于并发较高的互联网业务来说,因为只要表有更新缓存这张表的所有查询缓存就被清空,所以这个功能基本没用,不仅浪费内存开销,还会带来额外的cpu
压力,所以MySql 8.0
之后删除了该模块。 -
分析器
:这里就是典型的词法分析
与语法分析
了,类似PHP
等。词法分析是将文本按照规则解析成对应语义,如表名、列名等;语法分析则在词法分析基础上,分析语法是否正确。这里,有所谓解析器和预处理器的概念,解析器处理语法和解析查询,生成一课对应的解析树。预处理器进一步检查解析树的合法,例如数据表和数据列是否存在,别名是否有歧义等。如果通过则生成新的解析树,再提交给优化器。也即,如果查询一个表中不存在的列,这个阶段就会出错。 -
优化器
:这一步,是常见的sql
优化部分,优化器会分析sql
,从若干可执行路径里选择一个最优解,如是否走索引、走哪个索引、先join
哪张表等等,这一步可能会存在误判,可使用use index
强制指定索引。 -
执行器
:在拿到分析器处理后的sql
后,执行器首先会检查用户是否有权限进行相关操作,如是否有读取表的权限等等。权限校验通过后,根据表创建时设置的存储引擎,请求对应存储引擎提供的API
,请求执行sql
,并将存储引擎返回值保存在结果集(这里可能是多次请求多次返回),然后发送给客户端。
存储引擎层
存储引擎层主要负责数据的存储和提取。其架构模式是插件式的,支持 InnoDB
、MyISAM
等多个存储引擎。其中最常用的存储引擎是 InnoDB
,MySQL 5.5.5
版本成为默认存储引擎。
redo log、binlog 与 undo log
redo log
、binlog
、undo log
是 MySql
最重要几种日志。
redo log
redo log
称之为重做日志,是 InnoDB
特有的日志。redo log
是物理日志,记录的是 在某个数据页上做了什么修改。
我们知道,写日志一般是个耗时的过程,因为必然伴随着大量 IO
,但对于数据库来说,日志又是如此重要,所以 MySql
使用一种范式来处理日志,也就是所谓的 WAL 技术
(Write-Ahead-Logging
),它的本质就是先写日志、再写磁盘。这涉及到 fsync
、group commit
的概念。
具体来说,当有一条记录需要更新的时候,InnoDB
引擎就会先把记录写到 redo log
,并更新内存,更新成功则认为写成功。此后,InnoDB
会在适当的时候,将这个操作记录更新到磁盘。这个写入是顺序写
,写入效率极高。
redo log
是有固定大小的,假设配置了 4
个 1GB
大小的文件,则总是顺序写入,写到末尾再回到开头循环写。文件名形如 ib_logfile0
、ib_logfile1
等。这个原理与环形缓冲区类似ring buffer
,通过维护的 write pos
、check point
进行循环写入。
-
write pos
:记录当前写入的位置,不断写则不断后移。 -
check point
:标记当前需要擦除的位置,也是循环的移动。擦除记录前需要把记录更新到数据文件。当write pos == check point
时,表示写满了,此时会拒绝新的写入或更新,需要将数据写入磁盘,释放新的可写空间。 是crash recovery
的起点。
MySql
基于 redo log
实现了数据恢复的功能,也即 crash safe
机制。因为所有为落盘的记录都在 redo log
中,可以通过 redo log
恢复。
关于 redo log
,语义上包括两部分:一是内存中的日志缓冲(redo log buffer
),该部分日志是易失性的;二是磁盘上的重做日志文件(redo log file
),该部分日志是持久性的(表示事务以及提交)。
这里可能有个问题,比如先 update
再立即 select
,如果没设置自动提交,能否读取到该 update
后的新值呢?答案是可以读到的,直接读取内存即可。
将 innodb_flush_log_at_trx_commit
参数设置成 1
,保证每次事务的 redo log
都直接持久化到磁盘。
binlog
binlog
也即归档日志,属于 Server
层的日志。binlog
是逻辑日志,有两种记录模式,statement
格式记录的是具体 sql语句
,比如 update t set score = score + 2 where id = 1
,类似于 redis
的 AOF
;row
格式在记录数据行的更新前、更新后的两条日志。与 redo log
是循环写方式不同,binlog
是追加写入的,写满一个文件则会切换到下一个,并不会覆盖以前的日志。binlog
最常见的用途在于 主存复制
、容灾备份
等。
对于一条更新语句而言,如 update t set score = score + 2 where id = 1
,其大体工作流程如下:
-
执行器先调用存储引擎
API
读取id = 1
的行数据,此时存储引擎会先检查是否已经在内存中,在就直接返回了;不在则检测是否是索引,是则通过B+树
查找到后从磁盘读取到内存,然后返回。 -
执行器拿到行数据后,把
score
加 1,再调用存储引擎API
写入修改后的数据。 -
存储引擎将这行新数据更新到内存中,同时将这个更新操作记录到
redo log
,此时redo log
处于prepare
状态。然后告知执行器执行完成了,随时可以提交事务。执行器生成这个操作的binlog
,并把binlog
写入磁盘。执行器调用存储引擎的提交事务接口,存储引擎把刚刚写入的redo log
改成提交(commit
)状态,更新完成。这个写入机制也就是所谓的两阶段提交
。
通过两阶段提交协议,可以保证 crash safe
,因为是先 prepare redo log
后,再写 binlog
,然后再 commit redo log
,这样不会造成 redo log
与 binlog
不一致。本质是因为 binlog
是用来备份的,而 redo log
则记录了数据库的真正修改。如果先写 redo log
,那么 binlog
如果写入失败,则通过 binlog
恢复后的数据库会丢失这次更新;如果先写 binlog
再写 redo log
,如果后者写入失败,再恢复时,会多了一次更新。而通过两阶段提交,redo log
的 prepare
状态可以结合 binlog
进行 commit
。俩者之间通过 事务 ID
进行关联。
将 sync_binlog
参数设置成 1
,保证每次事务的 binlog
都持久化到磁盘。
undo log
undo log
也称之为回滚日志,是一种逻辑日志,主要实现回滚、MVCC
机制。
在数据修改的时候,不仅记录了 redo log
,还会记录相对应的 undo log
,记录的都是反向操作。如果事务失败或回滚,可以借助其进行回滚。比如在 delete
一条记录,undo log
会增加一条对应的 insert
语句,update
时则记录相反的 update
语句。当执行 rollback
操作时,就可以从 undo log
中的逻辑记录读取到相应的内容并进行回滚。
在 MVCC
下,可通过 undo log
来实现多版本控制,比如提供快照读等,实现了非锁定一致性读取。本质是每个事务启动后,其看见的数据视图(read view
)不一样,也即在回滚段上看到的数据不一样。
-
delete
操作不会直接删除,而是将delete
记录标记为delete flag
,事务提交后,由purge
线程完成真正删除。 -
update
操作,如果是是主键列,先删除一行再插入一行;如果不是主键列,记录该update
的反向操作。
所以如果数据库很多长事务,或未提交的事务,undo log
将会变得巨大,因为保存了太多回滚段。