深入分析Innodb的事务

1,027 阅读9分钟

首先,我们都知道数据库事务的四大特性:

  • 原子性:事务是最小的工作单元,一个事务要么全部成功,要么全部失败
  • 一致性:事务开始和结束之后,数据库的完整性不会遭到破坏,也就保证事务是从一个正确的状态变成另一个正确状态(正确状态:我们预期的状态)
  • 隔离性:不同事务之间不会相互影响,隔离级别一共有四种,读未提交,读已提交,可重复读,串行化
  • 持久性:事务提交之后,对数据的修改是永久的,即使系统出现故障也不会丢失

Innodb架构

上面是Innodb存储引擎的架构,我们可以直观的看到存储引擎由内存池,后台线程,和磁盘文件三大部分组成。

Buffer Pool缓冲池

用于处理数据,page是Innodb存储的最基本结构,也是Innodb磁盘管理的最小单位,数据变更的时候,缓存里的数据页和磁盘的数据页不一致,该数据页被称为脏页。

Redo Log Buffer 重做日志缓冲

redo log保证数据的可靠性,存储数据之前首先要存储变更数据的日志,一旦系统出现故障可以从日志中找。Innodb采用了Write Ahead Log(预写日志)策略,就是当事务提交时,先写重做日志,然后再择时将脏页落盘。Force Log at Commit机制保证事务的持久性,事务提交的时候,必须先将该事务的所有日志记录落盘,在每次将重做日志缓冲写入到重做日志后,必须调用一次fsync操作,将缓冲日志从文件系统缓存真正写入磁盘

重做日志的落盘机制

redo log落盘机制有三种,可以通过参数innodb_flush_log_at_trx_commit进行设置,默认值为1

  • 0:事务提交的时候不进行重做日志的写入,而且每隔固定的时间写入操作系统缓存并落盘
  • 1:事务提交的时候直接写入到缓存并刷新到磁盘
  • 2:事务提交的时候先写到操作系统的缓存,然后每隔固定的时间再将操作系统缓存中的数据刷新到磁盘

数据落盘机制

上图是数据落盘和redo log日志落盘的机制,我们说下这个数据落盘时候的双写机制和检查点

Double Write

Innodb通过双写机制保证数据的可靠性。Double Write由两个部分构成,一个是内存中的double write buffer,大小为2MB。第二部分是物理磁盘,共享表空间中的128个连续的页,大小也是2MB。

double write 过程

在对缓冲池中的脏页进行刷新的时候,并不是直接将数据写到磁盘。首先,通过mencpy函数将脏页复制到内存中的double write buffer区,然后将double write buffer中的数据分两次,每次1MB,将数据顺序地写入到共享表空间的磁盘上,然后马上调用fsnyc真正地落盘。完成之后,再讲double write buffer中的数据写入到各个表空间中。如果操作系统在写入磁盘的过程中崩溃了,可以从共享表空间中找到数据副本恢复数据。

checkpoint 检查点

表示将脏页写入到磁盘的时机:

  • 目的:用于缩短数据库的恢复时间,buffer pool空间不够时,将数据刷新到磁盘,redo log不够用的时候刷新脏页
  • 分类
    • sharp checkpoint:完全检查点,数据库正常关闭的时候,会触发把所有的脏页都写入磁盘
    • fuzzy checkpoint:模糊检查点,使用过程中会触发。
      • master thread checkpoint:固定时间将缓冲池中的脏页按一定比例落盘,异步落盘
      • flush_lru_list checkpoint:读取lru列表,将最近最少使用的数据刷新到磁盘
      • flush checkpoint:redo log file快满了的时候回触发批量的数据落盘,这个事件触发的时候有两种情况,不可被覆盖的redo log占log file的比值为75%触发异步刷新,大于等于90%会触发同步落盘。

undo log

当数据库崩溃之后会利用redo log将还没有及时落盘的数据恢复,重新写入磁盘。在恢复的过程中还需要去回滚还没有提交的事务,回滚事务就需要利用到undo log,而undo log的完整性和可靠性需要redo log保证,因此恢复数据库的时候,首先将redo log里面数据恢复,然后做undo log的回滚。

undo log的存储

数据和回滚日志的每条记录都会有三个额外的字段:

  • rowid:行id
  • Trx id:事务id
  • Roll Pointer:回滚指针,指向上一个历史版本

undo log并没有使用额外的文件存储,而是存放在共享表空间的回滚段中。undo log的产生也可以看成是数据库的数据,因此,undo log 也会写入到redo log中,也就是undo log 的产生会伴随着redo log的产生,undo log的完整性和可靠性也是由redo log来保证

原子性,一致性,持久性原理分析

