MySQL数据库本地事务原理解析
在经典的数据库理论里,本地事务具备四大特征:
- 原子性
事务中的所有操作都是以原子的方式执行的,要么全部成功,要么全部失败;
- 一致性
事务执行前后,所有的数据都应该处于一致性状态---即要满足数据库表的一致性约束,也要达到业务一致性(完成了业务目标);
- 隔离性
并发执行的事务不应该相互干扰;隔离性的强度由隔离级别决定;
- 持久性
事务一旦被提交,它添加/修改的数据不会随着系统崩溃而丢失;
在MySQL(InnoDB引擎)中,原子性和持久性是通过Redo Log来实现的,一致性是通过Undo Log实现的,而隔离性则是通过锁和MVCC来实现的。
ARIES算法
如果需要深入了解数据库本地事务原理,不得不提到ARIES算法,该算法全称为Algorithms for Recovery and Isolation Exploiting Semantics(基于语义的恢复与隔离算法),众多主流的关系型数据库都受到该算法的影响。
ARIES算法主要针对使用No Force + Steal的数据写入策略而采用的一种数据恢复方式。
该算法主要基于三个主要的原则:
- Write-ahead logging
出于性能上的考虑,数据的修改都是在内存中进行,并将这些“修改操作”记录到日志(Redo Log和Undo Log)中,然后异步将内存中的数据写入到磁盘;
- 通过Redo Log恢复数据
Redo Log用于记录事务对数据的修改操作,在数据库崩溃恢复时,ARIES通过Redo Log重放那些还未写入到数据库磁盘中的数据操作,将数据恢复至崩溃前的状态;
- 通过Undo Log回滚数据
对崩溃前未提交的事务,通过Undo Log进行回滚;
Write-ahead logging
每个事务执行时,都是在内存中进行数据的修改,并将这些“修改操作”记录到日志,然后将内存中的数据异步写入到磁盘里;
但日志也并非立刻写入至磁盘,而是先写入到Log Buffer,再按照相应的参数配置进行磁盘的写入操作;在写入至磁盘时,数据会先写入至操作系统内核缓冲区(OS Buffer),然后根据参数配置决定对内核缓冲区中的数据同步或异步刷盘。
如在InnoDB中,Redo Log的磁盘写入策略是由innodb_flush_log_at_trx_commit参数值来决定的:
0: 当参数值设置为0时,每隔1秒将Redo Log Buffer中的数据写入至OS Buffer,并同时调用fsync()函数完成刷盘操作;
1: 每次事务提交时,立即将Redo Log Buffer中的数据写入至OS Buffer,并同时调用fsync()函数完成刷盘操作;
2: 每次事务提交时,立即将Redo Log Buffer中的数据写入至OS Buffer,每隔1秒调用fsync()函数完成刷盘操作;
由此可见,当innodb_flush_log_at_trx_commit设置为0或2时,都会导致日志数据丢失;
以上讨论了“数据操作”日志的写入方式,而对于事务中真正修改的数据,Write-ahead logging根据事务提交的时间节点,将变动的数据写入至磁盘的时间节点分为Force和Steal两种:
Force:在事务提交时,是否强制将变动的数据完全写入至磁盘?
Steal:在事务提交前,是否允许将变动的数据提前写入至磁盘?
因此根据Force和Steal的值,数据的写入策略可以分为以下四种:
Steal | No Steal | |
Force | 事务提交时,强制将变动数据完全写入至磁盘 事务提交前,允许将变动的数据提前写入至磁盘 | 事务提交时,强制将变动数据完全写入至磁盘 事务提交前,不允许变动的数据提前写入至磁盘 |
No Force | 事务提交时,不需要强制变动数据完全写入至磁盘 事务提交前,允许将变动的数据提前写入至磁盘 | 事务提交时,不需要强制变动数据完全写入至磁盘 事务提交前,不允许变动的数据提前写入至磁盘 |
直观感觉就可以知道,采用No Force + Steal的方式,不需要在事务提交时,强制将所有的变动数据写入至磁盘,同时允许变动的数据在事务提交前即可提早写入至磁盘;这样的写入策略灵活性强且性能最好;MySQL InnoDB采用的就是此种写入方式。
Redo Log
Physiological Logging
在崩溃并重启后,数据库重放Redo Log进行数据恢复时,由于并不知道崩溃前哪些变动的数据已经写入到物理磁盘,因此需要保证Redo Log的重放是幂等的,即多次重放得到的结果不会改变;
InnoDB中所有的数据都是以数据页(Page)的形式存在于磁盘中的,因此Redo Log中的每一条日志,会记录被修改的数据页Page ID、被修改的记录在该Page中的位移、记录中哪些字段被修改了、修改后的字段值:
(Page ID,Record Offset,(Filed 1, Value 1) … (Filed i, Value i) … )
一个事务可能修改多条记录(这些记录可能位于同一个数据页,也可能位于不同的数据页),就会产生多条日志;同时数据库的多个事务都是并行执行的,出于性能的考虑,它们在Redo Log中并非以串行的方式写入,而是多个事务产生的多条日志互相穿插在Redo Log中,这就导致了Mini Transaction(Mtr)的产生。Mtr是数据库事务在Redo Log中的最小存储单元,一个数据库事务被划分为一个或多个Mtr,一个Mtr仅包含对一个数据页的修改(由于一个数据页可能包含多条记录,因此一个Mtr中包含的日志记录也不止一条)。
虽然同一个事务的多个Mtr在Redo Log中可能是不连续的,但同一个Mtr中包含的多条日志在Redo Log中一定是连续的。
我们把Redo Log这样的的存储方式称之为Physiological Logging。
LSN机制
Redo Log不是一个无限膨胀的日志文件,它具有固定的长度,日志先按照物理顺序一直往后添加,当达到空间限制后跳转到开始位置重新进行写操作/覆盖。
Redo Log中的记录也并非永远具有存在的价值,当事务所操作的数据已经被写入到物理磁盘中,这个事务对应的日志就没有存在的意义,事实上是可以被删除了。
MySQL数据库使用CheckPoint的值来指定日志文件可以擦除的位置,也就是说该位置之前的日志都是可以删除的;CheckPoint的值用LSN来表示。
LSN(Log Sequence Number)并非物理位置,它是一个8字节(64位)的整数且单调递增,代表着自数据库启动以来,至当前时间点写入至Redo Log中数据的总量(字节数)。
Redo Log中的每一条日志也使用LSN作为它的标识。
Double Write Buffer
上文谈到数据库通过异步的方式将修改的数据写入至物理磁盘,但如果无法保证数据写入到物理磁盘的原子性,当恰好写入了部分数据后发生崩溃,这会导致物理磁盘中存在一个被损坏的数据页;而Redo Log只记录哪些数据页被修改,但不会记录哪些数据页被损坏,因此无法通过Redo Log来修复这些被损坏的数据页;
MySQL InnoDB使用Double Write Buffer来解决写数据至物理磁盘时崩溃后数据页的恢复问题;
Double Write Buffer是一个存储区,当InnoDB尝试写数据页至物理磁盘之前,会先将数据页写入至该区域;如果写数据时崩溃,恢复时InnoDB可以从Double Write Buffer中找到这个被损坏的数据页的完整副本。
正如名称Double Write所示,这会导致两次磁盘写操作:一次是写入到Double Write Buffer,另外一次是写入到真正的数据页所在的磁盘位置。
另外一个问题:如果日志从Redo Log Buffer写入至磁盘时,数据库崩溃了该如何处理?
Redo Log写入至磁盘是通过日志块(Log Block)的方式进行写入的,日志块不同于内存中的数据页(1 Page = 16 KB),一个日志块的大小为512 Byte。每个日志块中包含该块的摘要值(CheckSum),通过该摘要值可判断写入至磁盘的这个日志块是否完整。
当innodb_flush_log_at_trx_commit设置为2时,事务的提交是以日志写入磁盘作为结束标志的,如果写入时崩溃,则代表事务提交失败,该日志块实际上可以直接丢弃。
Undo Log
在上文Redo Log的Physiological Logging中谈到,Redo Log只会记录某个数据字段修改后的值,在数据库崩溃后恢复阶段会利用Redo Log对事务中的数据操作进行重放,其中包括已提交的事务和未提交的事务;对于那些未被提交的事务,需要使用Undo Log对其进行回滚操作;
与Redo Log使用的Physiological Logging格式不同,Undo Log保存的是逻辑日志;如果事务执行的是一个INSERT操作,Undo Log会保存一条DELETE操作;如果事务中执行的是一个DELETE操作,Undo Log会保存一条INSERT操作;如果事务执行的是一条UPDATE操作,Undo Log中会保存一条反向的UPDATE操作......
Crash Recovery(崩溃恢复)
CheckPoint机制
InnoDB使用了一个叫做Fuzzy Checkpointing的CheckPoint机制来实现数据页写入至磁盘;它并非一次性的将所有内存中的数据页全部写入到磁盘中,因为这会阻塞在写入时其他的数据库操作,因此它采用小批量(Small batches)写入。
当数据库崩溃时,并非所有Redo Log中的事务(包括已提交和未提交的)所修改的数据页都已经写入到了物理磁盘中,我们把那些还未写入的数据页叫做脏页(Dirty Pages)。
在崩溃恢复时,需要找到所有的这些脏页,并利用Redo Log进行重放,也需要找出所有未提交的事务,利用Undo Log进行回滚。
由于Fuzzy Checkpointing只是小批量写入,因此并非所有已提交事务的数据页都写入至磁盘中;同时由于多个事务(包括已提交和未提交的)会修改同一个数据页,这会导致在数据页写入时可能将未提交事务的数据也写入到磁盘中了;所以在每一次Fuzzy Checkpointing之后,会把该次Fuzzy Checkpointing时未提交的事务列表和脏页列表形成为一个CheckPoint日志,保存到Redo Log中。
在崩溃恢复过程中,InnoDB引擎会找到Redo Log中最近一次CheckPoint日志,获取到未提交的事务列表和脏页列表,并以该日志为起点遍历至Redo Log末尾;在遍历过程中,如果遇到事务提交,将其从未提交事务列表中移除,如果遇到新事务开始,将它加入到未提交事务列表;同时对遍历到的所有Physiological Log,都添加到脏页列表;最后会形成一个最终的未提交事务列表和脏页列表。
对脏页列表,在Redo Log中找到最早的那个脏页所对应的日志,并以此为起点进行Redo Log重放。此时可能会遇到的Redo Log对应的数据页实际已经写入至磁盘中了,不过即使再次重放也没有关系,因为Redo Log是幂等的。
对所有未提交的事务列表,找到其对应的Undo Log,并进行回滚操作。
到此这篇关于MySQL数据库本地事务原理的文章就介绍到这了,更多相关MySQL数据库事务原理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
最新评论