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,并将存储引擎返回值保存在结果集(这里可能是多次请求多次返回),然后发送给客户端。

存储引擎层

存储引擎层主要负责数据的存储和提取。其架构模式是插件式的,支持 InnoDBMyISAM等多个存储引擎。其中最常用的存储引擎是 InnoDBMySQL 5.5.5 版本成为默认存储引擎。

redo log、binlog 与 undo log

redo logbinlogundo logMySql 最重要几种日志。

redo log

redo log 称之为重做日志,是 InnoDB 特有的日志。redo log 是物理日志,记录的是 在某个数据页上做了什么修改

我们知道,写日志一般是个耗时的过程,因为必然伴随着大量 IO,但对于数据库来说,日志又是如此重要,所以 MySql 使用一种范式来处理日志,也就是所谓的 WAL 技术Write-Ahead-Logging),它的本质就是先写日志、再写磁盘。这涉及到 fsyncgroup commit 的概念。

具体来说,当有一条记录需要更新的时候,InnoDB 引擎就会先把记录写到 redo log,并更新内存,更新成功则认为写成功。此后,InnoDB 会在适当的时候,将这个操作记录更新到磁盘。这个写入是顺序写,写入效率极高。

redo log 是有固定大小的,假设配置了 41GB 大小的文件,则总是顺序写入,写到末尾再回到开头循环写。文件名形如 ib_logfile0ib_logfile1 等。这个原理与环形缓冲区类似ring buffer,通过维护的 write poscheck 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,类似于 redisAOFrow 格式在记录数据行的更新前、更新后的两条日志。与 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 logbinlog 不一致。本质是因为 binlog 是用来备份的,而 redo log 则记录了数据库的真正修改。如果先写 redo log,那么 binlog 如果写入失败,则通过 binlog 恢复后的数据库会丢失这次更新;如果先写 binlog 再写 redo log,如果后者写入失败,再恢复时,会多了一次更新。而通过两阶段提交,redo logprepare 状态可以结合 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 将会变得巨大,因为保存了太多回滚段。