(很形象的一个栗子)
不知道你还记不记得《孔乙己》这篇文章,酒店掌柜有一个粉板,专门用来记录客人的赊账记录。如果赊账的人不多,那么他可以把顾客名和账目写在板上。但如果赊账的人多了,粉板总会有记不下的时候,这个时候掌柜一定还有一个专门记录赊账的账本。
如果有人要赊账或者还账的话,掌柜一般有两种做法:
在生意红火柜台很忙时,掌柜一定会选择后者,因为前者操作实在是太麻烦了。首先,你得找到这个人的赊账总额那条记录。你想想,密密麻麻几十页,掌柜要找到那个名字,可能还得带上老花镜慢慢找,找到之后再拿出算盘计算,最后再将结果写回到账本上。
这整个过程想想都麻烦。相比之下,还是先在粉板上记一下方便。你想想,如果掌柜没有粉板的帮助,每次记账都得翻账本,效率是不是低得让人难以忍受?
同样,在 MySQL 里也有这个问题,如果每一次的更新操作都需要写进磁盘,然后磁盘也要找到对应的那条记录,然后再更新,整个过程 IO 成本、查找成本都很高。为了解决这个问题,MySQL 的设计者就用了类似酒店掌柜粉板的思路来提升更新效率。
而粉板和账本配合的整个过程,其实就是 MySQL 里经常说到的 WAL 技术,WAL 的全称是 Write-Ahead Logging,它的关键点就是先写日志,再写磁盘,也就是先写粉板,等不忙的时候再写账本。
具体来说,当有一条记录需要更新的时候,InnoDB 引擎就会先把记录写到 redo log(粉板)里面,并更新内存,这个时候更新就算完成了。同时,InnoDB 引擎会在适当的时候,将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做,这就像打烊以后掌柜做的事。
如果今天赊账的不多,掌柜可以等打烊后再整理。但如果某天赊账的特别多,粉板写满了,又怎么办呢?这个时候掌柜只好放下手中的活儿,把粉板中的一部分赊账记录更新到账本中,然后把这些记录从粉板上擦掉,为记新账腾出空间。
与此类似,InnoDB 的 redo log 是固定大小的,比如可以配置为一组 4 个文件,每个文件的大小是 1GB,那么这块“粉板”总共就可以记录 4GB 的操作。从头开始写,写到末尾就又回到开头循环写,如下面这个图所示。
write pos 是当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头。checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。
write pos 和 checkpoint 之间的是“粉板”上还空着的部分,可以用来记录新的操作。如果 write pos 追上 checkpoint,表示“粉板”满了,这时候不能再执行新的更新,得停下来先擦掉一些记录,把 checkpoint 推进一下。
有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为 crash-safe。
要理解 crash-safe 这个概念,可以想想我们前面赊账记录的例子。只要赊账记录记在了粉板上或写在了账本上,之后即使掌柜忘记了,比如突然停业几天,恢复生意后依然可以通过账本和粉板上的数据明确赊账账目。
redo log file 存放位置可通过show variables like 'innodb_log_group_home_dir'
参数查看,默认为“.\”,即mysql安装目录下。
每个InnoDB存储引擎至少有1个重做日志文件组(group),每个文件组至少有2个重做日志文件,如默认的ib_logfile0和ib_logfile1。为了得到更高的可靠性,用户可以设置多个镜像日志组(mirrored log groups),将不同的文件组放在不同磁盘上以此提高可用性。
日志组中每个redo log file大小一致,并且循环写入(上文已经讲过)。
下列参数影响着redo log file的属性:
innodb_log_file_size:指定每个文件的大小。InnoDB1.2.x之前,redo log file总大小不得大于4GB,而1.2.x将该限制扩大为了521GB。
innodb_log_files_in_group:指定了redo log file的文件数量,默认为2。
innodb_mirrored_log_groups:日志文件镜像数量,默认为1,看mysql手册这个参数5.1已经不起作用了。
redo log file的大小设置对于InnoDB存储引擎的性能有非常大的影响。一方面不能设置太大,如果设置的很大,在恢复时可能需要很长的时间;另一方面又不能设置的太小,否则可能导致一个事务的日志需要多次切换redo log file。此外重做日志文件太小会导致频繁地发生async checkpoint,导致性能的抖动。例如,用户可能会在错误日志中看到如下警告信息:
InnoDB:ERROR:the age of the last checkpoint is 9433645,
InnoDB:which exceeds the log group capacity 9433498
这是因为重做日志有一个capacity变量,该值代表了最后的检查点不能超过这个阈值,如果超过则必须将缓冲池(innodb buffer pool)中的脏页列表(flush list)中的部分脏数据页写回磁盘,这时会导致用户线程的阻塞(缓冲池和脏页等概念如果你不甚明了不影响观看此文)。
上面我们聊到的redo log 是 InnoDB 引擎特有的日志,而 Server 层也有自己的日志,称为 binlog(归档日志)。
binlog记录了对mysql数据库执行更改的所有操作,但是不包括select和show这类操作,因为这类操作对数据库本身并没有修改。
二进制日志主要由以下几种作用。
为什么会有两份日志呢?
最开始 MySQL 里并没有 InnoDB 引擎。MySQL 自带的引擎是 MyISAM,但是 MyISAM 没有 crash-safe 的能力,binlog 日志只能用于归档。而 InnoDB 是另一个公司以插件形式引入 MySQL 的,既然只依靠 binlog 是没有 crash-safe 能力的,所以 InnoDB 使用另外一套日志系统——也就是 redo log 来实现 crash-safe 能力。
这两种日志有以下三点不同。
逻辑日志可以给别的数据库,别的引擎使用,已经大家都讲得通这个“逻辑”;
物理日志就只有“我”自己能用,别人没有共享我的“物理格式”
有了对这两个日志的概念性理解,我们再来看执行器和 InnoDB 引擎在执行一个简单的 update 语句时的内部流程(简化流程,实际的要比这复杂的多)。
例:mysql> update T set c=c+1 where ID=2;
这里我给出这个 update 语句的执行流程图,图中浅色框表示是在 InnoDB 内部执行的,深色框表示是在执行器中执行的。
你可能注意到了,最后三步看上去有点“绕”,将 redo log 的写入拆成了两个步骤:prepare 和 commit,这就是"两阶段提交"。
为什么必须有“两阶段提交”呢?这是为了让两份日志之间的逻辑一致。
由于 redo log 和 binlog 是两个独立的逻辑,如果不用两阶段提交,要么就是先写完 redo log 再写 binlog,或者采用反过来的顺序。我们看看这两种方式会有什么问题。
仍然用前面的 update 语句来做例子。假设当前 ID=2 的行,字段 c 的值是 0,再假设执行 update 语句过程中在写完第一个日志后,第二个日志还没有写完期间发生了 crash,会出现什么情况呢?
可以看到,如果不使用“两阶段提交”,那么数据库的状态就有可能和用它的日志恢复出来的库的状态不一致。
你可能会说,这个概率是不是很低,平时也没有什么动不动就需要恢复临时库的场景呀?
其实不是的,不只是误操作后需要用这个过程来恢复数据。当你需要扩容的时候,也就是需要再多搭建一些备库来增加系统的读能力的时候,现在常见的做法也是用全量备份加上应用 binlog 来实现的,这个“不一致”就会导致你的线上出现主从数据库不一致的情况。
简单说,redo log 和 binlog 都可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致。
redo log 用于保证 crash-safe 能力。innodb_flush_log_at_trx_commit 这个参数设置成 1 的时候,表示每次事务的 redo log 都直接持久化到磁盘。这个参数我建议你设置成 1,这样可以保证 MySQL 异常重启之后数据不丢失。
sync_binlog 这个参数设置成 1 的时候,表示每次事务的 binlog 都持久化到磁盘。这个参数我也建议你设置成 1,这样可以保证 MySQL 异常重启之后 binlog 不丢失。
两阶段提交是跨系统维持数据逻辑一致性时常用的一个方案,即使你不做数据库内核开发,日常开发中也有可能会用到。
写redo log也是io,写入数据库也是io。同样耗费性能。怎么能做到优化呢?
redo log是顺序IO,并且可以组提交,还有别的一些优化,收益最大是是这两个因素;
写入数据库是随机IO,顺序IO和随机IO的区别可以自行百度。
redo log 的写入策略?
其实写入redo log的操作也不是直接写,而是先写入一个重做日志缓冲(redo log buffer),然后按照一定的条件顺序地写入日志文件,条件是由参数innodb_flush_log_at_trx_commit控制,它的值可以有三个,分别为0,1,2,效果不同之处在于:
0:当提交事务时,并不将事务的重做日志写入磁盘上的日志文件(此时redo log buffer已经记录了该事务),而是等待主线程每秒的刷新(将redo log buffer的内容flush到磁盘);
1:执行commit时将redo log buffer同步写到磁盘,即伴有fsync的调用;
2:将重做日志异步写到磁盘,即写到文件系统的缓存中.因此不能完全保证在执行commit时肯定会写入重做日志文件,只是有这个动作发生;
因此为了保证事务的ACID中的持久性,必须将值设置为1.当数据库因为意外发生宕机时,可以通过redo log file恢复,并保证可以恢复已经提交的事务.
而设置为0和2时,都有可能发生恢复时丢失部分事务.不同之处在于设置为2时,当msyql数据库发生宕机而操作系统及服务器没有宕机时,由于此时未写入磁盘的事务日志保存在文件系统缓存中,当恢复时同样保证数据不丢失.
redo log和bin log都记了什么?
Redo log不是记录数据页“更新之后的状态”,而是记录这个页 “做了什么改动”,比如把某个字段从0改成了1。
Binlog有两种模式,statement 格式的话是记sql语句, row格式会记录行的内容,记两条,更新前和更新后都有。
如果在非常极端的情况下,redo log被写满,而redo log涉及的事务均未提交,此时又有新的事务进来时,就要擦除redo log,这就意味着被修改的的脏页此时要被迫被flush到磁盘了,因为用来保证事务持久性的redo log就要消失了。但如若真的执行了这样的操作,数据就在被commit之前被持久化到磁盘中了。当真的遇到这样的恶劣情况时,mysql会如何处理呢,会直接报错吗?还是有什么应对的方法和策略呢?
这些数据在内存中是无效其他事务读不到的(读到了也放弃),同样的,即使写进磁盘,也没关系,再次读到内存以后,还是原来的逻辑
如果在重启后,需要通过检查binlog来确认redo log中处于prepare的事务是否需要commit,那是否不需要二阶段提交,直接以binlog的为准,如果binlog中不存在的,就认为是需要回滚的。
“binlog没有被用来做崩溃恢复”,
历史上的原因是,这个是一开始就这么设计的,所以不能只依赖binlog。
操作上的原因是,binlog是可以关的,你如果有权限,可以set sql_log_bin=0关掉本线程的binlog日志。 所以只依赖binlog来恢复就靠不住。
必要容易的理解
1 prepare阶段 2 写binlog 3 commit
当在2之前崩溃时
重启恢复:后发现没有commit,回滚。备份恢复:没有binlog 。
一致
当在3之前崩溃
重启恢复:虽没有commit,但满足prepare和binlog完整,所以重启后会自动commit。备份:有binlog. 一致
只有redo log commit 之后才会真正提交到数据库吗?
是这样,正常执行是要commit 才算完,但是崩溃恢复过程的话,可以接受“redolog prepare 并且binlog完整” 的情况
redo log和bin log怎么对应?
事务ID
单独依靠redologo无法做到指定某一点的恢复,是不是因为redologo是循环写的,如果要恢复的那一时刻被擦掉了,就无法知道这点的数据变动轨迹,而binlog是追加的方式,在文件完整的前提下,数据的变动轨迹都可以知晓.
binlog模式 ①记录sql语句 ②记录更新前后的行 一般是用哪种模式?
建议使用row格式,就是第二种
怎么知道binlog是完整的?
一个事务的完整binlog是有固定格式,也就是说有固定结尾的
在binlog写入磁盘后,commit提交前发生crash,由于commit没有成功,那返回给客户端的消息是事物失败,但是在系统恢复的时候却会重新提交事物,使之成功,这不是在欺骗客户端嘛?
不是,并没有返回“事务失败”呀,而是“执行异常”。执行异常本来就可能成功也可能失败的。你想一下这个场景:执行全部完成了,在回复客户端的时候网络断了,这怎么算
两阶段提交为了保证binlog和redolog的一致性,
1 prepare阶段 2 写binlog 3 commit阶段
假如在3之前崩溃,恢复时,虽没有commit,但prepare和binlog完整,所以重启后会自动commit,以此来保证有binlog一致性。
既然能这样做,那为什么不把commit阶段去掉,恢复时,只要redolog和binlog完整就认为redolog有效,否则回滚呢? 是因为效率还是其他的原因?
如果redolog是完整的,(包括了prepare和commit),就直接认为成功,不去判断binlog
如果两种log都写完成了,但是redolog没有写磁盘,物理机挂了,redolog在内存中就丢失了吧,再启动跟磁盘中的binlog就不一致,后期恢复数据的时候会有问题吧
是的,这就trx_commit没设置成1的后果…
关于提到的’数据页’这个词我没有太理解,是一种存储方式么?
MySQL的记录是以“页”为单位存取的,默认大小16K。也就是说,你要访问磁盘中一个记录,不会只读这个记录,而会把它所在的16K数据一起读入内存
在一个大事物或长事物里,一边执行sql一边写redo log吗?未提交的事物也会写到redo log file吗?
会先写一块内存,叫做redo log buffer里面,在提交的时候再一次性写到磁盘
innodb_flush_log_at_trx_commit如果设置成1而sync_binlog未设置成1,这个时候如果执行完redo log后宕机了,是不是redo log的内容跟binlog也会不一样?
对,有可能
请问redo log等系统不忙更新数据的时候,也是需要先从磁盘读出数据到内存,然后用redolog里面的记录更新内存,最后再一起写入磁盘吗?
对,好问题,这个就是“推进checkpoint”的过程
当然也有可能redo log要更新的那个数据页刚好在内存,就跳过了你说的第一步