我所理解的MySQL之四:事務、隔離等級及MVCC

coldplay.xixi
發布: 2020-10-27 17:03:22
轉載
2593 人瀏覽過

mysql教程#欄目的介紹MySQL相關的交易、隔離等級及MVCC。

我所理解的MySQL之四:事務、隔離等級及MVCC

## MySQL系列的第四篇,主要內容是事務,包括ACID特性、隔離等級、可讀讀、不可重複讀、幻讀的理解以及多版本並發控制(MVCC)等內容。

事務( Transaction)能夠保證一組不可分割的原子性操作集合或都執行,或都不執行。在MySQL常用的儲存引擎中,InnoDB是支援交易的,原生的MyISAM引擎則不支援交易

在本文中,若未特別說明,使用的資料表及資料如下所示:

#

CREATE TABLE `user`  (  `id` int(11) DEFAULT NULL,  `name` varchar(12) DEFAULT NULL) ENGINE = InnoDB;insert into user values(1, '刺猬');复制代码
登入後複製
1. ACID 四大特性

首先需要理解的事務 ACID 四大特性,即原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)、持久性(Durability),這也是事務的四個基本要素。

為了詳細解釋ACID 功能,這裡先設想一個場景:我向你轉動100 元。

假設這個操作可以分為以下幾個部分(假設我和您的帳戶扣款100元):

    查詢我的帳戶餘額
  1. 我的帳戶扣款100元
  2. 100元開始轉移
  3. 查詢您的帳戶餘額
  4. 您的帳戶到100元
  5. ##1.1原子性(Atomicity)

事務的原子性是指:一個事務必須是不可再分割的最小工作單元,一個事務中的操作要么都成功,要么都失敗,不可能存在只執行一個事務中部分操作的情況。

在上述的輪轉場景中,原子性就要求了這五個步驟或都執行,或都不執行,不可能存在我的帳戶扣款100元,而你的帳戶100元沒有到帳的情況。

1.2一致性(一致性)

交易的一致性是指:資料庫總是從一個一致性狀態轉換到另一個一致性狀態,一致性重要性是資料的可見性,資料的中間狀態對外是不可見的。

同时,事务的一致性要求符合开发人员定义的约束,如金额大于0、身高大于0等。

在上述的转账场景中,一致性能够保证最终执行完整个转账操作后,我账户的扣款金额与你账户到账金额是一致的,同时如果我和你的账户余额不满足金额的约束(如小于0),整个事务会回滚。

1.3 隔离性(Isolation)

事务的隔离性是指:在一次状态转换过程中不会受到其他状态转换的影响。

假设我和你都有100元,我发起两次转账,转账金额都是50元,下面使用伪代码来表示的操作步骤:

  1. 查询我的账户余额 read my
  2. 我的账户扣款50元 my=my-50
  3. 50元开始转移
  4. 查询你的账户余额 read yours
  5. 你的账户到账50元 yours=yours+50

如果未保证隔离性就可能发生下面的情况:

时刻 第一次转账 第二次转账 我的账户余额 你的账户余额
1 read my(100)
my=100 yours=100
2
read my(100) my=100 yours=100
3 my=my-50=100-50=50
my=50 yours=100
4 read yours(100) my=my-50=100-50=50 my=50 yours=100
5 yours=yours+50=100+50=150
my=50 yours=150
6
read yours(150) my=50 yours=150
7
yours=yours+50=150+50=200 my=50 yours=200
7 end end my=50 yours=200

两次转账后,最终的结果是我的账户余额为50元,你的账户余额为200元,这显然是不对的。

而如果在保证事务隔离性的情况下,就不会发生上面的情况,损失的只是一定程度上的一致性。

1.4 持久性(Durability)

事务的持久性是指:事务在提交以后,它所做的修改就会被永久保存到数据库。

在上述的转账场景中,持久性就保证了在转账成功之后,我的账户余额为0,你的账户余额为200。

2. 自动提交与隐式提交

2.1 自动提交

