MySQL事务运行原理

一、事务的特性

  • 原子性:指单个事务本身涉及到的数据库操作,要么全部成功,要么全部失败,不存在完成事务中一部分操作的可能;
  • 隔离性:指多个事务之间,没有相互影响干扰。
  • 一致性:指事务执行前后的状态要一致,没有脏数据。
  • 持久性:数据库一旦完成事务的提交之后,那么这个事务的状态就会持久在数据库中。

二、事务的分类

  • 扁平事务:最简单也是实际使用最频繁的一种事务,由 begin 开始,commit work 或者 rollback 结束,期间的操作都是原子性操作,要么执行,要么回滚
  • 带有保存点的扁平事务:
    • 除了支持扁平事务支持的操作外,允许在事务执行过程中回滚到同一事务中较早的一个状态
    • 保存点用来通知系统应该记住事务当前的状态,一旦事务过程中发生错误,事务能回到保存点当时的状态
  • 链事务
    • 可视为带有保存点的扁平事务的变种,当发生系统崩溃时,所有保存点都将消失,因为保存点是易失的而非持久的
    • 当进行恢复时,事务需要从开始处重新执行,而不能从最近的一个保存点继续执行
  • 嵌套事务:由一个顶层事务控制着各个层次的事务,顶层事务之下嵌套的事务称为子事务,其控制着每个局部的变换
  • 分布式事务:通常是一个在分布式环境下运行的扁平事务,因此需要根据数据所在的位置访问网络中的不同节点

三、事务的实现

3.1 原子性和持久性

MySQL的原子性和持久性是通过redo log来完成的。redo log 称作为重做日志。

3.2 一致性和隔离性

通常,MySQL通过undo log 用来保证事务的一致性。但事务的隔离级别不同,意味着事务的并发处理是不一样,而不同的并发处理方式对于数据库的一致性是有影响的。事务的隔离级别包括:

  • RAED UNCOMMITED:读未提交,任何操作都不加锁,所以能读到其他事务修改但未提交的数据行,也称之为脏读(Dirty Read);
  • READ COMMITED:读操作不加锁,写操作加锁。读被加锁的数据时,读事务每次都读undo log中的最近版本,因此可能对同一数据读到不同的版本,造成不可重复读
  • REPEATABLE READ:第一次读数据的时候就将数据加行锁(共享锁),使其他事务不能修改当前数据,即可实现可重复读。但是不能锁住insert进来的新的数据,当前事务读取或者修改的同时,另一个事务还是可以insert提交,造成幻读
  • SERIALIZABLE:InnoDB 锁表,读锁和写锁阻塞,强制事务串行执行,解决了幻读的问题;

3.3 事务的锁

根据读写,分为共享锁S和排它锁X

  • 共享锁:即读加锁,不能写并且可并行读
  • 排它锁:写加锁,其他读写都阻塞

根据加锁范围分为表锁、行锁、间隙锁

  • 表锁:锁整个表,性能开销最大,其他的读写都要挂起
  • 行锁:锁整个行,以默认隔离级别RC为例:如果是读,那么会上共享锁,不允许写,如果是写,那么改行其他事务无论读写都得阻塞.
  • 间隙锁:间隙锁分为两种,一种是不包含记录间隙锁(GAP),一种是包含记录间隙锁(Next-Key Lock: Gap Lock+Record Lock)

3.4 InnoDb 默认隔离级别下的MVCC

