45:MySQL的binlog,redolog,undolog与MVCC

458 阅读10分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。


binlog

binlog用于记录数据库执行的修改性操作,以二进制的形式保存在磁盘中。binlog是MySQL的逻辑日志,由服务层进行记录,也就是说无论使用任何存储引擎的MySQL都会记录,binlog通过追加的方式写入,可以通过max_big_size参数设置每个binlog文件的大小,当文件大小达到给定值后,会生成新的文件来保存。binlog主要应用于数据的备份(包括主从复制)与数据恢复。

binlog在事务提交时记录在内存中,刷盘时机则通过sync_binlog参数决定,参数的取值范围为0-N:

  • 0:表示不强制要求,由系统自行判断何时写入磁盘
  • 1:表示每次commit都会将binlog写入磁盘
  • N:标识commit N个事务,进行一次写入操作

很明显,1是最安全的设置,也是MySQL 5.7.7之后的默认值。

binlog日志的记录形式有三种:STATEMENT,ROW,MIXED。通过binlog-format指定。

  • STATEMENT:MySQL 5.7.7之前的默认形式。此种方式会直接记录修改SQL到binlog中。这种方式的优点是无需记录每行的变化,减少了binlog的日志量,也从侧面降低了IO。缺点是在某些场景下会导致主从数据不一致。比如使用了sysdate()或者sleep(),由于机器时钟差异的问题,就会导致数据不一致,此外还有主从上下文不一致式,使用UUID()函数或者user()函数时,都可能导致数据不一致的问题。验证可以看这篇博文:MySQL主备复制数据不一致的情况
  • ROW:MySQL 5.7.7之后的默认形式。基于行的记录,也就是说不是记录SQL是什么,而是记录哪条数据被修改成了什么状态。当做就是一个对目标数据的set操作(只是可以这么理解,毕竟SQL并不只有update,还有表结构的修改)。这样的好处是不会出现数据不一致,缺点是会产生大量的日志,尤其是alter table时。
  • MIXED: 混合模式,MySQL自己决定使用STATEMENT还是ROW,一般是STATEMENT能胜任的使用STATEMENT,不能胜任的使用ROW。

主从复制时,主从的备份方式要记录设置同步,否则使用binlog备份时就会出现数据无法同步的方式。

redolog

redolog归属于InnoDB存储引擎,内部记录着所有执行的DML语句。属于物理日志(注意逻辑日志和物理日志的区分,逻辑日志只是记录了数据的逻辑,和磁盘地址没关系,而物理日志则有磁盘地址有关,记录着物理地址的值是什么)。我们都知道的事务的有持久化的特性,而要实现持久化,仅仅录入内存是不够的,势必要与磁盘进行交互,而InnoDB与磁盘的交互是以数据页为单位的,如果一个事务只修改了一个数据行内的某几个字段,此时就进行一次完整的数据页刷入的话,不仅影响性能,还导致资源浪费。而如果一个事务修改了了多个数据页,如果对没给数据页都进行磁盘操作,那么随机IO的情况下,这个事务的性能也会变得很差。redolog是InnoDB在保证性能的情况下实现持久性的关键,它记录事务对数据页进行了哪些修改,不仅减小了与磁盘的交互数据量大小,而且是顺序IO。

在计算机操作系统中,用户空间内的数据一般是无法直接写入磁盘的,需要先进入操作系统内核的缓冲区(OS Buffer),才能由操作系统调用fsync()写入磁盘。因此redolog的提供了三种刷盘方式,通过innodb_flush_log_at_trx_commit指定:

  • 0:延迟写,此种方式事务提交时不会直接将redolog buffer的日志写入OS Buffer,而是每秒写入一次,并调用fsync()写入磁盘(redolog文件)。此种无论是MySQL故障还是应用宕机都会导致1s以内的数据丢失。
  • 1:实时写,实时刷。默认的格式。此种方式事务的每次提交都会将redolog buffer的日志写入OS Buffer,再调用fsync()写入磁盘(redolog文件),不会导致数据丢失,但是性能相对差一些。
  • 2:实时写,延迟刷。此种方式事务提交后会直接将redolog buffer的日志写入OS Buffer,但是不会立即调用fsync(),而是1s执行一次。此种方式MySQL故障不会丢数据,因为数据已经进入操作系统,但是宕机依然会导致数据存在1s以内的丢失。

redolog不像binlog,无需长久保存,毕竟数据页持久化后他就没有存在的意义了,因此redolog采用的是大小固定,循环写入的方式,日志写到结尾是,会回到开头循环写,如下图:

在这里插入图片描述

redo文件记录有两个LSN(逻辑序列号)位置,分别是write pos和check point, check point表示当前数据页刷盘进度,write pos表示当前redolog 文件写入的位置。write pos随着redolog的写入追上check point时,就先暂停,推动check point向后移动空出新的位置。

如果应用故障或宕机,MySQL重启后就会先探查check point的位置,从check point开始进行恢复(当然实际不是这一句话这么简单)。

redolog和binlog各司其职,binlog进行数据备份, redolog的存在使得InnoDB具有crash-safe的能力。

undolog

undolog记录着数据的逻辑变化历程,属于逻辑日志,行数据的每次修改,都会copy原值的行到undolog日志中,然后生成新值的行。 通过他可以在事务执行失败时进行回滚,从而保证了事务的原子性。同时undolog也是MVCC实现的关键,这个在下面的MVCC章节来说。

