mysql中有两块非常重要的日志模块,分别是redo log 和binlog。这两种日志的主要特点如下:
(1)redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。
(2)redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1 ”。
(3)redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
一、redo log
Redo log不是记录数据页“更新之后的状态”,而是记录这个页 “做了什么改动”。
mysql中有一个非常重要的技术叫WAL(write-ahead-logging)。顾名思义,在mysql进行写操作时会先写日志,然后再写磁盘。而这里的写日志,写的就是redo log。
当有一条记录需要更新的时候,InnoDB 引擎就会先把记录写到 redo log里面,并更新内存,这个时候更新就算完成了。同时,InnoDB 引擎会在适当的时候,
将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做。
InnoDB 的 redo log 是固定大小的,比如可以配置为一组 4 个文件,每个文件的大小是 1GB,那么总共就可以记录 4GB 的操作。从头开始写,写到末尾就又回到开头循环写,如下面这个图所示。
redo log 的存储结构里主要由write pos和checkpoint两个指针控制,write pos 是当前记录的位置,一边写一边后移。checkpoint 是当前要清空数据的位置,也是往后推移并且循环的,
擦除记录前要把记录更新到数据文件(磁盘中)。write pos 和 checkpoint 之间的是redo log上还空着的部分,可以用来记录新的操作。如果 write pos 追上 checkpoint,表示redo log满了,
这时候不能再执行新的更新,得停下来先擦掉一些记录,把 checkpoint 推进一下。
有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为 crash-safe。
1.1 redo log 的写入机制
redo log 的日志主要存在于三个位置,分别是redo log buffer、page cache 和硬盘。
这三种状态分别是:
存在redo log buffer中,物理上是在MySQL进程内存中,就是图中的红色部分;
写到磁盘(write),但是没有持久化(fsync),物理上是在文件系统的page cache里面,也就是图中的黄色部分;
持久化到磁盘,对应的是hard disk,也就是图中的绿色部分。
日志写到redo log buffer是很快的,wirte到page cache也差不多,但是持久化到磁盘的速度就慢多了。
为了控制redo log的写入策略,InnoDB提供了innodb_flush_log_at_trx_commit参数,它有三种可能取值:
设置为0的时候,表示每次事务提交时都只是把redo log留在redo log buffer中;
设置为1的时候,表示每次事务提交时都将redo log直接持久化到磁盘;
设置为2的时候,表示每次事务提交时都只是把redo log写到page cache。
InnoDB有一个后台线程,每隔1秒,就会把redo log buffer中的日志,调用write写到文件系统的page cache,然后调用fsync持久化到磁盘。
注意,事务执行中间过程的redo log也是直接写在redo log buffer中的,这些redo log也会被后台线程一起持久化到磁盘。也就是说,一个没有提交的事务的redo log,也是可能已经持久化到磁盘的。
实际上,除了后台线程每秒一次的轮询操作外,还有两种场景会让一个没有提交的事务的redo log写入到磁盘中。
一种是,redo log buffer占用的空间即将达到 innodb_log_buffer_size一半的时候,后台线程会主动写盘。注意,由于这个事务并没有提交,所以这个写盘动作只是write,而没有调用fsync,也就是只留在了文件系统的page cache。
另一种是,并行的事务提交的时候,顺带将这个事务的redo log buffer持久化到磁盘。假设一个事务A执行到一半,已经写了一些redo log到buffer中,这时候有另外一个线程的事务B提交,如果innodb_flush_log_at_trx_commit设置的是1,那么按照这个参数的逻辑,事务B要把redo log buffer里的日志全部持久化到磁盘。这时候,就会带上事务A在redo log buffer里的日志一起持久化到磁盘。
二、binlog
用于记录了完整的逻辑记录,所有的逻辑记录在 bin log 里都能找到,所以在备份恢复时,是以 bin log 为基础,通过其记录的完整逻辑操作,备份出一个和原库完整的数据。
Binlog有三种模式,statement 格式的话是记sql语句, row格式会记录行的内容,记两条,更新前和更新后都有,Mixed是statement和row格式的融合
2.1事务两阶段提交过程
eg: update table set c=c+1 where id =2
(1)执行器先找引擎取 ID=2 这一行。ID 是主键,引擎直接用树搜索找到这一行。如果 ID=2 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
(2)执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是 N+1,得到新的一行数据,再调用引擎接口写入这行新数据。
(3)引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。
(4)执行器生成这个操作的 binlog,并把 binlog 写入磁盘。
(5)执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。
2.2 写binlog时机
binlog的写入逻辑比较简单:事务执行过程中,先把日志写到binlog cache,事务提交的时候,再把binlog cache写到binlog文件中。
一个事务的binlog是不能被拆开的,因此不论这个事务多大,也要确保一次性写入。这就涉及到了binlog cache的保存问题。
系统给binlog cache分配了一片内存,每个线程一个,参数 binlog_cache_size用于控制单个线程内binlog cache所占内存的大小。如果超过了这个参数规定的大小,就要暂存到磁盘。
事务提交的时候,执行器把binlog cache里的完整事务写入到binlog中,并清空binlog cache。
可以看到,每个线程有自己binlog cache,但是共用同一份binlog文件。
图中的write,指的就是把日志写入到文件系统的page cache,并没有把数据持久化到磁盘,所以速度比较快。
图中的fsync,才是将数据持久化到磁盘的操作。一般情况下,我们认为fsync才占磁盘的IOPS。
write 和fsync的时机,是由参数sync_binlog控制的:
(1)sync_binlog=0的时候,表示每次提交事务都只write,不fsync;
(2)sync_binlog=1的时候,表示每次提交事务都会执行fsync;
(3)sync_binlog=N(N>1)的时候,表示每次提交事务都write,但累积N个事务后才fsync。
因此,在出现IO瓶颈的场景里,将sync_binlog设置成一个比较大的值,可以提升性能。在实际的业务场景中,考虑到丢失日志量的可控性,一般不建议将这个参数设成0,比较常见的是将其设置为100~1000中的某个数值。
但是,将sync_binlog设置为N,对应的风险是:如果主机发生异常重启,会丢失最近N个事务的binlog日志。
三、两阶段提交事务回滚
(1)在prepare阶段carsh
因为事务还没有提交,binlog还没有写入磁盘,该事务会直接rollback。
(2)事务在将binlog cache写入磁盘的时候crash
因为该事务还没有全部写入磁盘,故此时xid不会写入到binlog,会认为该事务并没有提交,所以会将该事务回滚。
(3)事务已经全部刷新到磁盘,但在引擎层还没有commit
因为在binlog中已经有该事务的xid,所以会将该事务在引擎层提交,然后将redo log checkpoint点之后的事务进行重做。事务可以提交。
最后:打一个小广告,后续的文章会在微信公众号“程序员之家QAQ”推送,欢迎大家搜索关注~~