Photo by hippopx.com
《MySQL实战45讲》笔记。
孔乙己又来酒馆喝酒,兜里没钱手机也没电了,只能向掌柜的赊账。掌柜有一块粉板,当客人要赊账的时候就往上写一笔,等客人少的时候或者粉板写满了就记到账本里去。还好有这块粉板,不然每次客人要赊账,掌柜都要翻看账本,在密密麻麻的账本里找到赊账客人的名字绝对不是一件容易的事,有了粉板,掌柜只要往粉板上记一笔:“孔乙己 赊 两文”,空闲的时候再更新到账本里去,简单多了。
同样的,MySQL也有一块“粉板”—— redo log。更新的时候,先写到 redo log 和内存里,这次更新就算是结束了。等到合适的时机再写到磁盘里,大大减小了写磁盘的次数。
redo log 是固定大小、“循环写”的,就像粉板一样,顶多也就记个十几二十条,多了就记不下了,这时会把粉板上的帐都写到账本里,再擦掉粉板,从头开始记。假设 redo log 配置了4组文件,每个文件 1G ,一共可记录 4G 的操作,写满了就会擦掉一部分记录。
redo log 是物理日志,记录的是“在某个数据页上做了什么修改”。
有了 redo log,InnoDB 就可以保证即使数据库发生了异常重启,之前提交的记录都不会丢失,这个能力称为 crash-safe。
binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。
binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如”给 ID=2 这一行的 c 字段加1“。
binlog 是“追加写”的,一个文件写完了会切换到下一个,不会覆盖以前的日志。
为什么有了 redo log 还需要 binlog?
其实 redo log 才是那个新来的仔。MySQL 自带了 binlog 日志用于归档,没有 crash-safe 的能力。InnoDB 引擎以插件的形式引入 MySQL 时,为了能够实现 crash-safe 的能力,引入了 redo log 。
一般我们用 binlog 做主从复制,数据恢复等操作。
binlog 是如何做数据恢复的?
一般我们做数据库备份是一周一备,一天一备,也可能一月一备。
假设今天中午12点,我们发现部分数据被误删了。需要恢复到昨天晚上8点这个时间段。但是数据库是每天凌晨3点的时候备份,离我们最近的一份备份数据已经缺失,只能恢复到昨天凌晨3点。这个时候我们就可以拿出昨天凌晨3点到晚上8点这个时间段的 binlog,重放到数据缺失前的那个时刻。在把这份数据恢复到线上数据库去。
了解了 redo log 和 binlog 这两个日志的概念,我们再来看看执行器和 InnoDB 引擎在执行这个简单的 update 语句时的内部流程。
下图出自《MySQL实战45讲》,浅色框表示是在 InnoDB 内部执行的,深色框表示实在执行器中执行的。
为什么需要两阶段提交?
我们先假设没有两阶段提交时,可能会有以下两种情况:
综上我们知道,redo log 和 binlog 必须同时成功或同时失败,才能保证数据一致性。
两阶段提交是如何保证 redo log 和 binlog 同时成功或同时失败的?
假设已经有了两阶段提交,分析一下以下两种情况:
那么 MySQL 是怎么知道 binlog 是否完整的?
一个事务的 binlog 是有完整的格式的:
什么是 change buffer ?
当需要更新一个数据时,如果数据页在内存里就直接更新了,如果数据页不在内存里,InnoDB 会将这些更新操作缓存在 change buffer 中,这样就不需要读磁盘了。在下次查询需要访问到这个数据页的时候,将数据页读入内存,然后执行 change buffer 中与这个页有关的操作。
如果能够将更新操作先记录在 change buffer, 减少读磁盘,更新操作变快。而且数据读入内存是需要占用 buffer pool 的,所以这种方式还能够避免占用内存,提高内存利用率。
change buffer 是可以持久化的数据,change buffer 在内存中有拷贝,也会被写入到磁盘中。
将 change buffer 中的操作应用到原数据页,得到最新结果的过程称为 merge。以下情况会触发 merge:
为什么普通索引比唯一索引效率高?
什么情况下不适合使用 change buffer?
如果某个业务更新后马上做查询,即使我们把更新先记录在 change buffer,读取操作也会马上把数据读入内存,而且立即触发 merge 操作。这种情况下,随机访问磁盘的次数没有减少,反而增加了 change buffer 的维护代价。所以对于这种业务,change buffer 反而起到了反作用。
我们可以看到,执行这条语句的成本很低,写了两处内存(内存和change buffer),写了一处磁盘(redo log,两次操作合在一起写磁盘),而且还是顺序写(直接写日志文件)。
同时,图中两个虚线箭头,是后台操作(异步操作,空闲时间就刷的那种),不影响该语句的响应时间。
从上面两个案例我们可以看出:
binlog 的写入逻辑:事务执行过程中,先把日志写到 binlog cache,事务提交的时候,再把 binlog cache 写到 binlog 文件中。
一个事务的 binlog 是不能被拆开写的,因此不论这个事务多大,也要确保一次性写入。
系统给 binlog cache 分配了一片内存,每个线程一个,参数 binlog_cache_size
用于控制单个线程内 binlog cache 所占内存的大小。如果超过了这个参数规定的大小,就要暂存的磁盘中。
事务提交时,执行器把 binlog cache 里的完整事务写到 binlog file 和 磁盘中,并清空 binlog cache。状态如下图所示:
innodb_flush_log_at_trx_commit
参数用来控制 redo log 的写入策略: innodb_log_buffer_size
一半的时候,也会触发持久化操作。为了降低写磁盘的次数,redo log 把 write 和 fsync 拆成两个步骤,当有并发时,事务A写完 page cahce,事务B也写完了 page cache,事务A触发 fsync 的时候,会把两个事务的 redo log 并在一组,一起写磁盘。
并且为了能让更多的事务加入同一个组,InnoDB 让 redo log 和 binlog 的 write 和 fsync 交替执行,分组提交的优化,redo log 和 binlog 都有。
WAL 机制是减少磁盘写,但每次提交事务都要写 redo log 和 binlog,写磁盘的次数好像没有减少?
redo log 是如何保证 crash-safe 的。 写到 redo log buffer 不能保证 crash-safe,写到 fs cache 也不能保证 crash-safe,只有 redo log 写入磁盘之后,数据库异常重启,从磁盘中的 redo log 拿出未执行的日志进行恢复,才算是 crash-safe。 这也说明了多个事务提交之后才写磁盘,还是会有事务丢失。只有每个事务提交后都进行写磁盘才能保证数据完全不丢失。
binlog 为什么无法保证 crash-safe?
能否只使用 binlog 或 redo log 单个日志保证 crash-safe?
我的理解是:并不是单个 log 无法保证 crash-safe,而是 binlog 本身无法保证 crash-safe,因为 InnoDB 无法重新设计 binlog,所以引入了 redo log。并且花了很大力气来保证 redo log 和 binlog 的一致性。 如果重新设计 MySQL,可以使用 redo log 实现 binlog 的功能,也可以把 binlog 设计成 crash-safe 的,这样就只需要一种 log 了。