事物概览

ACID

原子性:当客户想进行多次写入,但在一些写操作处理完之后出现故障的情况。例如进程崩溃,网络连接中断,磁盘变满或者某种完整性约束被违反。如果这些写操作被分组到一个原子事务中,并且该事务由于错误而不能完成(提交),则该事务将被中止,并且数据库必须丢弃或撤消该事务中迄今为止所做的任何写入。
一致性对数据的一组特定陈述必须始终成立。即不变量(invariants)。例如,在会计系统中,所有账户整体上必须借贷相抵。如果一个事务开始于一个满足这些不变量的有效数据库,且在事务处理期间的任何写入操作都保持这种有效性,那么可以确定,不变量总是满足的。
隔离性:同时执行的事务是相互隔离的:它们不能相互冒犯。传统的数据库教科书将隔离性形式化为可序列化(Serializability),这意味着每个事务可以假装它是唯一在整个数据库上运行的事务。数据库确保当事务已经提交时,结果与它们按顺序运行(一个接一个)是一样的,尽管实际上它们可能是并发运行的。
持久性:是一个承诺,即一旦事务成功完成,即使发生硬件故障或数据库崩溃,写入的任何数据也不会丢失。

数据项和谓词

隔离级别与异常现象

ANSI SQL-92 提出了最经典的隔离级别定义,包括读未提交(Read Uncommitted)、读提交(Read Committed)、可重复读(Repeatable Read)和可序列化(Serializable)。
在这里插入图片描述
在文章 A Critique of ANSI SQL Isolation Levels 中,作者认为仅通过上述三种异象还不足以清晰的定义隔离级别。相应的,作者对 ANSI SQL 的异象与隔离级别做了扩展,重新定义了 8 种异象和 6 种隔离级别。后来,该文成为了理解数据库隔离性的重要论文之一。

为了能更清晰的表述事务之间的操作关系,我们将操作简化为 w w w (write), r r r (read) ,每个操作的下标 n n n 代表执行操作的事务,例如 r 1 r_1 r1 代表事务 1 读, w 2 w_2 w2 代表事务 2 写。紧跟着操作的中括号 [ ] [] [] 的内容代表当前操作所涉及的资源,例如 w 1 [ x ] w_1[x] w1[x] 代表事务 1 写入了数据项 x x x r 2 [ P ] r_2[P] r2[P] 代表事务 2 读取了满足谓词 P P P 的资源。最后,使用 c c c (commit) 和 a a a (abort) 来表示提交与回滚。

因此我们就可以用一连串的操作来表示一段操作历史:

w 1 [ x ] . . . r 2 [ x ] . . .   ( a 1   a n d   c 2   i n   a n y   o r d e r ) w_1[x]...r_2[x]...\ (a_1\ and\ c_2\ in\ any\ order) w1[x]...r2[x]... (a1 and c2 in any order)

可以表述事务 2 先写 x x x ,之后事务 1 读 x x x,最后事务 1 回滚或事务 2 提交。

加锁的方式实现隔离级别

为了实现事务之间的隔离,可以通过给读写分别加锁的方式(读共享锁、写独占锁)进行并发控制,加锁又可以分为以下三种粒度:

  1. 不加锁
  2. 使用前加锁,使用完立即释放锁,称为 Short duration
  3. 使用前加锁,直到事务提交后释放锁,称为 Long duration 锁 (类似两阶段锁的实现)

隔离级别与异常现象的定义与数据库具体实现事务的方式(2PL,MVCC,OCC)无关,这里为了能更好的让读者理解隔离级别与异常现象,下文分别解释了如何用加锁的方式实现各种隔离级别。
在使用锁隔离的事务控制实现下,能够定义如下异象与隔离级别的关系:

P0: Dirty Write

