• 进入"运维那点事"后,希望您第一件事就是阅读“关于”栏目,仔细阅读“关于Ctrl+c问题”,不希望误会!

MySQL InnoDB事务并发异象

MySQL 彭东稳 7年前 (2016-06-17) 17020次浏览 已收录 0个评论

试想,事务如果都是串行的,那么就不需要锁了,但是性能肯定没法接受。加锁只是为了提高事务并行度,并且解决并发事务执行过程中引起的脏写、脏读、不可重复读、幻读这些问题的一种解决方案(MVCC算是一种解决脏读、不可重复读、幻读这些问题的一种解决方案),一定要意识到加锁的出发点是为了解决这些问题,不同情景下要解决的问题不一样,才导致加的锁不一样。当然,有时候因为MySQL具体的实现而导致一些情景下的加锁有些不太好理解,这就得我们死记硬背了~

并发事务会出现的五个问题是:

  • Temporary Update Problem
  • Incorrect Summary Problem
  • Lost Update Problem
  • Unrepeatable Read Problem
  • Phantom Read Problem

一、丢失更新(lost update

当多个并发事务尝试读取和更新相同的数据时,就会出现丢失更新问题。简单来说就是一个事务的更新操作会被另一个事务的更新操作所覆盖,从而导致数据的不一致。让我们借助一个例子来理解这一点:

MySQL InnoDB事务并发异象

在此示例中,我们有两个并发事务尝试提取 50 美元和 30 美元来自 Bob 的帐户。经过两次转换完成后,我们希望看到账户余额 20 美元,但由于第二个事务只读取了承诺的数据,因此不知道并发操作并表现为从未发生的第一个事务。结果,第二笔交易覆盖了第一次更新,我们的系统损失了 50 美元。这是丢失更新问题的一个例子。

但是,在当前数据库的任何隔离级别下,都不会导致数据库理论意义上的丢失更新问题。这是因为,即使是READ UNCOMMITTED的事务隔离级别,对于行的DML操作,需要对行或其他粗粒度级别的对象加锁(U锁)。因此上面的事务B并不能对事务A正在操作的数据进行更新操作,其会被阻塞,直到事务A结束。

有的人可能会奇怪,在上述的例子中为什么不直接使用 UPDATE 语句,而首先要进行 SELECT 的操作。的确,直接使用 UPDATE 可以避免丢失更新问题的产生,然而在实际应用中,应用程序可能需要首先检测用户的余额信息,查看是否可以进行转账操作,然后再进行最后的 UPDATE 操作,因此在 SELECT UPDATE 操作之间可能还存在一些其他的 SQL 操作。

如何方式丢失更新?

有几种方法可以防止丢失更新。我将向您展示以下内容:

  • 提高事务隔离级别
  • 乐观锁定
  • 悲观锁定
  • 原子写操作

提高事务隔离级别

这种方法的一个优点是数据库可以结合可重复读隔离级别(repeatable read isolation level)有效地执行此检查。PostgreSQL 的 repeatable read、Oracle 的 serializable 和 SQL Server 的 snapshot isolation 级别会自动检测何时发生丢失更新并中止有问题的事务。但是,MySQL/InnoDB 的可重复读取不会检测到丢失的更新。自动丢失更新检测是一个很棒的功能,因为它不需要应用程序代码来使用任何特殊的数据库功能。然而,如果你的系统需要支持多个 RDBMS,它可能不适合你。

乐观锁定

乐观锁定也称为条件更新(Conditional Update)或比较并设置(Compare-And-Set),通过仅在自上次读取后值未更改时才允许更新来帮助避免丢失更新。如果当前值与你之前读取的不匹配,则更新无效,必须循环重试读取-修改-写入。我们可以通过不同的方式实现乐观锁定,但是我们将使用可能是最常见的一种。

首先,我们需要在我们的表中添加一个新的 version 字段。差不多就是这样,现在让我们检查一下我们的查询是如何因为这个小变化而改变的。

MySQL InnoDB事务并发异象

由于第一个事务已经更新了行版本,所以第二个更新没有生效,你的应用程序代码需要重试第二个事务。

Note

上面这种方案可以称之为行级 version 乐观锁,任何对行的更新都是用 version 字段进行判断,根据业务场景及并发需求,还可以采用局部 version 乐观锁,针对某些或某个字段的频繁更新专门创建对应的 version 字段,锁粒度更小。

悲观锁定

防止丢失更新的另一个选项是显式锁定将要更新的对象,然后应用程序可以执行一个读-修改-写循环,如果任何其他事务试图同时读取同一个对象,它就会被迫等待,直到第一个读-修改-写循环完成。这种方法称为悲观锁定。

在我们的示例中,我们将使用 PESSIMISTIC_WRITE 模式,该模式允许我们获得排他锁并防止数据被读取、更新或删除。

MySQL InnoDB事务并发异象

由于第二个事务只有在第一个事务完成后才读取数据,因此没有覆盖,因此不会发生丢失更新。

二、脏读(dirty read

在理解脏读之前,需要理解脏数据的概念。但是别把脏数据和脏页混淆,脏页指的是在缓冲池中已经被修改的页,但是没有刷新到磁盘中,即数据库实例内存中的页和磁盘中的页的数据是不一致的,当然在刷新到磁盘之前,日志都已经被写入到了 redo 日志文件中。而所谓脏数据是指事务对缓冲池中行记录的修改,并且还没有被提交(commit)。

对于脏页的读取,是非常正常的。脏也是因为数据库实例内存和磁盘的异步造成的,这并不影响数据的一致性(或者说两者最终会达到一致性,即当脏页都刷回到磁盘)。并且因为脏页的刷新是异步的,不影响数据库的可用性,因此可以带来性能的提高。

但脏数据却截然不同,脏数据是指未提交的数据,如果读到了脏数据,即一个事务可以读到另一个事务中未提交的数据,则显然违反了数据库的隔离性标准。简单来说“脏读”指的是一个事务可以读到另一个事务中未提交的数据。如,事务 A 修改某一数据并写回缓存池,然后事务 B 又读取该数据。事务 A 由于某种原因被撤销,数据恢复原值,从而导致事务 B 读取的数据为错误数据,也就是脏数据。一般出现“脏读”都是在 Read Uncommitted 隔离级别下才会发生,也就是事务会读取到另一个事务没有提交的数据。但是 MySQL 默认是 RR 隔离级别,而 Oracle 则在 RC 隔离级别,所以基本不会出现“脏读”的问题。

三、不可重复读(non-repeatable read

不可重复读是指在一个事务内相同查询语句多次查询时结果不同。比如,事务 A 读取了一行数据,事务 B 接着修改或者删除了这行数据,当事务 A 再次读取同一行数据的时候,读到的数据时修改之后的或者发现已经被删除。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为“不可重复读”。

不可重复读和脏读的区别是:脏读是读到未提交的数据,而“不可重复读”读到的确实是已提交的数据,但是其违反了 ACID 中的 I,即隔离性。下面通过一个例子来观察不可重复读的情况,在《InnoDB行锁算法介绍》这篇博客中也详细介绍了“不可重复读”问题。所以目前不可重复读只有在 RC 隔离级别下才会发生。

如下实例:

# 事务A

# 事务B

# 事务A

一般来说,不可重复读的问题是可以接受的,因为其读到的是已经提交的数据,本身并不会带来很大的问题。因此,很多数据库厂商将其数据库事务的默认级别设置为“读已提交”,在这种隔离级别下允许不可重复读的现象。而 MySQL 使用的是“可重复读”隔离级别。

InnoDB 存储引擎中,通过使用 Next-Key Lock 算法来避免不可重复读的问题。在 MySQL 官方文档中将不可重复读的问题定义为 Phantom Problem,即幻象问题。在 Next-Key Lock 算法下,对于索引的扫描,不仅是锁住扫描到的索引,而且还锁住这些索引覆盖的范围(gap)。因此在这个范围的插入都是不允许的。这样就避免了另外的事务在这个范围内插入数据导致的不可重复读的问题。因此,InnoDB 存储引擎的默认事务隔离级别时 REPEATABLE READ,采用 Next-Key Lock 算法,避免了不可重复读的问题。

四、幻读(Phantom)

幻读,是指当事务不是独立执行时发生的一种现象,事务 A 读取了满足某条件的一个数据集,事务 B 插入了一行或者多行数据满足了事务 A 的选择条件,导致事务 A 再次使用同样的选择条件读取的时候,得到了比第一次读取更多的数据集,就好象发生了幻觉一样。虽然 MySQL 在 Repeatable Read 隔离级别也解决了幻读问题,但也没有完全去解决。

我们在 MySQL 的 Repeatable Read 隔离级别下来复现一下这个没去完全解决的幻读问题。

事务A 事务B
begin;
select * from z
(没有数据)
insert into z select 1
update z set id=2
(影响一行)
select * from z
(查询一行)

对于“不可重复读”及“幻读”问题,有时候很容易搞混淆。但从定义上来看,“不可重复读”主要针对 update 操作,对上一次读到的数据再次读取时发生了改变;而“幻读”主要针对 insert 及 delete 操作,对上一次读取到的数据条数变多或变少了。

你如果去测试 PostgreSQL、SQL Server 可能得到的结果与 MySQL 不同,它们在 Repeatable Read 隔离级别不会产生幻读,在事务 A 更新数据的时候就不会看见事务 B 插入的数据。这主要与 MySQL 实现 Repeatable Read 隔离级别的方式有关,MySQL 中 DML 操作都是当前读,所以上面事务 A 才会更新了事务 B 的插入操作,那么也就把这一行的 trx_id 更新为事务 A 的 id 了。由于 MySQL 对记录可见性规则规定(涉及到 MVCC 的实现与机制),如果行的 trx_id 与当前事务 id 相同,那么就读,所以就发生了上面的幻读现象。

其实在 ANSI SQL 标准定义中,Repeatable Read 隔离级别不需要解决“幻读”,只解决“不可重复读”,Serializable 隔离级别解决幻读。所以 MySQL 这种实现方式是符合 ANSI SQL STANDARD,并非属于实现上的 BUG。

看一下 ANSI SQL STANDARD 对于各种隔离级别发生幻读的规定:

ANSI SQL-92 隔离级别

Dirty Read Non-repeatable Read Phantom Read
Read Uncommitted Possible Possible Possible
Read Committed Not Possible Possible Possible
Repeatable Read Not Possible Not Possible Possible
Serializable Not Possible Not Possible Not Possible

<延伸>

http://mysql.taobao.org/monthly/2017/06/07/


如果您觉得本站对你有帮助,那么可以支付宝扫码捐助以帮助本站更好地发展,在此谢过。
喜欢 (1)
[资助本站您就扫码 谢谢]
分享 (0)

您必须 登录 才能发表评论!