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 将会变得巨大,因为保存了太多回滚段。