在 MySQL 中,我们可以通过 begin 或 start transaction 来开启事务,通过 commit 来关闭事务,如果 SQL 语句中没有这两个命令,默认情况下每一条 SQL 都是一个独立的事务,在执行完成后自动提交

比如:

update user set name='重塑' where id=1;复制代码
登入後複製

假设我只执行这一条更新语句,在我关闭 MySQL 客户端然后重新打开一个新的客户端后,可以看到 user 表中的 name 字段值全变成了「重塑」,这也印证了这条更新语句在执行后已被自动提交。

自动提交是 MySQL 的一个默认属性,可以通过 SHOW VARIABLES LIKE 'autocommit' 语句来查看,当它的值为 ON 时,就代表开启事务的自动提交。

mysql> SHOW VARIABLES LIKE 'autocommit';
+---------------+-------+| Variable_name | Value |
+---------------+-------+| autocommit    | ON    |
+---------------+-------+1 row in set (0.00 sec)复制代码
登入後複製

我们可以通过 SET autocommit = OFF 来关闭事务的自动提交。

2.2 隐式提交

然而,即便我们已经将 autocommit 变量的值改为 OFF 关闭事务自动提交了,在执行某些 SQL 语句的时候,MySQL 还是会将事务自动提交掉,这被称为隐式提交

会触发隐式提交的 SQL 语句有:

  • DDL(Data definition language,数据定义语言),如 create, drop, alter, truncate
  • 修改 MySQL 自带表数据的语句,如 create/drop user, grant, set password
  • 在一个事务中,开启一个新的事务,会隐式提交上一个事务,如:
时刻 事务A 事务B
1 begin;
2 update user set name='重塑' where id=1;
3
select name from user where id=1;(N1)
4 begin;
5
select name from user where id=1;(N2)

在事務B中有兩個查詢語句N1和N2,執行的結果是N1=刺猬,N2=重塑,由此可以證明。

  • 其他還有一些管理語句就不一一舉例了,可自行百度。

3. 隔離等級

交易的隔離等級規定了一個交易中所做的修改,在事務內和事務間的可見性。較低層級的隔離通常可以執行較高的並發,系統開銷也較低。

在SQL 標準中定義了四種交易的隔離級別,分別是讀取未提交(Read Uncommitted)、讀取已提交(Read Committed)、可重複讀取(Repeatable Read)、可串行化( Serializable)。

為了詳細解釋這四個隔離等級及它們各自發生的現象,假設有兩個交易即將執行,執行內容如下表:

##3#update user set name='重塑' where id=1;4select name from user where id=1; (N1)5#commit;6select name from user where id=1;(N2)##78#在交易A和交易B執行的過程中,有三個查詢N1,N2,N3,在每個隔離等級下,它們值的情況是不同的,下面分別討論。
時刻 事務A 事務B
#1 begin;
2
#begin;




commit;
#select name from user where id=1;(N3)

3.1 讀取未提交(Read Uncommitted)

在讀取未提交的隔離等級下,

交易中的修改,即使沒有提交,對其他交易也都是可見的

在上述場景中,若資料庫的隔離等級為讀取未提交,由於事務A可以讀取未提交事務B修改後的數據,即時刻3中事務B的修改對事務A可見,所以N1=重塑,N2=重塑,N3=重塑。

3.2 讀取已提交(Read Committed)

在讀取已提交的隔離等級下,

交易中的修改只有在提交之後,才會對其他交易可見

在上述場景中,若資料庫的隔離等級為讀取已提交,由於事務A只能讀取事務B提交後的數據,即時刻3中事務B的修改對事務A不可見,N2處的查詢在事務B提交之後,故對事務A可見。所以N1=刺猬,N2=重塑,N3=重塑。

3.3 可重複讀取(Repeatable Read)

可重複讀取是 MySQL 的預設交易隔離等級