MVCC:多版本并发控制(MVCC,Multiversion Currency Control)。一般情况下,事务性储存引擎不是只使用表锁,行加锁的处理数据,而是结合了MVCC机制,以处理更多的并发问题。 MVCC的目标是追求数据库处理高并发能力,实现了非阻塞的读操作(解决了幻读、不可重复读的问题),但保存了两个额外的系统版本号,占用额外的存储空间,需要做更多的行检查工作,以及一些额外的维护工作。
实现原理:innodb存储的最基本row中包含一些额外的存储信息 DATA_TRX_ID,DATA_ROLL_PTR,DB_ROW_ID,DELETE BIT。

  • DATA_TRX_ID,6字节, 标记了最新更新这条行记录的transaction id,每处理一个事务,其值自动+1
  • DATA_ROLL_PTR:7字节,指向当前记录项的rollback segment的undo log记录,找之前版本的数据就是通过这个指针
  • DB_ROW_ID:6字节,当由innodb自动产生聚集索引时,聚集索引包括这个DB_ROW_ID的值,否则聚集索引中不包括这个值.,这个用于索引当中
  • DELETE BIT:位用于标识该记录是否被删除,这里是标志出来的删除。真正意义的删除是在commit的时候

在默认隔离级别RR下,MVCC的工作方式:

  • Select:
    • InnoDB只查找版本早于当前事务版本的数据行,保证事务读取的行, 要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的。
    • 行的删除版本定义,要么未定义,要么大于当前事务版本号。这可以确保事务读取到的行,在事务开始之前未被删除。
  • INSERT:InnoDB为新插入的每一行保存当前系统版本号作为行版本号。
  • DELETE:InnoDB为删除的每一行保存当前系统版本号作为行删除标识。
  • UPDATE:InnoDB为插入一行新记录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行删除标识。

四、MySQL日志

4.1 redo log

redo log是InnoDB存储引擎层的日志,又称重做日志文件,用于记录事务操作的变化,记录的是数据修改之后的值,不管事务是否提交都会记录下来。redo log日志的大小是固定的,即记录满了以后就从头循环写。

redo log包括两部分:一是内存中的日志缓冲(redo log buffer),该部分日志是易失性的;二是磁盘上的重做日志文件(redo log file),该部分日志是持久的。

为了确保每次日志都能写入到事务日志文件中,在每次将log buffer中的日志写入日志文件的过程中都会调用一次操作系统的fsync操作(即fsync()系统调用)。MySQL支持用户自定义在commit时如何将log buffer中的日志刷log file中。这种控制通过变量 innodb_flush_log_at_trx_commit 的值来决定。该变量有3种值:0、1、2,默认为1。但注意,这个变量只是控制commit动作是否刷新log buffer到磁盘。

  • 当设置为1的时候,事务每次提交都会将log buffer中的日志写入os buffer并调用fsync()刷到log file on disk中。这种方式即使系统崩溃也不会丢失任何数据,但是因为每次提交都写入磁盘,IO的性能较差。
  • 当设置为0的时候,事务提交时不会将log buffer中日志写入到os buffer,而是每秒写入os buffer并调用fsync()写入到log file on disk中。也就是说设置为0时是(大约)每秒刷新写入到磁盘中的,当系统崩溃,会丢失1秒钟的数据。
  • 当设置为2的时候,每次提交都仅写入到os buffer,然后是每秒调用fsync()将os buffer中的日志写入到log file on disk。

MySQL事务运行原理

4.2 logblock 

InnoDB 存储引擎中,重做日志都是以 512 字节进行存储的,重做日志缓存、重做日志文件都是以块(block)的方式进行保存的,称之为重做日志块(redo log block),每块大小为 512 字节。

4.3 undo log

undo log有两个作用:提供回滚和多个行版本控制(MVCC)。undo log和redo log记录物理日志不一样,它是逻辑日志。当执行rollback时,可以从undo log中的逻辑记录读取到相应的内容并进行回滚。undo log是采用段(segment)的方式来记录的,每个undo操作在记录的时候占用一个undo log segment另外,undo log也会产生redo log,因为undo log也要实现持久性保护。

undo log 格式:

  • insert undo log:insert 操作产生的 undo log ,在事务提交后就删除
  • update undo log:delete 和 update 操作产生的 undo log,该 undo log 可能需要提供 MVCC 机制,因此不能在事务提交时就删除

4.4 purge 

purge 用于最终完成 update 和 delete 操作,来支持 MVCC,是否可以完全删除由 purge 来判断,若该行记录已完全不被事务引用,那么就进行真正的 delete 操作。

