我们学习事务中,对于持久性(durability)是这样定义的:事务一旦提交,则其所有的修改将会保存到数据库当做。即使此时系统崩溃,修改的数据也不会丢失。同时数据库连接中,默认有一个参数autocommit=1表示每次执行一条sql如果没有显示启动事务语句(begin或start transaction)就会隐试的开启一个事务。

所以通常情况下,我们对数据库做的任何修改,只要事务提交都可以确保数据不会丢失。

在MySQL中完美的支持事务的存储引擎只有InnoDB,所以以下所有内容都是在InnoDB存储引擎下的故不会再做特别声明。

事务的持久性究竟是如何实现的,下面我们将一点一点的来探讨。

日志系统 如果要解释清楚持久性,就绕不开日志系统。同时MySQL的日志系统非常重要,如果不理解日志系统,后面所有的东西将无法理解,所以我将会尽我所能来解释清楚这个东西。

redo log 我们知道数据是存储在磁盘当中的,如果每一次数据修改操作都要写进磁盘,然后磁盘找到对应的那一条记录,然后再去更新。整个过程看下来IO成本、查询成本都非常高。

为了解决这个问题,MySQL采用了一种叫WAL(Write Ahead Logging)提前写日志的技术。意思就是说,发生了数据修改操作先写日志记录下来,等不忙的时候再持久化到磁盘。这里提到的日志就是redo log。

redo log称为重做日志,当有一条记录需要修改的时候,InnoDB引擎会先把这条记录写到redo log里面。redo log是物理格式日志,它记录的是对于每个页的修改。

redo log是由两部分组成的:一是内存中的重做日志缓冲(redo log buffer);二是用来持久化的重做日志文件(redo log file)。所以为了消耗不必要的IO操作,事务再执行过程中产生的redo log首先会redo log buffer中,之后再统一存入redo log file刷盘进行持久化,这个动作成为fsync。

至于什么时候从redo log buffer写入redo log file,可以通过InnoDB提供的innodb_flush_log_at_trx_commit参数来配置。

设置为0的时候,表示事物提交的时候不写入重做日志文件持久化。 设置为1的时候,表示每次事务提交都将redo log直接持久化到磁盘 设置为2的时候,表示每次事务提交时将重做日志写入重做日志文件,但是写入的仅仅是文件系统的缓存page cache不进行fsync。 InnoDB有一个后台线程master thread,每隔一秒就会把redo log buffer中的日志文件调用write写到文件系统缓存page cache,然后调用fsync持久化磁盘。

虽然设置成0或者2可以提升效率,但是也丧失了事务持久性的特性。

如果设置为0,事务提交之后master thread还没有来得及持久化MySQL就宕机了,那么这部分数据将会丢失。 如果设置成为1,MySQL发生宕机并不会导致数据丢失,但是当操作系统宕机时,重启数据库将会丢失文件系统缓存page cache中那部分数据。 redo log file也并不是无限大,而是固定大小的,默认是2个一组在我们MySQL安装路径下面就会找到两个文件“ib_logfile0”和“ib_logfile1”。它是从头开始写,写到末尾后回到头开始循环写,如下图所示。
image.png withe pos表示当前位置坐标,一边写一边往后移动。当write pos写到ib_logfile2末尾的时候,就会回到最开始的ib_logfile0文件。

check point表示已经刷新到磁盘上的位置,write pos到check point之间的位置表示安全可写的位置。如果write pos快要追上check point了,那么此时就暂停工作将一部分数据刷回磁盘。

Binlog 从MySQL架构上来看,主要分为Server层和存储引擎层。我们上面提到的redo log它是InnoDB引擎特有的日志,而Server层也有自己的日志,成为binlog二进制日志用于归档。

binlog记录了mysql执行更改了所有操作,但不包含select和show这类本对数据本身没有更改的操作。但是不是说对数据本身没有修改就不会记录binlog日志。

例如update t set a = 1 where a = 2,这条语句对数据库没有做任何修改,但是通过命令show binlog event也可以看到在二进制日志中做了记录。

Binlog日志的作用 恢复(recover):数据恢复 复制(replication):和恢复类似,用做主从复制 你可能会好奇,不是有了redo log还要再来一个binlog?

因为MySQL是存储引擎自带的,而redo log是InnoDB特有的。最开始的时候MySQL里没有InnoDB引擎,自带的MyISAM又没有crash-safe能力,binlog日志只能用于归档。为了让MySQL具有crash-safe能力所以就引入InnoDB,而InnoDB的crash-safe能力依靠redo log,所以就有了两套日志。

Redo log和binlog都是记录事务日志,他们有什么区别?

binlog是mysql自带的,他会记录所有存储引擎的日志文件。而redo log是InnoDB特有的,他只记录改存储引擎产生的日志文件 记录内容不同:binlog是逻辑日志,记录这个语句具体操作了什么内容。Redo log是屋里日志,记录的是每个页的更改情况。 写入方式不同:redo log是循环写,只有那么大的空间。binlog采用追加写入,当一个binlog文件写到一定大小后会切换到下一个文件。 更新一条语句的流程 InnoDB存储引擎中一条简单的更新语句就如下
20210407161802.png
首先执行器调用引擎获取数据,如果数据在内存中就直接返回;否则先从磁盘中读取数据,写入内存后再返回。 修改数据后再调用引擎接口写入这行数据 引擎层将这行数据更新到内存中,然后将更新操作写入redo log,这时候redo log标记为prepare状态。然后告诉执行器我处理完了,可以提交事务了。 执行器生成这个操作的binlog,并把binlog写入磁盘,然后调用引擎提交事务 引擎收到commit命令后,把刚才写入的redo log改成commit状态 至此我们的一条更新语句就算基本完成了,这里面涉及了两阶段提交prepare阶段和commit阶段。

为什么需要两阶段提交? 之前我们也清楚了,binlog是MySQL中Server层的日志,redo log是InnoDB存储引擎特有的。我们为了一致性就需要把这两个日志很好的持久化下来。而上面的redo log经历prepare和commit两个阶段才算提交。要解释为redo log什么需要两阶段提交,那么我们就用反证法说一说如果没有两阶段提交会发生什么问题吧?

redo log然后写binlog 如果redo log写完后,写binlog的时候,MySQL进程异常重启。但是我们redo log写入了这条更新,所以重启后这条数据依然会被修改。

但是因为binlog还没有写完就崩溃了,当我们需要用binlog来恢复临时库的时候就发现少了一次更新,这时候就会发生不一致。

先写binlog然后写redo log 如果binlog写完之后,redo log还没有写完就异常重启。那么InnoDB引擎就会判断事物无效,回滚这次操作。但是binlog里面却记录了这次操作。当我们使用binlog来恢复的时候就又多了一个事务,这时候又会数据不一致。