。在可重複讀取的隔離等級下,一個交易中多次查詢相同的記錄,結果總是一致的在上述場景中,若資料庫的隔離等級為可重複讀,由於查詢N1和N2在一個事務中,所以它們的值都是「刺蝟」,而N3是在事務A提交以後再進行的查詢,事務B的修改是可見的,所以N3=重塑。

3.4 可串列化(Serializable)

在可串列化的隔離等級下,

交易都是串列執行的,讀會加讀鎖,寫會加寫鎖

,交易不會並發執行,所以也就不會發生異常。 在上述場景中,若資料庫的隔離等級為可串行化,首先開啟事務A,在開啟事務B時被阻塞,直到事務A提交之後才會開啟事務B,所以N1=刺猬,N2=刺蝟。而N3處的查詢會在事務B提交之後才執行(事務B先被阻塞,執行順序在N3查詢語句之前),所以N3=重塑。

4. 隔離等級導致的問題

在不同的交易隔離等級中,如果遇到交易並發執行,就會出現很多問題,如髒讀(Dirty Read)、不可重複讀(Non-Repeatable Read)、幻讀(Phantom Read)等,以下就分別用不同的例子來詳細說明這些問題。

4.1 髒讀(Dirty Read)

髒讀

(Dirty Read)是指一個交易可以讀取另一個未提交交易修改的資料。 看下面的案例,假設隔離等級為讀取未提交:

1#23456#7#

在讀取未提交的隔離等級下,N1的值是「重塑」,由於事務B的回滾,N2的值是「刺蝟」。這裡在N1處就發生了髒讀,顯然N1處的查詢結果是一個髒數據,會對正常業務產生影響。

髒讀會發生在讀取未提交的隔離等級。

4.2 不可重複讀取(Non-Repeatable Read)

#無法重複讀取(Non-Repeatable Read)是指,兩次執行相同的查詢可能會得到不一樣的結果。

繼續使用介紹隔離等級時的AB交易案例,同時假設隔離等級為讀取已提交:

#交易A 。交易B
begin;

begin;

update user set name='重塑' where id=1;
select name from user where id=1;(N1)

rollback;
select name from user where id=1;(N2)
commit;
交易A 事務B
1 #begin;
2
begin;
#3
update user set name='重塑' where id=1;
#4 select name from user where id=1;(N1)
#5
#commit;
6 select name from user where id=1;(N2)
7 #commit;

8select name from user where id=1;(N3)

在讀取已提交的隔離等級下,交易可以讀取到其他交易提交的資料。在上述案例中結果是N1=刺猬,N2=重塑,N3=重塑,在事務A中,有兩次相同的查詢N1和N2,但是這兩次查詢的結果並不相同,這就發生了不可重複讀。 幻讀(Phantom Read)是指,一個事務在讀取某個範圍內記錄時,另一個事務在在該範圍內插入一筆新記錄1begin;##2 34#561lect name from user;(N2 )7select name from user for update;(N3)
不可重複讀取會發生在讀取未提交、讀取已提交的隔離等級。 4.3 幻讀(Phantom Read)
,當先前的事務再次讀取這個範圍的記錄時,會讀到這條新記錄。 看下面的案例,假設此時隔離等級為可重複讀取:
#交易A #事務B

select name from user;(N1)
# #begin;
insert into user values(2, '五條人');
commit;

######### ##########8######commit;######################

事務A有三次查詢,在N1和N2之間,事務B執行了一條 insert語句並提交,N3處的查詢使用的是 for update

N1處的結果很顯然只有“刺猬”,N2處的結果由於事務A開啟在事務B之前,所以也是“刺猬”,而N3處的結果理論上在可重複讀的隔離級別中也應該只有「刺蝟」,但實際上N2的結果是「刺蝟」和「五條人」,這就發生了幻讀。

這就很奇怪了,不是說可重複讀的隔離等級能夠保證一個事務中多次查詢相同的記錄,結果總是一致的嗎?這種結果並不滿足可重複讀的定義。

事實上,在可重複讀的隔離等級下,如果使用的是目前讀取,那麼就可能發生幻讀現象。

