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