事务是指一个或者多个数据库操作,要么全部没有执行,要么全部成功执行。
中途失败需要回滚到指定状态,全部执行成功需要确保持久保存在数据库中。
事务拥有四个特性,习惯上被称之为ACID特性。
为了更直观的解释ACID特性,下面先说明A, B, C之间互相转账的过程。
假设A有10元,B有15元,C有8元
A给B转账5元,操作记为T1。
T1: read(A), A=A-5, write(A), read(B), B=B+5, write(B)。
T1操作的大体流程,先读取A到账户余额,将A的账户余额扣减5元后再写入数据库中,
读取B的账户余额,将B的账户余额增加5元后再写入到数据库中。
同时,C给B转账4元,操作记为T2。
T2: read(C), C=C-4, write(C), read(B), B=B+4, write(B)
T1操作的大体流程,先读取C的账户余额,将C的账户余额扣减4元后再写入数据库中,
读取B的账户余额,将B的账户余额增加4元后再写入到数据库中。
事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行,是一个最小执行单元,不可分割。
A给B转账5元的操作T1是包含多个读写操作,这些操作要么全部执行,要么全部不执行。
假设由于断电等意外事件,导致T1只执行了部分操作,如T1:read(A), A=A-5, write(A)
这就会导致A凭空少了5元,并且B没有收到A转的5元,
因此事务需要保证保证在事务执行过程中出现错误时,将已经执行的操作“撤销”,恢复到原始状态。
事务应确保数据库的状态从一个一致状态转变为另一个一致状态。
假设A有10元,B有15元,C有8元,不管A, B, C之间如何进行转账(没有其他人参与),三个点账户总余额一定是33元,而不会是其它值。
多个事务并发执行时,一个事务的执行不应影响其他事务的执行。
A给B转账5元,同时C给B转账4元,这个两个事务应该是互相隔离的,互不影响。
最终A余额为5元,C余额为4元,B总共收到两次转账,余额应该为24元。
假设T1, T2可以交叉执行,如下图所示。最终结果看起来B只收到了A的5元转账,余额为20元。
已被提交的事务对数据库的修改应该永久保存在数据库中。
MySQL操作一般是先写入缓存,满足指定条件后才将缓存更新到磁盘
磁盘写操作相当耗时,而且同一个事务可能修改多个数据页面,而且可能执行页面中一个字节的数据。
因此每次数据库提交执行缓存刷新磁盘操作不太合理,
MySQL设计人员通过redo日志来持久化最小量的数据来达到相同的效果。
为了保证事务的持久性,在事务提交动作完成之前,需要把该事务修改所有页面都刷新到磁盘,但是存在以下问题
为了解决上述到问题,InnoDB设计了redo日志,把事务修改的内容采用特定的格式按顺序保存到磁盘上,
即使在系统崩溃之后,按照redo日志重新更改数据页进行数据恢复即可。
redo日志通用格式如下图所示
往表中插入一条记录,可能产生多条redo日志,因为可能产生聚簇索引对应B+树页面的分裂操作,
可能需要性申请数据页,金额可能要修改各种段、区的统计信息等,
最终插入一条记录可能产生多条redo日志,这些日志是不可分割的,
在崩溃恢复时,也是将这一组日志作为不可分割的整体来处理,
类似的,将一组不可分割的redo日志称为Mini-Transaction,即MTR
为了避免频繁的磁盘IO,并不是每生成一条redo日志就同步到磁盘上。
而是先将redo日志放到缓冲区,在特定时机刷新到磁盘。
redo日志缓冲区页面结构如下图所示。
redo日志是以MTR为单位写入到redo日志缓冲区的,redo日志缓冲区是有若干个512B大小的block构成的一片连续的内存空间,
InnoDB引擎使用lsn(log sequence number)来记录系统当前有多少redo日志写入到缓冲区
InnoDB会将lsn相关信息写入到flush链表中,进而方便判断哪些redo日志文件可以被重复使用,
因为只要脏页被刷新到磁盘,相应的redo日志内容就没有存在的意义了,而且redo日志文件大小也有限。
Buffer Pool中页面会在控制块中记录页面的修改信息
flush链表与oldest_modification(o_m)和newest_modification(n_m)的关系如下图所示。
flush链表的基节点start指针出发,flush链表的脏页时按照第一次修改发生的时间倒序排列的,也就是按照oldest_modification代表的lsn值倒叙排列,
当页面被多次更新时,会更新对应页面的newest_modification变量的值。
当页面1被属性到磁盘,从页面2的控制块可以看出,lsn低于8916的redo日志可以被覆盖,系统会将8916赋值到redo文件的checkpoint_lsn的操作。
InnoDB将检查flush链表最小的oldest_modification的lsn值称为checkpoint操作。
磁盘上存在多个redo日志文件,会被循环使用,这一组redo日志文件称为redo日志文件组。
和redo日志缓冲区一样,redo日志文件也是由若干个512B构成的block组成
其中,redo日志文件的头2048个字节用于存储一些管理信息,系统会将checkpoint操作得到的checkpoint_lsn赋值到checkpoint1的checkpoint_lsn上。
崩溃恢复会从checkpoint_lsn在日志文件组中对应的偏移量开始。
除了前面阐述的checkpoint,redo日志刷盘时机还包括
当遇到异常情况导致服务器挂掉,在重启时可以根据redo日志文件恢复到奔溃前的状态。
InnoDB从redo日志文件组的第一个文件的checkpoint信息,然后从checkpoint_lsn在日志文件组中对应的偏移量开始,
一直扫描日志文件中的,直到某个block的写入量的值不等于512,根据redo日志格式将修改的内容恢复到奔溃前状态。
在事务执行过程中可能遇到各种错误,导致中途就结束事务了,但是在遇到错误退出前,可能修改多个行记录,
但是为了保证事务的原子性,需要将数据恢复到事务开启前,这个恢复过程就称为回滚。
为了回滚,就需要将事务修改的内容记录下来,包括插入的行记录、修改行记录的内容、删除的行记录,
保存事务执行过程中修改内容的东西称为undo日志。
InnoDB会将聚簇索引行结构如下图所示
InnoDB会将聚簇索引行结构补充trx_id和roll_pointer两个隐藏列
roll_pointer结构如下图所示
如果需要回滚插入操作,只需要将插入的记录删除即可,
因此在记录undo日志时,只需要记录插入的记录的主键信息即可,通过主键能找到唯一的记录
插入操作的对应的undo日志类型为TRX_UNDO_INSERT_REC,结构如下图所示
在事务中执行删除操作,会将记录的deleted_flag标识为值为1,但该记录依然在正常记录链表,并没有移动到垃圾记录链表,这个过程称为delete mark。
在事务提交后,才把该记录从正常记录链表挪到垃圾记录链表
删除操作产生TRX_UNDO_DEL_MARK_REC类型的日志,结构如下图所示,
在对一条记录进行delete mark操作前,将该记录的trx_id和roll_pointer的旧值保存到undo日志的trx_id和roll_pointer变量中。
假设一个事务对某条记录先更新再删除,这样就能通过TRX_UNDO_DEL_MARK_REC找到更新的undo日志。
更新操作的场景较复杂,InnoDB将其分为更新主键和不更新主键两种场景。
针对以上各种情况,InnnoDB设计了对应的undo日志格式,限于篇幅这里就不展开说明。
和InnoDB普通页面结构类型,undo日志页面结构及页面链表如下图所示。
一个undo日志页面只能存储一种类型,InnoDB将undo日志分为两大类,
InnoDB对临时表和普通表产生的undo日志分开记录,因此一个事务最多可能需要4个undo页面链表。
同一个时刻,可能存在多个事务在执行,为了更好的管理undo页面链表,
InnoDB设计了Rollback Segment Header页面用于存放各个Undo页面链表的第一个undo页面的页号,即undo slot。
在奔溃恢复时,需要将未提交事务的修改回滚掉,通过undo slot找到undo页面链表,
通过判断undo页面链表的Undo Log SegmentHeader的TRX_UNDO_STATE属性值,
如果为TRX_UNDO_ACTIVE,则进一步通过undo链表最后一个页面的Undo Log Header中找到该事务对应的事务ID,
然后通过undo日志内容将该事务修改的内容全部撤销,从而保证事务的原子性。
脏写(Dirty Write)
一个事务修改了另外一个未提交事务修改过的数据。
脏读(Dirty Read)
一个事务读取了另外一个未提交事务修改过的数据。
不可重复读(Non-repeatable Read)
一个事务修改了另一个未提交事务读取的数据。
幻读(Phantom)
一个事务根据某些搜索条件查询出一些记录,在该事务未提交时,另一个事务写入了一些符合搜索条件的记录,
再次以相同条件查询,前后两次结果不一致。
SQL标准中定义了四种隔离级别
读未提交(Read Uncommitted), 读已提交(Read Committed), 可重复读(Repeatable Read), 串行化(Serializable)。
不同隔离级别对应的可能和不可能发生的一致性问题如下图所示。
其中脏写问题是不允许发生的。
InnnoDB存储引擎的聚簇索引的版本链如下图所示
假设在某个时刻,事务321、315、301对某条记录进行Updata操作后,形成的版本链如下图所示。其中事务301对这条记录更新了两次
Multi-Version Concurrency Control, 即多版本并发控制,
多版本并发控制机制利用聚簇索引的版本链来控制并发事务访问相同记录时的行为,从而解决脏读和不可重复读的不一致性问题。
ReadView,即一致性视图,通过这个视图可以判断版本链的某个版本是否可被当前事务访问,中ReadView包含以下4个比较重要的数据
判断是否可见的规则
在读已提交和可重复读隔离级别下,ReadView生成的时机有所不同,