原子性,一致性和持久性主要是通过redo log,undo log和Force Log at commit机制来完成的。redo log用在数据库崩溃的时候,从redo log恢复数据,undo log用于对事务的影响进行撤销,也就是回滚,还用于多我们后面会讲到的多版本控制。Force log at commit保证事务提交之后能够持久化到redo log。

隔离性

事务的并发问题

  • 丢失更新:两个事务针对同一个数据进行更新的时候,会存在丢失更新问题
  • 脏读:一个事务读取到另一个事务未提交的数据
  • 不可重复度:一个事务对同一条记录读取两个的结果不一样
  • 幻读:一个事务对同一张表读取两次结果不一致

针对上面的四个问题,事务之间有不同的隔离级去解决上面的问题

四种隔离级别

  • RU(读未提交):最低的隔离级别,无法解决上面的任何问题
  • RC(读已提交):可以避免脏读的发送,只有提交的数据才能被读取
  • RR(可重复读):可以避免脏读和不可重复读(Innodb中,RR还可以用于解决幻读,主要是通过Next——Key锁实现,关于锁的相关问题,可以阅读上一篇文章:juejin.cn/post/684490…
  • Serializable(串行化):可以避免脏读,不可重复读,幻读的发生,但效率是最低的

Innodb的MVCC实现

在Innodb中,使用的是MVCC来保证事务之间的隔离,MVCC使得普遍的select语句不会加锁,提高数据库的并发处理能力

什么是MVCC

MVCC:多版本并发控制,是一种并发控制的方法,用来实现数据库的事务性。

当前读和快照读

在MVCC中,读操作可以分成两种

  • 当前读:读取的是记录的最新版本,并且读到的记录,都会加锁,保证其他事务不会再修改这条记录
  • 快照读:读取的是记录的可见版本,有可能读的是历史版本,不需要额外加锁,就是select语句

在Innodb中简单的select语句属于快照读,不加锁,读的是历史版本。而其他的增删改语句属于当前读,需要加锁,读的是当前版本。

一致性非锁定读

一致性非锁定读:Innodb引擎通过MVCC读取当前数据库中行数据的方式。如果读取的是正在执行删除或者更新操作的记录,那么本次读操作不会因此阻塞去等待锁的释放。而会去读取该行的一个最新的可见快照。

MVCC的实现原理

MVCC的实现主要依赖的是undo log和read view(事务链表)

undo log

我们知道undo log的行记录中有三个隐藏字段:分别对应该行的rowid、事务号db_trx_id和回滚指针db_roll_ptr,其中db_trx_id表示最近修改的事务的id,db_roll_ptr指向回滚段中的undo log。 根据行为的不同,undo log分为两种

  • insert undo log:insert中产生的undo log,insert操作只对当前事务可见,rollback在该事务中是直接删除的,因此不需要进行purge操作(清除操作,通过purge Thread实现)
  • update undo log:更新或者删除操作产生的undo log,这两类操作会对已经存在的记录产生影响,为了被MVCC读取,不能在事务提交的时候进行删除,而是在事务提交之后放到历史列表中,等待purge线程进行最后的删除。

read view

事务链表,mysql中的事务在开始到提交的阶段都会被保持在一个叫trx_sys的事务链表中啊,事务链表中保持的都是还未提交的事务,事务一旦被提交,就会从链表中删除该事务。

  • RR隔离级别下,每个事务开始的时候会将当期系统中所有活跃的事务拷贝到链表中
  • RC隔离级别下,每个语句开始的时候,会将当前系统中的所有活跃的事务拷贝到一个链表中。

说道这里,大家就应该清楚了为什么RR可以避免不可以重复读而RC不行,因为两者获取事务链表的方式不同,RC每个语句都会重新获取,因此可以读取到其他事务最新提交的数据。 我们可以通过show engine innodb status语句查看事务链表

如何读历史版本

我们知道事务开启的时候会获取事务链表,这个类中存储了当前链表中最大的事务ID和最小的事务id。 假设当前活跃的事务链表如下:

ct-trx-->trx11 --> trx10 --> trx9 --> trx5 --> trx2;

ct-trx表示当前事务id,对应的read_view数据结构如下

read_view->creator_trx_id = ct-trx; 
read_view->up_limit_id = trx2; 低水位 
read_view->low_limit_id = trx11; 高水位 
read_view->trx_ids = [trx11, trx10, trx9, trx5, trx2];
  • low_limit_id:高水位,及当前活跃事务的最大id,如果读到的row的事务id>=low_limit_id,说明这些id在此之前的数据都没有提交,数据都不可以见
  • up_limit_id:低水位,及最小的事务id,同理,如果读到的row的事务id<=up_limit_id,说明这些数据在事务创建的时候id都已经提交了,数据均可见。
  • 当事务id在两者之间的时候,则查找该记录的当前记录的事务id是否在链表中,如果在说明,当前事务还未提交在当前版本中还未提交,当前版本不可见,如果不在,则数据可见。