P0: w 1 [ x ] . . . w 2 [ x ] . . .   ( ( c 1   o r   a 1 )   a n d   ( c 2   o r   a 2 )   i n   a n y   o r d e r ) w_1[x]...w_2[x]...\ ((c_1\ or\ a_1)\ and\ (c_2\ or\ a_2)\ in\ any\ order) w1[x]...w2[x]... ((c1 or a1) and (c2 or a2) in any order)
P0 称为 Dirty Write,在读写均不加锁时可能发生 P0。这种异象会破坏数据库的一致性,因此是任何隔离级别都不可容忍的

以一个二手车销售网站为例,Alice和Bob两个人同时试图购买同一辆车。购买汽车需要两次数据库写入:网站上的商品列表需要更新,以反映买家的购买,销售发票需要发送给买家。在下图中,销售是属于Bob的(因为他成功更新了商品列表),但发票却寄送给了爱丽丝(因为她成功更新了发票表)。
在这里插入图片描述

P1: Dirty Read

严格的脏读 A1: Dirty Read

ANSI 对于脏读定义如下:

Transaction T1 modifies a data item.
Another transaction T2 then reads that data item before T1
performs a COMMIT or ROLLBACK. If T1 then performs a
ROLLBACK, T2 has read a data item that was never
committed and so never really existed. 

A1: w 1 [ x ] . . . r 2 [ x ] . . .   ( a 1   a n d   c 2   i n   a n y   o r d e r ) w_1[x]...r_2[x]...\ ( a_1 \ and \ c_2 \ in\ any\ order ) w1[x]...r2[x]... (a1 and c2 in any order)

宽松的脏读 P1: Dirty Read

P1: w 1 [ x ] . . . r 2 [ x ] . . .   ( ( c 1   o r   a 1 )   a n d   ( c 2   o r   a 2 )   i n   a n y   o r d e r ) w_1[x]...r_2[x]...\ ((c_1\ or\ a_1)\ and\ (c_2\ or\ a_2)\ in\ any\ order) w1[x]...r2[x]... ((c1 or a1) and (c2 or a2) in any order)
P1 称为 Dirty Read,在读不加锁,写加 Long duration 锁时(Read Uncommitted)可能发生 P1,不会发生 P0 ( w 2 [ x ] w_2[x] w2[x]会阻塞)。这种情况可能导致事务读取到未提交的脏数据。如下事务执行历史模拟了 P1 发生的情况:

H1: r 1 [ x = 50 ] . . w 1 [ x = 10 ] . . r 2 [ x = 10 ] . . r 2 [ y = 50 ] . . c 2 . . r 1 [ y = 50 ] . . w 1 [ y = 90 ] . . c 1 r_1[x=50]..w_1[x=10]..r_2[x=10]..r_2[y=50]..c_2..r_1[y=50]..w_1[y=90]..c_1 r1[x=50]..w1[x=10]..r2[x=10]..r2[y=50]..c2..r1[y=50]..w1[y=90]..c1

P2: Non-repeatable Read

严格的不可重复读 A2: Non-repeatable Read

ANSI 对于不可重复读定义如下:

Transaction T1 reads a data item. 
Another transaction T2 then modifies or deletes that data item and commits. 
If T1 then attempts to reread the data item, 
it receives a modified value or discovers that the data item has been deleted.

A2: r 1 [ x ] . . . w 2 [ x ] . . . c 2 . . . r 1 [ x ] . . . c 1 r_1[x]...w_2[x]...c_2...r_1[x]...c_1 r1[x]...w2[x]...c2...r1[x]...c1

事务 T1 读取 一个数据项。另一个事务 T2 然后 修改或删除(update or delete) 该数据项并提交。如果 T1 然后尝试重新读取该数据项,它会收到修改后的值或发现数据项已被删除。

宽松的不可重复读 P2: Non-repeatable Read

先考虑如下事务执行历史:

H2: r 1 [ x = 50 ] . . r 2 [ x = 50 ] . . w 2 [ x = 10 ] . . r 2 [ y = 50 ] . . w 2 [ y = 90 ] . . c 2 . . r 1 [ y = 90 ] . . c 1 r_1[x=50]..r_2[x=50]..w_2[x=10]..r_2[y=50]..w_2[y=90]..c_2..r_1[y=90]..c_1 r1[x=50]..r2[x=50]..w2[x=10]..r2[y=50]..w2[y=90]..c2..r1[y=90]..c1