當前讀和快照讀會在下文中介紹事務的實現原理及 MVCC 時討論,這裡先給一個結論。

幻讀會發生在讀取未提交、讀取已提交、可重複讀取的隔離等級。

這裡需要額外注意的是:幻讀和不可重複讀都是說在一個事務中的同一個查詢語句結果不同,但幻讀更著重於查詢到其他交易新插入的資料(insert)或其他交易刪除的資料(delete),而不可重複讀的範圍更廣,只要結果不同就可以認為是不可重複讀,但一般我們認為不可重複讀更著重於其他事務對資料的更新(update)。

4.4 小結

透過上面的描述,我們已經知道四種隔離等級的概念以及它們分別會遇到的問題,事務的隔離等級越高,隔離性就越強,所遇到的問題也就越少。但同時,隔離等級越高,並發能力越弱。

下表是隔離等級的概念不同隔離等級會發生的問題情況的小結:

# #√交易中的修改,即使沒有提交,對其他交易也都是可見的讀未提交#交易中的修改只有在提交之後,才會對其他交易可見一個交易中多次查詢相同的記錄,結果總是一致的
#髒讀 不可重複讀取 幻讀 概念
已提交



可重複讀取
#√

可串行化
######################################################################### ########交易都是串列執行的,讀會加讀鎖,寫會加寫鎖############

5. MVCC

MVCC(Multi-Version Concurrency Control)即多版本并发控制,这是 MySQL 为了提高数据库并发性能而实现的。它可以在并发读写数据库时,保证不同事务的读-写操作并发执行,同时也能解决脏读、不可重复读、幻读等事务隔离问题。

在前文讨论幻读的时候提到过当前读的概念,正是由于当前读,才会在可重复读的隔离级别下也会发生幻读的情况。

在解释可重复读隔离级别下发生幻读的原因之前,首先介绍 MVCC 的实现原理。

5.1 MVCC 的实现原理

首先我们需要知道,InnoDB 的数据页中每一行的数据是有隐藏字段的:

  • DB_ROW_ID: 隐式主键,若表结构中未定义主键,InnoDB 会自动生成该字段作为表的主键
  • DB_TRX_ID: 事务ID,代表修改此行记录的最后一次事务ID
  • DB_ROLL_PTR: 回滚指针,指向此行记录的上一个版本(上一个事务ID对应的记录)

每一条修改语句都会相应地记录一条回滚语句(undo log),如果把每一条回滚语句视为一条数据表中的记录,那么通过事务ID和回滚指针就可以将对同一行的修改记录看作一个链表,链表上的每一个节点就是一个快照版本,这就是 MVCC 中多版本的意思。

举个例子,假设对 user 表中唯一的一行「刺猬」进行多次修改。

update user set name='重塑' where id=1;update user set name='木马' where id=1;update user set name='达达' where id=1;复制代码
登入後複製

那么这条记录的我所理解的MySQL之四:事務、隔離等級及MVCC就是:

我所理解的MySQL之四:事務、隔離等級及MVCC

在这个我所理解的MySQL之四:事務、隔離等級及MVCC中,头结点就是当前记录的最新版本。DB_TRX_ID 事务ID 字段是非常重要的属性,先 Mark 一下。

除此之外,在读已提交(RC,Read Committed)和可重复读(RR,Repeatable Read)的隔离级别中,事务在启动的时候会创建一个读视图(Read View),用它来记录当前系统的活跃事务信息,通过读视图来进行本事务之间的可见性判断

在读视图中有两个重要的属性:

  • 当前事务ID:表示生成读视图的事务的事务ID
  • 事务ID列表:表示在生成读视图时,当前系统中活跃着的事务ID列表
  • 最小事务ID:表示在生成读视图时,当前系统中活跃着的最小事务ID
  • 下一个事务ID:表示在生成读视图时,系统应该分配给下一个事务的事务ID

