以前咱们了解了一条查询语句的执行流程,并介绍了执行过程当中涉及的处理模块。一条查询语句的执行过程通常是通过链接器、分析器、优化器、执行器等功能模块,最后到达存储引擎。ios
那么,一条 SQL 更新语句的执行流程又是怎样的呢?算法
首先咱们建立一个表 T,主键为 id,建立语句以下:sql
CREATE TABLE `T` ( `ID` int(11) NOT NULL, `c` int(11) DEFAULT NULL, PRIMARY KEY (`ID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
插入一条数据:数据库
INSERT INTO T VALUES ('2', '1');
数组
若是要将 ID=2 这一行的 c 的值加 1,SQL 语句为:缓存
UPDATE T SET c = c + 1 WHERE ID = 2;
架构
前面介绍过 SQL 语句基本的执行链路,这里把那张图拿过来。由于,更新语句一样会走一遍查询语句走的流程。app
其中,这两种日志默认在数据库的 data 目录下,redo log 是 ib_logfile0 格式的,binlog 是 xxx-bin.000001 格式的。async
接下来让咱们分别去研究下日志模块中的 redo log 和 binlog。分布式
在 MySQL 中,若是每一次的更新操做都须要写进磁盘,而后磁盘也要找到对应的那条记录,而后再更新,整个过程 IO 成本、查找成本都很高。为了解决这个问题,MySQL 的设计者就采用了日志(redo log)来提高更新效率。
而日志和磁盘配合的整个过程,其实就是 MySQL 里的 WAL 技术,WAL 的全称是 Write-Ahead Logging,它的关键点就是先写日志,再写磁盘。
具体来讲,当有一条记录须要更新的时候,InnoDB 引擎就会先把记录写到 redo log(redolog buffer)里面,并更新内存(buffer pool),这个时候更新就算完成了。同时,InnoDB 引擎会在适当的时候(如系统空闲时),将这个操做记录更新到磁盘里面(刷脏页)。
redo log 是 InnoDB 存储引擎层的日志,又称重作日志文件,redo log 是循环写的,redo log 不是记录数据页更新以后的状态,而是记录这个页作了什么改动。
redo log 是固定大小的,好比能够配置为一组 4 个文件,每一个文件的大小是 1GB,那么日志总共就能够记录 4GB 的操做。从头开始写,写到末尾就又回到开头循环写,以下图所示。
图中展现了一组 4 个文件的 redo log 日志,checkpoint 是当前要擦除的位置,擦除记录前须要先把对应的数据落盘(更新内存页,等待刷脏页)。write pos 到 checkpoint 之间的部分能够用来记录新的操做,若是 write pos 和 checkpoint 相遇,说明 redolog 已满,这个时候数据库中止进行数据库更新语句的执行,转而进行 redo log 日志同步到磁盘中。checkpoint 到 write pos 之间的部分等待落盘(先更新内存页,而后等待刷脏页)。
有了 redo log 日志,那么在数据库进行异常重启的时候,能够根据 redo log 日志进行恢复,也就达到了 crash-safe。
redo log 用于保证 crash-safe 能力。innodb_flush_log_at_trx_commit 这个参数设置成 1 的时候,表示每次事务的 redo log 都直接持久化到磁盘。这个参数建议设置成 1,这样能够保证 MySQL 异常重启以后数据不丢失。
MySQL 总体来看,其实就有两块:一块是 Server 层,它主要作的是 MySQL 功能层面的事情;还有一块是引擎层,负责存储相关的具体事宜。redo log 是 InnoDB 引擎特有的日志,而 Server 层也有本身的日志,称为 binlog(归档日志)。
binlog 属于逻辑日志,是以二进制的形式记录的是这个语句的原始逻辑,依靠 binlog 是没有 crash-safe 能力的。
binlog 有两种模式,statement 格式的话是记 sql 语句,row 格式会记录行的内容,记两条,更新前和更新后都有。
sync_binlog 这个参数设置成 1 的时候,表示每次事务的 binlog 都持久化到磁盘。这个参数也建议设置成 1,这样能够保证 MySQL 异常重启以后 binlog 不丢失。
为何会有两份日志呢?
由于最开始 MySQL 里并无 InnoDB 引擎。MySQL 自带的引擎是 MyISAM,可是 MyISAM 没有 crash-safe 的能力,binlog 日志只能用于归档。而 InnoDB 是另外一个公司以插件形式引入 MySQL 的,既然只依靠 binlog 是没有 crash-safe 能力的,因此 InnoDB 使用另一套日志系统——也就是 redo log 来实现 crash-safe 能力。
redo log 和 binlog 区别:
有了对这两个日志的概念性理解后,再来看执行器和 InnoDB 引擎在执行这个 update 语句时的内部流程。
下图为 update 语句的执行流程图,图中灰色框表示是在 InnoDB 内部执行的,绿色框表示是在执行器中执行的。
其中将 redo log 的写入拆成了两个步骤:prepare 和 commit,这就是两阶段提交(2PC)。
MySQL 使用两阶段提交主要解决 binlog 和 redo log 的数据一致性的问题。
redo log 和 binlog 均可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致。下图为 MySQL 二阶段提交简图:
两阶段提交原理描述:
备注: 每一个事务 binlog 的末尾,会记录一个 XID event,标志着事务是否提交成功,也就是说,recovery 过程当中,binlog 最后一个 XID event 以后的内容都应该被 purge。
binlog 会记录全部的逻辑操做,而且是采用追加写的形式。当须要恢复到指定的某一秒时,好比今天下午二点发现中午十二点有一次误删表,须要找回数据,那你能够这么作:
这样你的临时库就跟误删以前的线上库同样了,而后你能够把表数据从临时库取出来,按须要恢复到线上库去。
redo log 和 binlog 有一个共同的数据字段,叫 XID。崩溃恢复的时候,会按顺序扫描 redo log:
一个事务的 binlog 是有完整格式的:
在 MySQL 5.6.2 版本之后,还引入了 binlog-checksum 参数,用来验证 binlog 内容的正确性。对于 binlog 日志因为磁盘缘由,可能会在日志中间出错的状况,MySQL 能够经过校验 checksum 的结果来发现。因此,MySQL 是有办法验证事务 binlog 的完整性的。
redo log 过小的话,会致使很快就被写满,而后不得不强行刷 redo log,这样 WAL 机制的能力就发挥不出来了。
若是是几个 TB 的磁盘的话,直接将 redo log 设置为 4 个文件,每一个文件 1GB。
实际上,redo log 并无记录数据页的完整数据,因此它并无能力本身去更新磁盘数据页,也就不存在由 redo log 更新过去数据最终落盘的状况。
在一个事务的更新过程当中,日志是要写屡次的。好比下面这个事务:
begin; INSERT INTO T1 VALUES ('1', '1'); INSERT INTO T2 VALUES ('1', '1'); commit;
这个事务要往两个表中插入记录,插入数据的过程当中,生成的日志都得先保存起来,但又不能在还没 commit 的时候就直接写到 redo log 文件里。
所以就须要 redo log buffer 出场了,它就是一块内存,用来先存 redo 日志的。也就是说,在执行第一个 insert 的时候,数据的内存被修改了,redo log buffer 也写入了日志。
可是,真正把日志写到 redo log 文件,是在执行 commit 语句的时候作的。
如下是我截取的部分 redo log buffer 的源代码:
/** redo log buffer */ struct log_t{ char pad1[CACHE_LINE_SIZE]; lsn_t lsn; ulint buf_free; // buffer 内剩余空间的起始点的 offset #ifndef UNIV_HOTBACKUP char pad2[CACHE_LINE_SIZE]; LogSysMutex mutex; LogSysMutex write_mutex; char pad3[CACHE_LINE_SIZE]; FlushOrderMutex log_flush_order_mutex; #endif /* !UNIV_HOTBACKUP */ byte* buf_ptr; // 隐性的 buffer byte* buf; // 真正操做的 buffer bool first_in_use; ulint buf_size; // buffer大小 bool check_flush_or_checkpoint; UT_LIST_BASE_NODE_T(log_group_t) log_groups; #ifndef UNIV_HOTBACKUP /** The fields involved in the log buffer flush @{ */ ulint buf_next_to_write; volatile bool is_extending; lsn_t write_lsn; /*!< last written lsn */ lsn_t current_flush_lsn; lsn_t flushed_to_disk_lsn; ulint n_pending_flushes; os_event_t flush_event; ulint n_log_ios; ulint n_log_ios_old; time_t last_printout_time; /** Fields involved in checkpoints @{ */ lsn_t log_group_capacity; lsn_t max_modified_age_async; lsn_t max_modified_age_sync; lsn_t max_checkpoint_age_async; lsn_t max_checkpoint_age; ib_uint64_t next_checkpoint_no; lsn_t last_checkpoint_lsn; lsn_t next_checkpoint_lsn; mtr_buf_t* append_on_checkpoint; ulint n_pending_checkpoint_writes; rw_lock_t checkpoint_lock; #endif /* !UNIV_HOTBACKUP */ byte* checkpoint_buf_ptr; byte* checkpoint_buf; /* @} */ };
redo log buffer 本质上只是一个 byte 数组,可是为了维护这个 buffer 还须要设置不少其余的 meta data,这些 meta data 所有封装在 log_t 结构体中。
这篇文章主要介绍了 MySQL 里面最重要的两个日志,即物理日志 redo log(重作日志)和逻辑日志 binlog(归档日志),还讲解了有与日志相关的一些问题。
另外还介绍了与 MySQL 日志系统密切相关的两阶段提交(2PC),两阶段提交是解决分布式系统的一致性问题经常使用的一个方案,相似的还有 三阶段提交(3PC) 和 PAXOS 算法。
参考《MySQL实战45讲》