MySQL数据库事务、隔离级别以及多版本并发控制(MVCC)

事务

事务就是一组原子性的SQL查询。也就是说,事务内的语句,要么全部执行成功,要么全部执行失败。

ACID

原子性(atomicity)

一个事务必须被视为一个不可分割的最小单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说,不可能只执行其中的一部分操作,这就是事务的原子性。

一致性(consistency)

数据库总是从一个一致性的状态转换到另外一个一致性的状态。在前面的例子中,一致性确保了事务只要没有最终提交,就不会把事务中所作的修改保存到数据库中。

隔离性(isolation)

通常来说(因为要考虑隔离级别,所以说是通常情况),一个事务所作的修改在最终提交之前,对其他事务是不可见的。

持久性(durability)

一旦事务提交,其所作的修改就会永久保存到数据库中。此时即使系统奔溃,修改的数据也不会丢失。持久性是个模糊的概念,因为持久性也分很多级别。有些持久性策略能够提供非常强的安全保障,而有些则未必。而且不可能做到百分之百的持久性保证的策略。

隔离级别

隔离性其实比想象的要复杂。在SQL标准中定义了四种隔离级别,每一种级别规定了一个事务中所作的修改,哪些在事务间是可见的,哪些是不可见的。较低级别的隔离通常可以执行更高的并发,系统的开销也更低。

READ UNCOMMITTED(读未提交)

在READ UNCOMMITTED级别,事务中的修改即使是没有提交,对其他事务也是可见的。事务可以读取到未提交的数据,这被称为脏读(Dirty Read)。从性能来说,READ UNCOMMITTED不会比其他级别好太多,但缺乏其他级别的很多优势,所以实际应用很少。

READ COMMITTED(读提交)

大多数数据库的默认隔离级别都是READ COMMITTED(但MySQL不是)。READ COMMITTED满足隔离性的简单定义:一个事务开始时,只能看见已经提交的事务所作的修改。换句话说,一个事务从开始直到提交之前,所作的任何修改对其他事务都是不可见的。这个级别有时候会产生不可重复读(nonrepeatable read),因为执行两次执行同样的查询,可能会得到不一样的结果。

REPEATABLE READ(可重复读)

REPEATABLE READ解决了脏读,和不可重复读的问题。但是无法解决幻读(Phantom Read)的问题。幻读是指当某个事务在读取某个范围内的记录时,另外一个事务又在范围内插入新的记录,当之前的事务再次读取该范围的记录,会产生幻行(Phantom Row)。InnoDB和XtraDB存储引擎通过多版本并发控制(MVCC)解决了幻读的问题。(可重复读是MySQL的默认事务隔离级别)。

SERIALIZABLE(可串行化)

SERIALIZABLE是最高隔离级别。它通过强制事务串行执行,避免幻读问题。简单来说,SERIALIZABLE会在读取的每一行上都加锁,所以可能导致大量的超时和锁争用的问题。实际应用很少使用该隔离级别,只有在非常需要确保数据一致性而且可以接受没有并发的情况下,才考虑采用该级别。

隔离级别

隔离级别 脏读可能性 不可重复读可能性 幻读可能性 加锁读
READ UNCOMMITTED Yes Yes Yes No
READ COMMITTED No Yes Yes No
REPEATABLE READ No No Yes No
SERIALIZABLE No No No Yes

多版本并发控制(MVCC)

MySQL的大多数事务型存储引擎实现的都不是简单的行级锁。基于提升并发性能的考虑,它们一般都同时实现了多版本并发控制(MVCC)。不仅是MySQL,包括Oracle、PostgreSQL等其他数据库系统也实现了MVCC,但是实现机制不尽相同,因为MVCC没有一个统一的实现标准。

可以认为MVCC是行级锁的一个变种,但是它在多数情况下都避免了加锁操作,因此开销更低。虽然实现机制有所不同,但大都实现了非阻塞的读操作,写操作也只锁定必要的行。

MVCC的实现,是通过保存数据在某个时间点的快照来实现的。也就是说,不管执行多长时间,每个事物看到的数据都是一致的。根据事务开始时间的不同,每个事物对同一张表,同一时刻看到的数据可能是不一样的。

InnoDB的MVCC,是通过在每行记录后面保存两个隐藏的列来实现的。这两个列,一个保存了行的创建时间,一个保存行的过期时间或者删除时间。存储的并不是实际的时间值,而是系统版本号(system version number)。每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。

MVCC在REPEATABLE READ隔离级别下的操作

SELECT

InnoDB会根据以下两个条件检查每行记录:

a.InnDB只查找版本早于当前事务版本的数据行(也就是,行的系统版本号小于或者等于事务的系统版本号),这样可以确保事务读取的行,要么是在事务开始前已经存在,要么是事务自身插入或者修改过的。

b.行的删除版本要么未定义,要么大于当前事务版本号。这可以确保事务读取到的行,在事务开始之前未被删除。

只有符合上述两个条件的记录,才能返回作为查询的结果。

INSERT

InnoDB为新插入的每一行保存当前系统版本号作为行版本号。

DELETE

InnoDB为删除的每一行保存当前系统版本号作为行删除标识。

UPDATE

InnoDB为插入一行新记录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行的行删除标识。

保存这样两个系统版本号,使大多数读操作都可以不用加锁。这样设计使得读操作很简单,性能很好,并且也能保证只会读到符合标准的行。不足之处是每行记录都是需要额外的存储空间,需要做更多的行检查工作以及一些额外的维护工作。

MVCC只在REPEATABLE READ和READ COMMITTED两个隔离级别下工作。其他两个隔离级别都和MVCC不兼容。因为READ UNCOMMITTED总是读到最新的数据行,而不是符合当前事务版本的数据行。SERIALIZABLE则会对所有读取的行都加锁。

InnoDB中幻读的解决到底是依赖Next-Key Locks还是MVCC?

首先读分为:快照读、当前读。

  • 快照读。例: SELECT * FROM table WHERE …;
  • 当前读。特殊的读操作,更新,插入,删除操作,属于当前读,需要加锁。 SELECT FROM table WHERE ? lock in share mode;SELECT from FROM table WHERE ? for update;
    INSERT INTO table values (…);
    UPDATE table SET ? WHERE ?;

结论:

  • 对于快照读来讲,幻读的解决依赖于MVCC,使用 MVCC 读取的是快照中的数据,这样可以减少加锁所带来的开销。
  • 对于当前读,读取的是最新的数据,需要加锁,依赖Next-Key Locks。

拓展:

Next-key Locks是InnoDB存储引擎的一种锁实现。

Record Locks:锁定一个记录上的索引,而不是记录本身。

Gap Locks:锁定索引之间的间隙,但是不包含索引本身。

Next-key Locks:是上述两者的结合,不仅锁定一个记录上的索引,也锁定索引之间的间隙。

参考

  • 《高性能MySQL》

猜你喜欢

转载自blog.csdn.net/wantaceveryday/article/details/84962217