H2 显然不属于 A2,因为 T1 没有对某一个数据项重复读两次。但是 H2 (实际上是一个 Read Skew),仍然违背了业务层的一致性(T2 将 40 元从 x 转到 y,T1 应该看到两个账户的总额不变)。我们说 H2 是由 P2 引起的。P2 是 A2 的宽松解释,P2 包含了 A2。

P2: r 1 [ x ] . . . w 2 [ x ] . . .   ( ( c 1   o r   a 1 )   a n d   ( c 2   o r   a 2 )   i n   a n y   o r d e r ) r_1[x]...w_2[x]...\ ((c_1\ or\ a_1)\ and\ (c_2\ or\ a_2)\ in\ any\ order) r1[x]...w2[x]... ((c1 or a1) and (c2 or a2) in any order)
P2 称为 Non-repeatable Read (Fuzzy Read),在读(数据项、谓词)加 Short duration 锁,写加 Long duration 锁时(Read Committed)可能发生 P2,不会发生 P0,P1 ( r 2 [ x ] r_2[x] r2[x]会阻塞)。这种情况可能导致同一事务中两次读取到的数据不一致。

P3: Phantom

严格的幻读 A3: Phantom Read

ANSI 对于幻读定义如下:

Transaction T1 reads a set of data items satisfying some. 
Transaction T2 then creates data items that satisfy T1’s and commits. 
If T1 then repeats its read with the same , 
it gets a set of data items different from the first read.

事务 T1 读取一组 满足某些 <搜索条件> 的数据项。事务 T2 创建 了满足 T1 的 <搜索条件> 的数据项并提交。如果 T1 用 相同的 <搜索条件> 再次读取,得到一组不同于第一次读取的数据。这就叫幻读。

注意和严格的不可重复读定义的两个不同:

  1. 幻读定义中有 < 搜索条件 >
  2. 幻读定义中 T2 是 创建数据,不可重复读的定义中 T2 是 修改或者删除数据

修改和删除数据必定是对已经存在的数据行操作,而创建数据则意味着创建之前这个数据项是不存在的。“创建数据”不仅是 insert,还包括 update。update 把本来不满足谓词范围的数据项更新成满足谓词范围的数据项,比如:谓词范围是 a>1 and a<5,update a=4 where a=6 就是这样的情况。
再直观点讲(个人解读),不可重复读是说读的结果的行数不变或者减少,结果的内容发生变化;而幻读呢,就是读的结果的行数变多了。

A3: r 1 [ P ] . . . w 2 [ y   i n   P ] . . .   c 2 . . . r 1 [ P ] . . .   c 1 r_1[P]...w_2[y \ in \ P]...\ c_2 ... r_1[P] ...\ c_1 r1[P]...w2[y in P]... c2...r1[P]... c1

上述执行历史描述了事务 1 基于某种条件谓词 P P P 来读取,这时事务 2 插入了一条新的满足谓词 P P P 的数据项,在事务 2 提交后,事务 1 再基谓词 P P P 来读取,会发现两次读取结果不同。

宽松的幻读 P3: Phantom

先考虑如下事务执行历史:

H3: r 1 [ P ] . . w 2 [ i n s e r t   y   t o   P ] . . r 2 [ z ] . . w 2 [ z ] . . c 2 . . r 1 [ z ] . . c 1 r_1[P]..w_2[insert\ y\ to\ P]..r_2[z]..w_2[z]..c_2..r_1[z]..c_1 r1[P]..w2[insert y to P]..r2[z]..w2[z]..c2..r1[z]..c1

H3 显然不属于 A3,因为 T1 没有基于某种条件谓词 P P P 来读取两次。但是 ,仍然违背了业务层的一致性:上述执行历史描述了事务 1 基于某种条件谓词 P P P 来查找在职员工列表,这时事务 2 向在职员工列表插入了一个新员工,并更新了代表员工总数的数据项 z z z,在事务 2 提交后,事务 1 检查数据项 z z z,会发现与读取的在职员工列表不符。我们说 H3 是由 P3 引起的。P3 是 A3 的宽松解释,P3 包含了 A3。

