首先,我们都知道数据库事务的四大特性:
- 原子性:事务是最小的工作单元,一个事务要么全部成功,要么全部失败
- 一致性:事务开始和结束之后,数据库的完整性不会遭到破坏,也就保证事务是从一个正确的状态变成另一个正确状态(正确状态:我们预期的状态)
- 隔离性:不同事务之间不会相互影响,隔离级别一共有四种,读未提交,读已提交,可重复读,串行化
- 持久性:事务提交之后,对数据的修改是永久的,即使系统出现故障也不会丢失
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是否在链表中,如果在说明,当前事务还未提交在当前版本中还未提交,当前版本不可见,如果不在,则数据可见。