Innodb引擎的官方文档地址:https://dev.mysql.com/doc/refman/8.0/en/innodb-storage-engine.html

了解Innodb的锁和MVCC前,我们应该先了解MySQL支持的存储引擎及事务,以及各个事务隔离级别的区别

存储引擎

MySQL的存储引擎是插件式的体系结构,每个存储引擎都有其各自的特点,我们可以根据具体的应用需求使用不同的存储引擎表,现在最常用的存储引擎是Innodb。

可以使用show engines;命令查询存储引擎

MySQL中有很多存储引擎,数据库中的每一个表都可以使用不同的存储引擎。

Support表示某种引擎是否能使用:YES表示可以使用、NO表示不能使用、DEFAULT表示该引擎为当前默认的存储引擎 。

MEMORY引擎

Memory存储引擎的数据存储在内存中,但是服务重启或发生崩溃时数据会丢失,该引擎适合用在临时表中存储临时数据。

特点:

  1. Memory存储引擎支持hash索引和B+树索引,默认使用hash索引。
  2. 存储在Memory数据表里的数据必须使用的是长度不变的格式,所以BLOB和TEXT这样的长度可变的数据类型是不能使用的,VARCHAR在MySQL内部当做长度固定不变的CHAR类型,所以可以使用。

ARCHIVE引擎

Archive是归档的意思,在归档之后仅仅支持最基本的插入和查询两种功能。Archive拥有很好的压缩机制,它使用zlib算法将数据行压缩后进行存储,在记录被请求时会实时压缩,所以在使用大量的数据采集时可以使用,比较适合用来存储日志信息,提供高速的插入和压缩功能。

特点:

  1. ARCHIVE引擎只支持insert和select操作,不支持update和delete操作。
  2. ARCHIVE引擎的数据文件比较小,占用的磁盘空间更少。

MyISAM引擎

MyISAM存储引擎不支持事务,也不支持外键;访问速度快,对事务完整性没有要求或者以查询、插入为主的应用可以使用这个引擎来创建表。每个MyISAM在磁盘上存储成表定义文件、索引文件和数据文件3个文件

特点:

  1. MyISAM引擎中存储表的数据和索引需要分开存储。
  2. MyISAM中不支持事务以及行锁,只能使用表级锁,所以并发性能较低。

InnoDB引擎

Mysql5.5及以后版本的默认存储引擎,innoDB存储引擎支持事务且引入了行级锁和外键约束等功能。

特点:

  1. InnoDB引擎在存储表时会以聚集索引的形式进行数据存储,也就是索引和数据文件在同一个文件上。
  2. InnoDB支持行锁,采用MVCC支持高并发,并且支持事务。

事务

InnoDB与MyISAM的最大不同有两点:一是支持事务;二是采用了行级锁。行级锁与表级锁本来就有许多不同之处,另外,事务的引入也带来了一些新问题。下面我们先介绍一点背景知识,然后详细讨论InnoDB的锁问题。

事务是数据库操作的最小工作单元,是作为单个逻辑工作单元执行的一系列操作;是一组不可再分割的工作单元

银行的账户转账就是一个典型的事务例子,一个账户的扣除余额操作和一个账号的增加余额操作是两条sql语句,但是必须保证同时成功或者同时失败。

事务ACID特性

  • 原子性(Atomicity)
    最小的工作单元,整个工作单元要么一起提交成功,要么全部失败回滚
  • 一致性(Consistency)
    事务中操作的数据及状态改变是一致的,即写入资料的结果必须完全符合预设的规则,不会因为出现系统意外等原因导致状态的不一致
  • 隔离性(Isolation)
    一个事务所操作的数据在提交之前,对其他事务不可见
  • 持久性(Durability)
    事务所做的修改就会永久保存,不会因为系统意外导致数据的丢失

事务日志

InnoDB的事务日志有两种,分别是redo log和undo log。

redo log是防止发生故障时有脏页未写入磁盘,保证了事务的持久性;

undo log是回滚日志,提供回滚操作,保证了事务的一致性。

redo log是物理日志,记录的是数据页的物理修改;undo log是逻辑日志,根据每行记录进行记录。

Redo Log

redo log有两部分,一个是内存中的日志缓冲(redo log buffer),第二是磁盘上的重做日志文件(redo log file)。

InnoDB在事务提交的时候,必须先将该事务的所有事务日志写入到磁盘上的重做日志文件 (redo log file) 和回滚日志文件 (undo log file) 中进行持久化。

在将内存中的日志缓冲区的内容写到磁盘上的日志文件时,首先会将用户空间中的缓存区日志复制到操作系统的缓冲区中,之后调用操作系统的fsync()操作,操作系统才会将缓冲区中的日志写到磁盘中的文件中,过程如下:

我们可以通过变量 innodb_flush_log_at_trx_commit 的值来决定在提交事务时何时将日志文件持久化到磁盘中。该变量有3种值:0、1、2,默认为1。

  • 设置为0时,事务提交时不写入os buffer,而是每秒钟写入os buffer并调用fsync()写入log file。也就是说当系统崩溃时会丢失1秒的数据。
  • 设置为1时,事务每次提交时都会将日志缓冲区中的日志写入os buffer并调用fsync()写入日志文件。这样即使系统崩溃时也不会丢失数据,但是每次提交都需要写入磁盘,IO的性能较差。
  • 设置为2时,事务每次提交时都会将日志缓冲区中的日志写入os buffer,然后是每秒调用fsync()将os buffer中的日志写入到磁盘中的日志文件中 。

Undo Log

对数据进行修改时,需要记录redo log 和 undo log,redo log 保证了事务提交后数据的持久性,而 undo log 保证了如果事务失败,则可以借助undo log进行回滚操作。

当删除一条记录时,undo log 中会记录删除前的数据以保证回滚时的数据一致性,执行修改时也一样,回滚日志中会记录更改前的数据。

Purge线程

当事务提交的时候,InnoDB不会立即删除undo log,因为InnoDB支持MVCC,所以后续还可能会用到undo log的记录,在默认的隔离级别repeatable read下,事务读取的都是开启事务时的最新提交行版本,只要该事务还没有结束,该行版本就不能被删除。

但是在事务提交的时候,会将该事务对应的undo log放入到删除列表中,通过purge后台线程来删除。

并发事务

相对于串行处理事务,并发事务可以提升数据库资源的利用率,但并发事务处理也会带来一些问题,主要包括以下几种情况。

  • 脏读

    在事务A对数据修改后但未提交时,此时事务B读取了事务A修改后的数据并产生后续处理,这种现象被叫做脏读,因为事务A的记录还没有提交,有可能会进行回滚或其他操作。

  • 不可重复读

    一个事务读取的数据在某个时间后再次读取,但是读取出的数据却已经发生了改变,出现了不一致现象,这种现象被称为不可重复读。

  • 幻读

    一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象被称为幻读。

隔离级别

为了解决并发事务时会产生的问题,数据库需要提供事务隔离机制来解决,数据库实现事务隔离的方式,基本上可分为以下两种:

  • 第一种是在读取数据时加锁,其他事务就不能对数据进行修改了。
  • 第二种为不加锁,生成一个数据的不同时间点的数据快照,使用快照实现不同事务的读取。这种技术叫做数据多版本并发控制(MultiVersion Concurrency Control,简称MVCC)。

数据库的事务隔离在一定程度上牺牲了并发的性能,但是不同的应用程序可能对于事务的隔离程度要求并不是太严格,所以在SQL92标准中定义了4个事务隔离级别,根据隔离程度允许出现的副作用不同,应用可以根据自己的需求来选择事务的隔离级别。

  • Read Uncommitted(未提交读)

    事务未提交时的数据对其他事务也是可见的,这是最低的隔离级别,允许脏读,不可重复读及幻读。

  • Read Committed(已提交读,也称不可重复读)

    一个事务开始之后,只能看到提交的事务所做的修改,解决了脏读问题,但是会出现不可重复读和幻读。

  • Repeatable Read (可重复读)

    在同一个事务中多次读取同样的数据结果是一样的,解决了不可重复读问题,这种隔离级别未定义解决幻读的问题,在innodb中通过临键锁解决了幻读问题。

  • Serializable(串行化)

    最高的隔离级别,强制事务的串行执行。

锁用于管理不同事务对共享资源的并发访问,在读取数据前,对其加锁,阻止其他事务对数据进行修改。

表锁的并发性能相对于行锁较低,且行锁的锁定粒度和冲突概率也比表锁更小。

共享锁和排他锁

InnoDB中实现的行锁有两种类型,共享锁和排他锁。

共享锁 (shared lock):

共享锁简称s锁,持有该行的s锁的事务允许读取行数据,多个事务对于同一行数据可以共享锁,但是只有读取数据的权限,没有对数据进行修改的权限。

排他锁 (exclusive lock):

排他锁简称x锁,持有该行的x锁的事务允许更新或删除行数据,如果一个事务获取了一个数据行的x锁,其他事务必须等待锁的释放。

意向共享锁和意向排它锁

InnoDB中可以支持多种粒度的锁定,允许表锁和行锁共存,意向锁就是表锁,有两种类型,意向共享锁和意向排它锁。意向锁的意思为稍后对表中的数据行所需的锁类型,可能是共享锁或排他锁。

意向共享锁 (intention shared lock):

意向共享锁简称is锁,表示事务准备获取某些数据行的共享锁,在获取表中数据行的共享锁之前,必须首先获得表上的意向共享锁。