P3: r 1 [ P ] . . . w 2 [ y   i n   P ] . . .   ( ( c 1   o r   a 1 )   a n d   ( c 2   o r   a 2 )   i n   a n y   o r d e r ) r_1[P]...w_2[y \ in \ P]...\ ((c_1\ or\ a_1)\ and\ (c_2\ or\ a_2)\ in\ any\ order) r1[P]...w2[y in P]... ((c1 or a1) and (c2 or a2) in any order)
P3 称为 Phantom,在读数据项加 Long duration 锁读谓词(Predicate)加 Short duration 锁,写加 Long duration 锁时(Repeatable Read)可能发生 P3,不会发生P0,P1,P2 ( w 2 [ x ] w_2[x] w2[x]会阻塞)。这种情况可能导致同一事务中两次读取到的数据量不一致。

Serializable

读数据项加 Long duration 锁读谓词(Predicate)加 Long duration 锁,写加 Long duration 锁时(Serializable)不会发生P0,P1,P2,P3 ( w 2 [ y   i n   P ] w_2[y \ in \ P] w2[y in P]会阻塞)

MVCC的方式实现隔离级别

为了在性能和一致性之间找到更好的平衡,许多数据库选择使用快照版本来进行并发控制(即 MVCC)。在事务开始时获取 Start-Timestamp,依据该时间戳读取最新的快照,读、写都基于快照进行,因此只读事务可以不被阻塞。

MySQL MVCC 浅析

REPEATABLE READ 隔离级别是 启动事务时生成一个 Read View,然后整个事务期间都在用这个 Read View。
READ COMMITTED 隔离级别是在每个 select 都会生成一个新的 Read View。

Read View 有四个重要的字段:

  1. m_ids :指的是创建 Read View 时当前数据库中活跃且未提交的事务的事务 id 列表,注意是一个列表。
  2. min_trx_id :指的是创建 Read View 时当前数据库中活跃且未提交的事务中最小事务的事务 id,也就是 m_ids 的最小值。
  3. max_trx_id :这个并不是 m_ids 的最大值,而是创建 Read View 时当前数据库中应该给下一个事务的 id 值;
  4. creator_trx_id :指的是创建该 Read View 的事务的事务 id。

在这里插入图片描述
对于使用 InnoDB 存储引擎的数据库表,它的聚族索引记录中都包含下面两个隐藏列:

  1. trx_id,当一个事务对某条聚族索引记录进行改动时,就会把该事务的事务 id 记录在 trx_id 隐藏列里;

  2. roll_pointer,每次对某条聚族索引记录进行改动时,都会把旧版本的记录写入到 undo 日志中,然后这个隐藏列是个指针,指向每一个旧版本记录,于是就可以通过它找到修改前的记录。
    在这里插入图片描述

假设事务 A 和 事务 B 差不多同一时刻启动,那这两个事务创建的 Read View 如下:
在这里插入图片描述
事务 A 和 事务 B 的 Read View 具体内容如下:

  1. 在事务 A 的 Read View 中,它的事务 id 是 51,由于与事务 B 同时启动,所以此时活跃的事务的事务 id 列表是 51 和 52,活跃的事务 id 中最小的事务 id 是事务 A 本身,下一个事务 id 应该是 53。
  2. 在事务 B 的 Read View 中,它的事务 id 是 52,由于与事务 A 同时启动,所以此时活跃的事务的事务 id 列表是 51 和 52,活跃的事务 id 中最小的事务 id 是事务 A,下一个事务 id 应该是 53。

然后让事务 A 去读账户余额为 100 万的记录,在找到记录后,它会先看这条记录的 trx_id,此时发现 trx_id 为 50,通过和事务 A 的 Read View 的 m_ids 字段发现,该记录的事务 id 并不在活跃事务的列表中,并且小于事务 A 的事务 id,这意味着,这条记录的事务早就在事务 A 前提交过了,所以该记录对事务 A 可见,也就是事务 A 可以获取到这条记录。
接着,事务 B 通过 update 语句将这条记录修改了,将小林的余额改成 200 万,这时 MySQL 会记录相应的 undo log,并以链表的方式串联起来,形成版本链,如下图:

