事务底层实现原理
“原子性”涉及到的一些东西
说到原子性之前,先了解下redu log(重写日志)和undo log(回滚日志)。
(1)redu log保证的是事物的持久性
(2)undo log保证的是事务的原子性和隔离性
原子性实现的关键:当事务出现回滚时候能够撤销所有已经成功执行的sql语句。
InnoDB能够实现回滚主要是因为undo log:当事务中出现对数据修改的的时候,InnoDB就会生成相应undo log。如果事务执行失败或者rollback,就可以通过undo log中的信息将数据回滚到修改之前的样子。
undo log 属于一个逻辑日志,它用来记录的是sql执行相关的信息。对于一个insert语句在回滚时候会执行delete,相反也是如此。例如对于一个update在执行的时候,其生成的undolog 中会包含被修改的主键(以便知道修改了哪些行,修改了哪些列)以便在回滚时候能够使用这些记录的信息将数据还原到执行之前。
由上图可知,在每次更新都会生成undu log,也可以得出结论:
- insert,update,delete 都会生成undo log。
- 当rollback后,将根据undo log回滚,逆过程。
从上面一段话引出了很多问题,其中包括:
(1)什么是undo log?
答:undo log和下面提到的redo log记录物理日志不一样,它是逻辑日志。可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的update记录。
(2)undo log的作用?
答:提供回滚和多个行版本控制(MVCC)。在数据修改的时候,不仅记录了redo log,还记录了相对应的undo log,如果因为某些原因导致事务失败或回滚了,就可以从undo log中的逻辑记录读取到相应的内容并进行回滚。有时候应用到行版本控制的时候,也是通过undo log来实现的:当读取的某一行被其他事务锁定时,它可以从undo log中分析出该行记录以前的数据是什么,从而提供该行版本信息,让用户实现非锁定一致性读取。
(3)delete/update操作的内部机制?
答:当事务提交的时候,innodb不会立即删除undo log,因为后续还可能会用到undo log,如隔离级别为repeatable read时,事务读取的都是开启事务时的最新提交行版本,只要该事务不结束,该行版本就不能删除,即undo log不能删除。
但是在事务提交的时候,会将该事务对应的undo log放入到删除列表中,未来通过purge来删除。并且提交事务时,还会判断undo log分配的页是否可以重用,如果可以重用,则会分配给后面来的事务,避免为每个独立的事务分配独立的undo log页而浪费存储空间和性能。
通过undo log记录delete和update操作的结果发现:(insert操作无需分析,就是插入行而已)
-
delete操作实际上不会直接删除,而是将delete对象打上delete flag,标记为删除,最终的删除操作是purge线程完成的。
-
update分为两种情况:update的列是否是主键列。
- 如果不是主键列,在undo log中直接反向记录是如何update的。即update是直接进行的。
- 如果是主键列,update分两部执行:先删除该行,再插入一行目标行。
“持久性”涉及到的一些东西
前面提到了InnoDB实现了两个事务日志,首先我们来聊一下redo log 存在的背景。
InnoDB作为MySQL的默认存储引擎(在5.5版本以后),数据是存放在磁盘中的,但如果每次读写数据都需要磁盘IO,效率会很低。为此,InnoDB提供了缓存(Buffer Pool),这个的Buffer Pool和redu log buffer区分(如下图所示),Buffer Pool中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲:
当从数据库读取数据时,会首先从Buffer Pool中读取,如果Buffer Pool中没有,则从磁盘读取后放入Buffer Pool;当向数据库写入(更新)数据时,会首先写入(更新)Buffer Pool,Buffer Pool中修改的数据会定期刷新到磁盘中(Buffer Pool -> 磁盘,这一过程称为刷脏)。
Buffer Pool的使用大大提高了读写数据的效率,但是也带了新的问题:如果MySQL宕机,而此时Buffer Pool中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证。
于是,redo log被引入来解决这个问题:当数据修改时,除了修改Buffer Pool中的数据(更新数据页),还会在redo log记录这次操作(往redo log buffer写日志);当事务提交时,会调用fsync接口对redo log(redo log buffer中的日志)进行刷盘。如果MySQL宕机,重启时可以读取redo log中的数据,对数据库进行恢复。redo log采用的是WAL(Write-ahead logging,预写式日志),所有修改先写入日志,再更新到Buffer Pool,保证了数据不会因MySQL宕机而丢失,从而满足了持久性要求。
既然redo log也需要在事务提交时将日志写入磁盘,为什么它比直接将Buffer Pool中修改的数据写入磁盘(即刷脏)要快呢?主要有以下两方面的原因:
(1)刷脏是随机IO,因为每次修改的数据位置随机,但写redo log是追加操作,属于顺序IO。
(2)刷脏是以数据页(Page)为单位的,MySQL默认页大小是16KB,一个Page上一个小修改都要整页写入;而redo log中只包含真正需要写入的部分,无效IO大大减少。
从上面一段话引出了很多问题,其中包括:
(1)什么是脏页?
答:Buffer Pool中数据页被修改以后,跟磁盘的数据页不一致。
(2)什么是redu log?
答:redo log通常是物理日志,记录的是数据页的物理修改,比如"在某个数据页上做了什么修改",而不是"这个数据修改后最新值为什么"。因此是需要先把磁盘的数据读入内存(数据页)再执行redo_log中的内容的(因此内存中的数据可能和磁盘中的数据不一致,称为脏页)。redo log主要用来做崩溃恢复,它用来恢复提交后的物理数据页(恢复数据页,且只能恢复到最后一次提交的位置)。redo log包括两部分:一是内存中的日志缓冲(redo log buffer),该部分日志是易失性的;二是磁盘上的重做日志文件(redo log file),该部分日志是持久的。
(3)刷脏和redo log有关系吗?
答:没关系。redo log只在数据恢复时使用,这是前提。在数据崩溃恢复时,先将磁盘数据读入到内存,然后再通过redo log更新内存页,更新完成后,内存页变成脏页,刷脏是单独的机制。
(4)正常运行中的实例,数据写入后的最终落盘,是从 redo log 更新过来的还是从 buffer pool 更新过来的呢?
答:其实从上面那段话已经可以知道,数据最终写入磁盘是从buffer pool(内存)中更新过去的,和redo log无关。
1.如果是正常运行的实例的话,数据页被修改以后,跟磁盘的数据页不一致,称为脏页。最终数据落盘,就是把内存中的数据页写盘。这个过程,甚至与 redo log 毫无关系。2.在崩溃恢复场景中,InnoDB 如果判断到一个数据页可能在崩溃恢复的时候丢失了更新,就会将它读到内存,然后让 redo log 更新内存内容。更新完成后,内存页变成脏页,就回到了第一种情况的状态。
(5)redo log buffer 是什么?是先修改内存,还是先写 redo log 文件?
答:其实在写redo log也不是直接写入磁盘的,而是会先写入到redo log buffer中,最后等事务提交的时候才会主动写到磁盘中。
begin;
insert into t1 ...
insert into t2 ...
commit;
这个事务往表中插入两条数据,首先会更新数据页,然后在redo log buffer中写入redo log日志,直到执行commit是才会把redo log写入磁盘(可能还包括undo log)。如图:
(6)为什么要调用fsync接口对redo log buffer中的日志进行刷盘?
答:因为MariaDB/MySQL是工作在用户空间的,MariaDB/MySQL的log buffer处于用户空间的内存中。要写入到磁盘上的log file中(redo:ib_logfileN文件,undo:share tablespace或.ibd文件),中间还要经过操作系统内核空间的os buffer,调用fsync()的作用就是将OS buffer中的日志刷到磁盘上的log file中。
也就是说,从redo log buffer写日志到磁盘的redo log file中,过程如下:
\
(7)调用fsync接口对存在redo log buffer中的redo log刷盘的时候崩溃了,会怎样?
答:MySQL支持用户自定义在commit时如何将log buffer中的日志刷log file中。这种控制通过变量 innodb_flush_log_at_trx_commit 的值来决定。该变量有3种值:0、1、2,默认为1。但注意,这个变量只是控制commit动作是否刷新log buffer到磁盘。
- 0表示每秒将"log buffer"同步到"os buffer"且从"os buffer"刷到磁盘日志文件中。也就是说设置为0时是(大约)每秒刷新写入到磁盘中的,当系统崩溃,会丢失1秒钟的数据。
- 1表示每事务提交都将"log buffer"同步到"os buffer"且从"os buffer"刷到磁盘日志文件中。这种方式即使系统崩溃也不会丢失任何数据,但是因为每次提交都写入磁盘,IO的性能较差。
- 2表示每事务提交都将"log buffer"同步到"os buffer"但每秒才从"os buffer"刷到磁盘日志文件中。
redo log 与 binlog
我们知道,在MySQL中还存在binlog(二进制日志)也可以记录写操作并用于数据的恢复,但二者是有着根本的不同的:
(1)作用不同:redo log是用于crash recovery的,保证MySQL宕机也不会影响持久性;binlog是用于point-in-time recovery的,保证服务器可以基于时间点恢复数据,此外binlog还用于主从复制。
(2)层次不同:redo log是InnoDB存储引擎实现的,而binlog是MySQL的服务器层(可以参考文章前面对MySQL逻辑架构的介绍)实现的,同时支持InnoDB和其他存储引擎。
(3)内容不同:redo log是物理日志,内容基于磁盘的Page;binlog的内容是二进制的,根据binlog_format参数的不同,可能基于sql语句、基于数据本身或者二者的混合。
(4)写入时机不同:binlog在事务提交时写入;redo log的写入时机相对多元:
- 前面曾提到:当事务提交时会调用fsync对redo log进行刷盘;这是默认情况下的策略,修innodb_flush_log_at_trx_commit参数可以改变该策略,但事务的持久性将无法保证。
- 除了事务提交时,还有其他刷盘时机:如master thread每秒刷盘一次redo log等,这样的好处是不一定要等到commit时刷盘,commit速度大大加快。
“隔离性”涉及到的一些东西
定义:与原子性,持久性侧重于研究事务本身不同,隔离性研究的是不同事务之间的相互影响。是指事务内部的操作与其他的事务是隔离的,并发执行的各个事务之间是不能相互干扰。隔离性,对应了事务隔离级别中的 Serializable,但是在我们的实际的开发中很少使用到可串行化。
隔离性追求的是并发情形下事务之间不会相互干扰,简单起见,我们仅考虑最简单的读操作和写操作(暂时不考虑带锁读等特殊操作),那么隔离性的探讨,主要可以分为两个方面:
- (一个事务)写操作对(另一个事务)写操作的影响:锁机制保证隔离性
- (一个事务)写操作对(另一个事务)读操作的影响:MVCC保证隔离性
MVCC
前面讲到了 RR解决了 脏读,不可重复读,幻读等问题使用到的就是MVCC(Multi-Version Concurrency Control) 既多版本的并发控制:在同一个时刻,不同的事物读取到的数据可能是不同的(多版本)。对于MVCC来说最大的优点就是读不加锁,因此读写不冲突,并发性能好
下图中,在T5时刻,事务A和事务C可以读取到不同版本的数据。
InnoDB实现MVCC,多个版本的数据可以共存,主要是依靠数据的隐藏列(也可以称之为标记位)和undo log。其中数据的隐藏列包括了该行数据的版本号、删除时间、指向undo log的指针等等;当读取数据时,MySQL可以通过隐藏列判断是否需要回滚并找到回滚需要的undo log,从而实现MVCC。
下面结合前面提到的几个问题说明:
「脏读」
当事务A在T3时间节点读取zhangsan的余额时,会发现数据已被其他事务修改,且状态为未提交。此时事务A读取最新数据后,根据数据的undo log执行回滚操作,得到事务B修改前的数据,从而避免了脏读。\
「不可重复读」
当事务A在T2节点第一次读取数据时,会记录该数据的版本号(数据的版本号是以row为单位记录的),假设版本号为1;当事务B提交时,该行记录的版本号增加,假设版本号为2;当事务A在T5再一次读取数据时,发现数据的版本号(2)大于第一次读取时记录的版本号(1),因此会根据undo log执行回滚操作,得到版本号为1时的数据,从而实现了可重复读。
「幻读」
InnoDB实现的RR通过next-key lock机制避免了幻读现象。
next-key lock是行锁的一种,实现相当于record lock(记录锁) + gap lock(间隙锁);其特点是不仅会锁住记录本身(record lock的功能),还会锁定一个范围(gap lock的功能)。当然,这里我们讨论的是不加锁读:此时的next-key lock并不是真的加锁,只是为读取的数据增加了标记(标记内容包括数据的版本号等);准确起见姑且称之为类next-key lock机制。
当事务A在T2节点第一次读取0<id<5数据时,标记的不只是id=1的数据,而是将范围(0,5)进行了标记,这样当T5时刻再次读取0<id<5数据时,便可以发现id=2的数据比之前标记的版本号更高,此时再结合undo log执行回滚操作,避免了幻读。
参考:
zhuanlan.zhihu.com/p/270209292
www.cnblogs.com/f-ck-need-u…
juejin.cn/post/692412…