需要注意下一个事务I的值,并不是事务ID列表中的最大值+1,而是当前系统中已存在过的事务的最大值+1。例如当前数据库中活跃的事务有(1,2),此时事务2提交,同时又开启了新事务,在生成的读视图中,下一个事务ID的值为3。

我们通过将我所理解的MySQL之四:事務、隔離等級及MVCC与读视图两者结合起来,来进行并发事务间可见性的判断,判断规则如下(假设现在要判断事务A是否可以访问到事务B的修改记录):

  • 若事务B的当前事务ID小于事务A的最小事务ID的值,代表事务B是在事务A生成读视图之前就已经提交了的,所以事务B对于事务A来说是可见的。
  • 若事务B的当前事务ID大于或等于事务A下一个事务ID的值,代表事务B是在事务A生成读视图之后才开启,所以事务B对于事务A来说是不可见的。
  • 若事务B的当前事务ID在事务A的最小事务ID下一个事务ID之间(左闭右开,[最小事务ID, 下一个事务ID)),需要分两种情况讨论:
    • 若事务B的当前事务ID在事务A的事务ID列表中,代表创建事务A时事务B还是活跃的,未提交,所以事务B对于事务A来说是不可见的。
    • 若事务B的当前事务ID不在事务A的事务ID列表中,代表创建事务A时事务B已经提交,所以事务B对于事务A来说是可见的。

如果事务B对于事务A来说是不可见的,就需要顺着修改记录的我所理解的MySQL之四:事務、隔離等級及MVCC,从回滚指针开始往前遍历,直到找到第一个对于事务A来说是可见的事务ID,或者遍历完我所理解的MySQL之四:事務、隔離等級及MVCC也未找到(表示这条记录对事务A不可见)。

这就是 MVCC 的实现原理。

5.2 讀取視圖的創建時機

這裡需要注意的是讀取視圖的創建時機,在上面的論述中我們已經知道事務在啟動時會創建一個讀取視圖(Read View),而開啟一個事務有兩種方式,一是begin/start transaction,二是start transaction with consistent snapshot#,透過這兩種方式開啟事務,建立讀取視圖的時機也是不同的:

  • 如果是以begin/start transaction 方式開啟事務,讀取視圖會在執行第一個快照讀語句時創建
  • 如果以start transaction with consistent snapshot 方式開啟事務,同時便會建立讀取視圖

5.3 MVCC 的運行過程

為了詳細說明MVCC 的運行過程,以下舉個例子,假設目前存在有兩個交易(交易隔離等級為MySQL 預設的可重複讀取):

這裡需要注意的是事務的啟動時機,在上面的論述中我們已經知道事務在啟動時會創建一個讀取視圖(Read View),而開啟一個事務有兩種方式,一是begin/start transaction,二是start transaction with consistent snapshot,透過這兩種方式開啟事務,建立讀取視圖的時機也是不同的:

  • #如果是以begin/start transaction 方式開啟事務,讀取視圖會在執行第一個快照讀取語句時建立
  • 如果以start transaction with consistent snapshot 方式開啟事務,同時便會建立讀取視圖
##交易B1start transaction with consistent snapshot;2start transaction with consistent snapshot;3update user set name ='重塑' where id=1;4select name from user where id=1;(N1)#5commit;##6#7##

然後根據上面所描述的版本鏈以及兩個事務開啟時的讀取視圖來分析 MVCC 的運行過程。

我所理解的MySQL之四:事務、隔離等級及MVCC

上圖是兩個交易開啟時的讀取視圖,而當交易B的更新語句執行之後,id=1行的版本鏈如下所示。

mvcc2-我所理解的MySQL之四:事務、隔離等級及MVCC

先來看N1處的查詢語句,事務B的目前事務ID=2,其值等於事務A的下一個事務ID,所以依照上文所論述的可見性判斷,事務B對於事務A來說是不可見的,需要循著當前行的版本鍊網上檢索。

於是循著版本鏈來到DB_TRX_ID=1事務ID=1的歷史版本,恰巧等於事務A的事務ID值,也就是事務A開啟時該行的版本,此版本對於事務A來說當然是可見的,所以讀取到了id=1行的name='刺猬',即最終N1=刺猬。