在这里插入图片描述
「可重复读」隔离级别就是在启动时创建了 Read View,然后在事务期间读取数据的时候,在找到数据后,先会将该记录的 trx_id 和该事务的 Read View 里的字段做个比较:

  1. 如果记录的 trx_id 比该事务的 Read View 中的 creator_trx_id 要小,且不在 m_ids 列表里,这意味着这条记录的事务早就在该事务前提交过了,所以该记录对该事务可见;

  2. 如果记录的 trx_id 比该事务的 Read View 中的 creator_trx_id 要大,或在 m_ids 列表里,这意味着该事务读到的是在自己后启动的事物修改的数据,或是和自己同时启动的另外一个事务修改的数据,这时就不应该读取这条记录,而是沿着 undo log 链条往下找旧版本的记录。

就是通过这样的方式实现了,「可重复读」隔离级别下在事务期间读到的数据都是事务启动前的记录。

Read Uncommitted

避免了 P0: w 1 [ x ] . . . w 2 [ x ] . . .   ( ( c 1   o r   a 1 )   a n d   ( c 2   o r   a 2 )   i n   a n y   o r d e r ) w_1[x]...w_2[x]...\ ((c_1\ or\ a_1)\ and\ (c_2\ or\ a_2)\ in\ any\ order) w1[x]...w2[x]... ((c1 or a1) and (c2 or a2) in any order)

在这里插入图片描述

出现了 A1: w 1 [ x ] . . . r 2 [ x ] . . .   ( a 1   a n d   c 2   i n   a n y   o r d e r ) w_1[x]...r_2[x]...\ ( a_1 \ and \ c_2 \ in\ any\ order ) w1[x]...r2[x]... (a1 and c2 in any order)
在这里插入图片描述

Read Committed

避免了 A1: w 1 [ x ] . . . r 2 [ x ] . . .   ( a 1   a n d   c 2   i n   a n y   o r d e r ) w_1[x]...r_2[x]...\ ( a_1 \ and \ c_2 \ in\ any\ order ) w1[x]...r2[x]... (a1 and c2 in any order)
在这里插入图片描述

出现了 A2: r 1 [ x ] . . . w 2 [ x ] . . . c 2 . . . r 1 [ x ] . . . c 1 r_1[x]...w_2[x]...c_2...r_1[x]...c_1 r1[x]...w2[x]...c2...r1[x]...c1
在这里插入图片描述

出现了 A3: r 1 [ P ] . . . w 2 [ y   i n   P ] . . .   c 2 . . . r 1 [ P ] . . .   c 1 r_1[P]...w_2[y \ in \ P]...\ c_2 ... r_1[P] ...\ c_1 r1[P]...w2[y in P]... c2...r1[P]... c1
在这里插入图片描述

Repeatable Read(Snapshot)

对于这种基于 MVCC 的隔离方式,A Critique of ANSI SQL Isolation Levels 提出了一种新的隔离级别 SNAPSHOT ISOLATION(MySQL 的 REPEATABLE READ 实际上是 SNAPSHOT ISOLATION)。

一致性读(快照读)

避免了 A2: r 1 [ x ] . . . w 2 [ x ] . . . c 2 . . . r 1 [ x ] . . . c 1 r_1[x]...w_2[x]...c_2...r_1[x]...c_1 r1[x]...w2[x]...c2...r1[x]...c1
在这里插入图片描述

避免了 A3: r 1 [ P ] . . . w 2 [ y   i n   P ] . . .   c 2 . . . r 1 [ P ] . . .   c 1 r_1[P]...w_2[y \ in \ P]...\ c_2 ... r_1[P] ...\ c_1 r1[P]...w2[y in P]... c2...r1[P]... c1
在这里插入图片描述