意向排它锁 (intention exclusive lock):

意向排它锁简称ix锁,表示事务准备获取某些数据行的排他锁,在获取表中数据行的排他锁之前,必须首先获得表上的意向排他锁。

当事务想要获取数据行上的锁时,需要先获取意向锁,如果锁定请求与现有锁定冲突,则无法授予锁。

记录锁

记录锁 (record lock):

记录锁就就是对索引记录的锁定。即使表中没有创建索引,InnoDB也会创建一个隐藏的聚簇索引并且使用此索引来进行记录锁定。记录锁就是所谓的行锁。

间隙锁

间隙锁 (gap lock)

间隙锁就是锁定索引记录之间的间隙,或者锁定在第一个索引记录之前或最后一个索引记录之后的间隙。

以下sql语句将阻止其他事务将值15插入列column1中,因为该范围中之间的间隙已经被锁定。

1
select column1 from table_name where column1 > 10 and column1 < 20 for update

假设表中column1列的索引包含值10,13,18,20,那么该列索引中的间隙如下:

需要注意的是,锁定的间隙可能会跨越多个索引值,也可能为空。间隙锁只被用于事务级别为Repeatable Read中。

Next-Key锁

next-key lock

next-key锁是锁定索引记录中的记录及锁定索引记录之间的间隙,也就是记录锁和间隙锁的组合。

Innodb执行行锁的锁定过程是,首先扫描表的索引,在满足条件的索引记录上设置共享锁或排他锁,这样行锁实际上就是锁定索引上的记录。

锁定索引上的下一个索引同样会影响该索引记录之前的间隙,也就是说next-key锁是锁定索引记录中的记录加上锁定索引记录之前的间隙。

假设表中column1列的索引包含值10,13,18,20,那么此索引的next-key锁包括以下几个区间,左开右闭。

对于第一个区间,锁定了索引中最小值之前的间隙,对于最后一个索引则锁定了索引中最大值之后的间隙。

因为默认情况下InnoDB使用的隔离级别为REPEATABLE READ,而在这种隔离级别下,InnoDB使用Next-Key锁阻止了幻读现象。

MVCC

MVCC:Multiversion concurrency control (多版本并发控制)

Innodb的多版本并发控制允许在数据库事务在Repeatable-Read的隔离级别下时,事务执行一致性的读取操作。其他事务在进行修改行时,其他事务可以不用获取该行的锁,并可以直接查看该行更新前的值。

Innodb的隐藏列

Innodb在内部为数据库中存储的每一行添加了三个隐藏列。

列名 作用
DB_TRX_ID 插入或更新的事务id,删除在内部被视为更新,并设置删除标记位
DB_ROLL_PTR undo log指针,指向对应记录当前的undo log
DB_ROW_ID 行ID,用来生成默认聚集索引

MVCC实现

Innodb保存了已经被更改的数据行的旧版本信息,以支持并发和回滚等事务性的功能,这些信息保存在undo log中,Innodb使用undo log中的信息构建数据行的早期版本实现一致读取。

当事务1执行insert table_name(id,col1,col2,col3) value(10,1,2,3,4)时,数据行如下:

当事务2修改数据时,DB_TRX_ID记录修改数据的事务id,如果是删除数据的操作,则设置删除标识,最终的删除操作由purge线程完成。同时,purge线程会查询比当前最老的事务id还早的undo log并删除它们。

Innodb的可见性判断

可见性比较的方法

  • 并不是直接使用当前的事务id与表中各个数据行上的事务id去比较。
  • 在每个事务开始时,会将当前系统中的所有活跃事务拷贝到一个列表中(read view),根据read view最早的一个事务id和最晚的一个事务id来做比较;这样就能确保在当前事务之前没有提交的事务及后续启动事务的变更在当前的事务中是看不到的。
  • 当然,当前事务自身的变更还是需要看到的。

可见性判断的流程

当开始一个事务时,把当前系统中活动的事务id都拷贝到一个列表中,这个列表中最早的活动事务id为tmin,最晚的活动事务id为tmax。

当读到一行时,该行上的当前事务id为tid,当前数据行是否可见的逻辑见下图:

如果事务需要获取一个数据行的数据,那么首先获取到数据行上的最后更新事务id,如果数据行上的更新事务id在活跃事务列表中小于最早的活动事务id,则可以直接获取该数据行的数据。

如果数据行上的更新事务id并不小于最早的活动事务id,也不大于最晚的活动事务id,但是不在活跃事务列表中,则也可以直接获取该数据行的数据。

如果数据行上的更新事务id大于最晚的活动事务id或者还在活跃事务列表中,则从undo log中取出对应的最新数据行,事务重新进行判断是否可以获取该数据行的数据。