4.5 group commit

对于InnoDB来说,事务提交的两个阶段

  • 修改内存中事务对应的信息,并且将日志写入重做日志缓冲
  • 调用 fsync 将确保日志都从重做日志缓冲写入磁盘

若事务为非只读事务,则每次事务提交时需要进行一次 fsync 操作,以此保证重做日志都已经写入磁盘,为了提高磁盘 fsync 的效率,当前数据库提供了 group commit 的功能,即一次 fsync 可以刷新确保多个事务日志被写入文件

4.6 binlog 二进制日志

mysql-binlog是MySQL数据库的二进制日志,用于记录用户对数据库操作的SQL语句((除了数据查询语句)信息。binlog的格式也有三种:STATEMENT、ROW、MIXED 。

  • STATMENT模式:基于SQL语句的复制(statement-based replication, SBR),每一条会修改数据的sql语句会记录到binlog中。优点是不需要记录每行数据的变化,减少磁盘IO,提高了性能。缺点是某些情况下,会导致master-slave中的数据不一致(如sleep()函数, last_insert_id(),以及user-defined functions(udf)等会出现问题)
  • 基于行的复制(row-based replication, RBR):不记录每一条SQL语句的上下文信息,仅需记录哪条数据被修改了,修改成了什么样子了。优点是:不会出现某些特定情况下的存储过程、或function、或trigger的调用和触发无法被正确复制的问题。缺点是会产生大量的日志,尤其是alter table后日志会暴涨。
  • 混合模式复制(mixed-based replication, MBR):以上两种模式的混合使用,一般的复制使用STATEMENT模式保存binlog,对于STATEMENT模式无法复制的操作使用ROW模式保存binlog,MySQL会根据执行的SQL语句选择日志保存方式。

4.7 binlog 和事务日志的先后顺序

事务在提交的时候不仅会记录事务日志,还会记录二进制日志,但是它们谁先记录呢?二进制日志是MySQL的上层日志,先于存储引擎的事务日志被写入。在MySQL5.6以前,当事务提交(即发出commit指令)后,MySQL接收到该信号进入commit prepare阶段;进入prepare阶段后,立即写内存中的二进制日志,写完内存中的二进制日志后就相当于确定了commit操作;然后开始写内存中的事务日志;最后将二进制日志和事务日志刷盘,它们如何刷盘,分别由变量 sync_binlog 和 innodb_flush_log_at_trx_commit 控制。

但因为要保证二进制日志和事务日志的一致性,在提交后的prepare阶段会启用一个prepare_commit_mutex锁来保证它们的顺序性和一致性。但这样会导致开启二进制日志后group commmit失效,特别是在主从复制结构中,几乎都会开启二进制日志。在MySQL5.6中进行了改进。提交事务时,在存储引擎层的上一层结构中会将事务按序放入一个队列,队列中的第一个事务称为leader,其他事务称为follower,leader控制着follower的行为。虽然顺序还是一样先刷二进制,再刷事务日志,但是机制完全改变了:删除了原来的prepare_commit_mutex行为,也能保证即使开启了二进制日志,group commit也是有效的。

MySQL5.6中分为3个步骤:flush阶段、sync阶段、commit阶段。

MySQL事务运行原理

  • flush阶段:向内存中写入每个事务的二进制日志。
  • sync阶段:将内存中的二进制日志刷盘。若队列中有多个事务,那么仅一次fsync操作就完成了二进制日志的刷盘操作。这在MySQL5.6中称为BLGC(binary log group commit)。
  • commit阶段:leader根据顺序调用存储引擎层事务的提交,由于innodb本就支持group commit,所以解决了因为锁 prepare_commit_mutex 而导致的group commit失效问题。

参考文献:

https://learnku.com/articles/33779

https://www.cnblogs.com/DataArt/p/10209573.html

https://blog.csdn.net/lzhcoder/article/details/88814364