MySQL 快照读和当前读混用造成的异常(幻读?)

InnoDB提供了这样的机制,在默认的 Repeatable Read(Snapshot)隔离级别,可以使用 加锁读 去查询 最新的已提交 的数据(降低为 Read Committed 隔离级别)。
http://dev.mysql.com/doc/refman/5.0/en/innodb-consistent-read.html
https://dev.mysql.com/doc/refman/8.0/en/innodb-locking-reads.html
在这里插入图片描述
session2 使用 加锁读 才能读到 session1 已经 commit 的数据。

SELECT … FOR SHARE

Sets a shared mode lock on any rows that are read. Other sessions can read the rows, but cannot modify them until your transaction commits. If any of these rows were changed by another transaction that has not yet committed, your query waits until that transaction ends and then uses the latest values.

SELECT … FOR UPDATE

For index records the search encounters, locks the rows and any associated index entries, the same as if you issued an UPDATE statement for those rows. Other transactions are blocked from updating those rows, from doing SELECT … FOR SHARE, or from reading the data in certain transaction isolation levels. Consistent reads ignore any locks set on the records that exist in the read view. (Old versions of a record cannot be locked; they are reconstructed by applying undo logs on an in-memory copy of the record.)

Repeatable Read 对于 SELECT…FOR UPDATE(FOR SHARE)、UPDATE 、DELETE 语句,锁取决于语句是使用了具有唯一搜索条件的唯一索引,还是使用范围类型的搜索条件。

  1. 对于唯一搜索条件的唯一索引,InnoDB 只锁住索引找到的记录,而不会包含 gap 锁
  2. 对于其他的搜索条件,InnoDB 会锁住整个范围,使用 gap 锁或 next-key 锁来阻塞其他会话对被覆盖范围数据的插入。

READ COMMITTED 对于 SELECT…FOR UPDATE(FOR SHARE)、UPDATE 、DELETE 语句,InnoDB 只锁索引记录,不使用 gap 锁。由于不使用 gap 锁,可能会出现幻读(phantom)现象。

在这里插入图片描述

MySQL 独特的现象

在 Snapshot Isolation 隔离级别下,读、写操作都会基于事务开始时选择的一个快照,在事务提交时,要确保发生写操作的数据项的快照依旧是当前最新的数据,则事务提交成功,否则失败,符合 First-committer-wins 机制。

先看一下一个开源数据库PostgreSQL在REPEATABLE READ 隔离级别下的工作方式:

myc_test=# begin;                                               myc_test=# begin;                      
BEGIN                                                           BEGIN

myc_test=*# select * from test;                                 myc_test=*# select * from test;
 id | score                                                      id | score 
----+-------                                                    ----+-------
  1 |     2                                                       1 |     2
(1 row)                                                         (1 row)

                                                                myc_test=*# update test set score = 3 where id = 1;
                                                                UPDATE 1

                                                                myc_test=*# commit;
                                                                COMMIT

                                                                myc_test=# select * from test;
                                                                 id | score 
                                                                ----+-------
                                                                  1 |     3
                                                                (1 row)

myc_test=*# update test set score = 4 where id = 1;
ERROR:  could not serialize access due to concurrent update                        

session 1 和 session 2 同时开启,sesssion 2 先 update test set score = 3 where id = 1; 并且 commit 成功;session 1 再 update test set score = 4 where id = 1; 失败。

在这里插入图片描述
session A 和 session B 同时开启,sesssion A 先 update,session B 再 update ,会 block,然后 session A commit,session B 失败。

同样测试了 SQL SERVER,得到的结果与 PostgreSQL 是一致的。符合快照隔离下的 First-committer-wins 机制, 符合 Snapshot Isolation 隔离级别。

下面看一下MySQL在REPEATABLE READ 隔离级别下的工作方式:

MySQL root@127.0.0.1:myc_test> begin;                                   MySQL root@127.0.0.1:myc_test> begin;
Query OK, 0 rows affected                                               Query OK, 0 rows affected
Time: 0.001s                                                            Time: 0.001s
MySQL root@127.0.0.1:myc_test> select * from test;                      MySQL root@127.0.0.1:myc_test> select * from test;
+----+-------+                                                          +----+-------+
| id | score |                                                          | id | score |
+----+-------+                                                          +----+-------+
| 1  | 2     |                                                          | 1  | 2     |
+----+-------+                                                          +----+-------+
1 row in set                                                            1 row in set
Time: 0.010s                                                            Time: 0.010s
MySQL root@127.0.0.1:myc_test> update test set score = 3 where id = 1;
Query OK, 1 row affected
Time: 0.001s
MySQL root@127.0.0.1:myc_test> commit;
Query OK, 0 rows affected
Time: 0.023s
MySQL root@127.0.0.1:myc_test> select * from test;
+----+-------+
| id | score |
+----+-------+
| 1  | 3     |
+----+-------+
1 row in set
Time: 0.004s

                                                                        MySQL root@127.0.0.1:myc_test> select * from test;
                                                                        +----+-------+
                                                                        | id | score |
                                                                        +----+-------+
                                                                        | 1  | 2     |
                                                                        +----+-------+
                                                                        1 row in set
                                                                        Time: 0.007s
                                                                        MySQL root@127.0.0.1:myc_test> update test set score = 4 where id = 1;
                                                                        Query OK, 1 row affected
                                                                        Time: 0.001s
                                                                        MySQL root@127.0.0.1:myc_test> select * from test;
                                                                        +----+-------+
                                                                        | id | score |
                                                                        +----+-------+
                                                                        | 1  | 4     |
                                                                        +----+-------+
                                                                        1 row in set
                                                                        Time: 0.010s
                                                                        MySQL root@127.0.0.1:myc_test> commit;
                                                                        Query OK, 0 rows affected
                                                                        Time: 0.023s
                                                                        MySQL root@127.0.0.1:myc_test> select * from test;
                                                                        +----+-------+
                                                                        | id | score |
                                                                        +----+-------+
                                                                        | 1  | 4     |
                                                                        +----+-------+
                                                                        1 row in set
                                                                        Time: 0.005s

在这里插入图片描述

从上面的执行情况我们可以看到MySQL不符合快照隔离下的 First-committer-wins 机制, 不符合 Snapshot Isolation 隔离级别。可以看这个:http://mysql.taobao.org/monthly/2017/06/07/

TiDB在乐观事务时符合 First-committer-wins,但是在悲观事务时,行为则和MySQL一致。可以看文档:
TiDB与MySQL可重复读隔离级别的区别

业务程序员的错误

指业务程序员使用了不合适的隔离级别来操纵数据库,导致得到的最终结果不符合业务的一致性(consistency)。

Read Skew

错误的使用了会产生 Repeatable Read 的隔离级别(Read Uncommitted,Read Committed)

爱丽丝在银行有1000美元的储蓄,分为两个账户,每个500美元。现在一笔事务从她的一个账户中转移了100美元到另一个账户。如果她在事务处理的同时查看其账户余额列表,不幸地在转账事务完成前看到收款账户余额(余额为500美元),而在转账完成后看到另一个转出账户(已经转出100美元,余额400美元)。对爱丽丝来说,现在她的账户似乎只有900美元——看起来100美元已经消失了。
在这里插入图片描述

Lost Update

Read Uncommitted,Read Committed,MySQL的Repeatable Read 会出现 Lost Update

基于锁实现的 Repeatable Read 和 PostgresSQL的Repeatable Read 不会出现 Lost Update

(假设数据库中没有自增操作)每个客户端需要读取计数器的当前值,加 1 ,再回写新值。下图中,因为发生了两次增长,计数器应该从42增至44;但由于竞态条件,实际上只增至 43 。
在这里插入图片描述

Write Skew

Read Uncommitted,Read Committed,MySQL/PostgresSQL的Repeatable Read(Snaphot Isolation) 会出现 Write Skew

基于锁实现的 Repeatable Read 不会出现 Write Skew