MySQL的binlog和InnoDB的redolog(undolog)采用二阶段提交方式,更新操作执行完毕后,InnoDB会记录redolog(undolog),此时redolog(undolog)进入prepare状态,然后InnoDB告知执行器执行完成,执行器记录binlog,记录完成后调用存储引擎接口,提交redolog(undolog)。以此来避免可能产生的数据不一致问题。如果出现宕机,会判定binlog是否完整,如果不完整就回滚redolog中的数据来保证数据同步。

处理过程如下:

  1. 从内存中找出数据,进行更新
  2. 更新结果情况,写入undolog
  3. 将本次对物理数据页的修改记录到redolog
  4. 将本次SQL逻辑操作记录到binlog中
  5. 由后台线程通过fsync进行刷盘。

MVCC-多版本并发控制

MySQL的大部分事务型引擎实现的都是行级锁,基于并发性能考虑,一般都实现了多版本并发控制(MVCC),不仅MySQL,Oracle,PgSQL等系统也都实现了MVCC,只不过实现机制可能不同。MVCC是行级锁的变种,避免了大多数情况下读的锁操作,因此开销更低,虽然机制不同,但是MVCC一般都实现了非阻塞的读操作。注意MVCC针对是读操作,写操作依然需要加锁实现。

MVCC是通过数据快照实现的,即整个事务期间,事务能观察到的数据都是一致的,都是事务开始时数据的快照。这就有一种可能,两个开始时间不同的事务,在同一时间观察到数据是不一致的。因此MVCC只在REPEATABLE_READ和READ_COMMITTED两个级别下工作,READ_UNCOMMITTED和SERIALIZABLE都和MVCC不兼容,这是因为READ_UNCOMMITTED总是读最新的数据,而不是符合事务版本的行,而SERIALIZABLE是对所有读取的行都加锁。 对于MVCC,InnoDB引擎下会维护一个事务版本号,每开启一个新事务,事务版本号都会自动递增并赋予这个事务,InnoDB通过在每行记录后面添加两个隐藏行来实现的,这两个隐藏的数据行一个存储创建事务id(trx_id),一个存储回滚指针roll_pointer,这个回滚指针就是指向undolog的日志地址。

undolog章节提到:行数据的每次修改,都会copy原值的行到undolog日志中,然后生成新值的行,再通过每一行的隐藏列roll_pointer,就可以得到一个数据行的变动历史。如图

在这里插入图片描述

图片来源面试中的老大难-mysql事务和锁,一次性讲清楚!

ReadView

有了这个变动链,怎么判断一个事务应不应该被访问呢?InnoDB引入了可读视图ReadView的概念,包含以下四个属性:

  • m_ids: 记录ReadView创建时当前系统中活跃的读写事务的事务id列表。
  • min_trx_id:表示ReadView创建时系统中活跃的读写事务中最小的事务id,就是m_ids中的最小值。
  • max_trx_id:注意不是m_ids的最大值,而是ReadView生成时应该分配的下一个事务id。
  • creator_trx_id:关联事务的事务id

有了ReadView就可以判定一个数据行应不应该被访问了:

  1. 如果数据行的trx_id等于creator_trx_id,那么说明这个事务在访问一个自己修改的数据行,这个数据行应该被访问。
  2. 如果数据行的trx_id小于min_trx_id,那么这个数据行应该被访问。小于说明该版本已经被提交,因为min_trx_id代表活跃的最小版本。
  3. 如果数据行的trx_id大于等于max_trx_id,说明该版本在当前事务之后开启,不应该就访问。
  4. 如果数据行的trx_id在min_trx_id(包含)和max_trx_id(不包含)之间,那么就需要判断trx_id是不是在m_ids列表中,如果在说明该版本在当前事务启动时还没提交,处于运行状态,不应该被访问,如果不在说明该版本在当前事务启动已经提交,该版本可以被访问。

REPEATABLE_READ和READ_COMMITTED两个事务级别很大程度就是通过控制ReadView的创建时间实现的,READ_COMMITTED在每次SELECT时都会新建一个ReadView,而REPEATABLE_READ仅仅是在第一次SELECT时生成一个ReadView,后续就复用这个ReadView,这种方式不仅使得可重复读,还避免了一般意义上的幻读(为什么是一般意义上,见下一小节)。可以看到如果仅仅使用MVCC,RR反而比RC性能好一点,因为不需要频繁创建ReadView,然后实际场景中并不是只有简单读。

幻读,MVCC和间隙锁(GAP锁)

既然REPEATABLE_READ级别下好像通过复用ReadView就能解决幻读,为什么在上方存储引擎-InnoDB章节还说InnoDB通过GAP锁解决了幻读呢?这是因为MySQL的读分为两种,一种为快照读,一种为当前读。

  • 快照读:简单的SELECT语句就属于快照读,
  • 当前读:SELECT *** FOR UPDATE。或者是UPDATE、INSERT、DELETE操作对于的读操作。不要惊讶,UPDATE、INSERT、DELETE都是需要执行读操作的,毕竟UPDATE和DELETE要找到目标数据,INSERT也要判断是否冲突。

快照读可以通过MVCC解决幻读的问题,毕竟仅有读,但是当前读则不行,因为当前读涉及到了数据修改,对于修改,在一个事务锁定期间是不能运行其他事务操作的,此时仅靠MVCC就不行了,需要进行加锁。此时就需要间隙锁的加入了,间隙锁通过锁住两条记录之间的间隙,来达到避免幻读的效果。


开发成长之旅 [持续更新中...]
欢迎关注…