再來看N2處的查詢語句,此時事務B已提交,版本鏈還是如上圖所示,由於目前版本的事務ID等於事務A讀視圖中的下一個事務ID,所以目前版本的記錄對於事務A來說是不可見的,所以同樣N2=刺蝟。

這裡需要注意的是,若例子中事務A的時刻4語句變更為對該行的更新語句,那麼事務A便會等待事務B提交之後再執行更新語句,這是因為事務B未提交,即事務B對於id=1行的寫鎖未釋放,而事務A也要更新該行,必須是更新當前的最新版本(當前讀取)才可以,所以事務A就被阻塞了,必須等待事務B對該行的寫鎖釋放,才會繼續執行更新語句。

5.4 RC 與RR 產生讀取視圖的時機對比

上面所討論的MVCC 運行過程都是針對可重複讀取(RR, Repeatable Read)隔離級別的,如果是讀已提交(RC, Read Committed)等級呢?

上文中已經討論過讀已提交隔離級別中關於不可重複讀的情況了,這裡就不再舉例,直接給出結論就可以了。

  • 可重複讀取(RR, Repeatable Read)隔離等級下產生讀取視圖(Read View)的時機是開啟交易的時候
  • 讀取已提交(RC, Read Committed)隔離等級下產生讀取視圖(Read View)的時機是每一語句執行前

對於上文中描述MVCC 執行過程中的例子,如果隔離等級是讀取已提交(RC, Read Committed ):

  • N1處的查詢語句,由於事務B還未提交,事務A可見的版本依舊是事務ID=1的版本,所以N1=刺猬
  • N2處的查詢語句,事務B已提交,N2處查詢語句執行時也會產生讀取視圖,其目前事務ID=3,而在該記錄的版本鏈中,當前版本的事務ID DB_TRX_ID=2,在N2查詢語句事務ID之前,是可見的,所以N2=重塑

5.5 目前讀與快照讀

  • 目前讀:讀取記錄的最新版本
  • 快照讀取:讀取記錄時會根據某一規則讀取交易可見版本的記錄

5.6 可重複讀發生幻讀的原因

在理解了MVCC 之後,我們再來看在可重複讀隔離等級下發生幻讀的原因。上文中說到正是由於當前讀,才會在可重複讀的隔離級別下發生幻讀的情況,首先來回顧一下例子。

#交易A





select name from user where id=1;(N2)
commit;
##1begin;2select name from user;(N1)3#begin;##45678

N1,N2處的查詢想必已經十分明確都是「刺蝟」了。而在N3處所使用的查詢語句是for update,使用它進行查詢就會對目標記錄添加一把“行級鎖”,行級鎖的意義以後再說,現在只需要知道for update能夠鎖定目標記錄就可以了。

加鎖自然是防止別人修改,那麼理所當然,鎖住的當然也就是記錄的最新版本了。所以,在使用for update來查詢的時候,會使用目前讀,讀到目標記錄的最新版本,所以在N3處的查詢語句就會把事務B中本對於事務A來說不可見的記錄也查詢出來,也就發生了幻讀。

使用目前讀取的語句有:

  • select ... for update
  • select ... lock in share mode(共享讀鎖)
  • update ...
  • insert ...
  • delete ...

#6. 溫故知新

    ##交易的ACID四大特性
  1. 自動提交與隱含提交(隱含提交的語句)
  2. #交易的隔離等級
  3. 交易各個隔離等級可能出現的問題
  4. MVCC 的實作原則與流程
  5. RC 與RR 產生讀取視圖的時機

#更多相關免費學習推薦:mysql教學(影片)

「時刻」 ##交易B




#insert into user values(2, '五條人');

commit;
select name from user;(N2)
select name from user for update;(N3)
commit;

以上是我所理解的MySQL之四:事務、隔離等級及MVCC的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:juejin.im
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
作者最新文章
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!