首先,想象一下这个例子:你正在为医院写一个医生轮班管理程序。医院通常会同时要求几位医生待命,但底线是至少有一位医生在待命。医生可以放弃他们的班次(例如,如果他们自己生病了),只要至少有一个同事在这一班中继续工作。
现在想象一下,Alice和Bob是两位值班医生。两人都感到不适,所以他们都决定请假。不幸的是,他们恰好在同一时间点击按钮下班。下图说明了接下来的事情。
在这里插入图片描述
在两个事务中,应用首先检查是否有两个或以上的医生正在值班;如果是的话,它就假定一名医生可以安全地休班。由于数据库使用快照隔离,两次检查都返回 2 ,所以两个事务都进入下一个阶段。Alice更新自己的记录休班了,而Bob也做了一样的事情。两个事务都成功提交了,现在没有医生值班了。违反了至少有一名医生在值班的要求。

再看一个例子,即 T1 尝试把 x 的值赋给 y,T2 尝试把 y 的值赋给 x,如果这两个事务 Serializable 的执行,那么在结束之后 x 和 y 应该拥有一样的值,但是在 Write Skew 中,并发操作使得他们的值互换了。
在这里插入图片描述

可以将写入偏差视为丢失更新问题的一般化。如果两个事务读取相同的对象,然后更新其中一些对象(不同的事务可能更新不同的对象),则可能发生写入偏差。在多个事务更新同一个对象的特殊情况下,就会发生丢失更新

避免 Lost Update & Write Skew

在 Serializable 的隔离级别中进行操作当然避免,但是 MySQL 如何在 Repeatable Read 隔离级别中避免 Lost Update & Write Skew。

假设有A、B两个用户同时各购买一件 id=1 的商品,用户A获取到的库存量为 1000,用户B获取到的库存量也为 1000,用户A完成购买后修改该商品的库存量为 999,用户B完成购买后修改该商品的库存量为 999,此时库存量数据产生了不一致。(类似上面计数器的例子)

有两种解决方案:

悲观方案:每次获取商品时,对该商品通过 select for update 加排他锁,也就是在用户A获取获取 id=1 的商品信息时对该行记录加锁,期间其他用户阻塞等待访问该记录。

begin;
select * from goods where id = 1 for update;
update goods set stock = stock - 1 where id = 1;
commit;

乐观方案:每次获取商品时,不对该商品加锁。在更新数据的时候需要比较程序中的库存量与数据库中的库存量是否相等,如果相等则进行更新,反之程序重新获取库存量,再次进行比较,直到两个库存量的数值相等才进行数据更新。乐观锁适合读取频繁的场景。

#不加锁获取 id=1 的商品对象
select * from goods where id = 1

begin;
#更新 stock 值,这里需要注意 where 条件 “stock = cur_stock”,只有程序中获取到的库存量与数据库中的库存量相等才执行更新
update goods set stock = stock - 1 where id = 1 and stock = cur_stock;
commit;

参考

https://juejin.cn/post/6844903927536844808
https://juejin.cn/post/6844903952165634061
https://www.jianshu.com/p/eb3f56565b42
http://blog.sina.com.cn/s/blog_499740cb0100ugs7.html
https://blog.csdn.net/taylor_tao/article/details/7063639
https://www.jianshu.com/p/cef49aeff36b
https://blog.csdn.net/claram/article/details/54023216
https://vonng.gitbooks.io/ddia-cn/content/ch7.html
https://pingcap.com/zh/blog/take-you-through-the-isolation-level-of-tidb-1
https://mp.weixin.qq.com/s?__biz=MzUxODAzNDg4NQ==&mid=2247496769&idx=1&sn=30990d141185303fd0c7ecf63c125b30&chksm=f98db0ebcefa39fda085edb2596398c046541851d9650a7a8328874476984e4caac1055c4c68&scene=178&cur_album_id=1955634887135199237#rd
https://lenshood.github.io/2020/11/30/isolation-level-with-mysql/
https://opensource.actionsky.com/20210818-mysql/
http://mysql.taobao.org/monthly/2017/06/07/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值