一文完全讀懂MySQL事務的四大隔離級別

前言

以前分析一個死鎖問題,發現本身對數據庫隔離級別理解還不夠清楚,因此趁着這幾天假期,整理一下MySQL事務的四大隔離級別相關知識,但願對你們有幫助~html

事務

什麼是事務?

事務,由一個有限的數據庫操做序列構成,這些操做要麼所有執行,要麼所有不執行,是一個不可分割的工做單位。mysql

假如A轉帳給B 100 元,先從A的帳戶里扣除 100 元,再在 B 的帳戶上加上 100 元。若是扣完A的100元后,還沒來得及給B加上,銀行系統異常了,最後致使A的餘額減小了,B的餘額卻沒有增長。因此就須要事務,將A的錢回滾回去,就是這麼簡單。sql

事務的四大特性

  • 原子性: 事務做爲一個總體被執行,包含在其中的對數據庫的操做要麼所有都執行,要麼都不執行。
  • 一致性: 指在事務開始以前和事務結束之後,數據不會被破壞,假如A帳戶給B帳戶轉10塊錢,無論成功與否,A和B的總金額是不變的。
  • 隔離性: 多個事務併發訪問時,事務之間是相互隔離的,一個事務不該該被其餘事務干擾,多個併發事務之間要相互隔離。。
  • 持久性: 表示事務完成提交後,該事務對數據庫所做的操做更改,將持久地保存在數據庫之中。

事務併發存在的問題

事務併發執行存在什麼問題呢,換句話說就是,一個事務是怎麼幹擾到其餘事務的呢?看例子吧~數據庫

假設如今有表:安全

CREATE TABLE `account` (
  `id` int(11) NOT NULL,
  `name` varchar(255) DEFAULT NULL,
  `balance` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `un_name_idx` (`name`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

表中有數據:session

髒讀(dirty read)

假設如今有兩個事務A、B:數據結構

  • 假設如今A的餘額是100,事務A正在準備查詢Jay的餘額
  • 這時候,事務B先扣減Jay的餘額,扣了10
  • 最後A 讀到的是扣減後的餘額

由上圖能夠發現,事務A、B交替執行,事務A被事務B干擾到了,由於事務A讀取到事務B未提交的數據,這就是髒讀併發

不可重複讀(unrepeatable read)

假設如今有兩個事務A和B:mvc

  • 事務A先查詢Jay的餘額,查到結果是100
  • 這時候事務B 對Jay的帳戶餘額進行扣減,扣去10後,提交事務
  • 事務A再去查詢Jay的帳戶餘額發現變成了90

事務A又被事務B干擾到了!在事務A範圍內,兩個相同的查詢,讀取同一條記錄,卻返回了不一樣的數據,這就是不可重複讀高併發

幻讀

假設如今有兩個事務A、B:

  • 事務A先查詢id大於2的帳戶記錄,獲得記錄id=2和id=3的兩條記錄
  • 這時候,事務B開啓,插入一條id=4的記錄,而且提交了
  • 事務A再去執行相同的查詢,卻獲得了id=2,3,4的3條記錄了。

事務A查詢一個範圍的結果集,另外一個併發事務B往這個範圍中插入/刪除了數據,並靜悄悄地提交,而後事務A再次查詢相同的範圍,兩次讀取獲得的結果集不同了,這就是幻讀

事務的四大隔離級別實踐

既然併發事務存在髒讀、不可重複、幻讀等問題,InnoDB實現了哪幾種事務的隔離級別應對呢?

  • 讀未提交(Read Uncommitted)
  • 讀已提交(Read Committed)
  • 可重複讀(Repeatable Read)
  • 串行化(Serializable)

讀未提交(Read Uncommitted)

想學習一個知識點,最好的方式就是實踐之。好了,咱們去數據庫給它設置讀未提交隔離級別,實踐一下吧~

先把事務隔離級別設置爲read uncommitted,開啓事務A,查詢id=1的數據

set session transaction isolation level read uncommitted;
begin;
select * from account where id =1;

結果以下:


這時候,另開一個窗口打開mysql,也把當前事務隔離級別設置爲read uncommitted,開啓事務B,執行更新操做

set session transaction isolation level read uncommitted;
begin;
update account set balance=balance+20 where id =1;

接着回事務A的窗口,再查account表id=1的數據,結果以下:

能夠發現,在讀未提交(Read Uncommitted) 隔離級別下,一個事務會讀到其餘事務未提交的數據的,即存在髒讀問題。事務B都還沒commit到數據庫呢,事務A就讀到了,感受都亂套了。。。實際上,讀未提交是隔離級別最低的一種。

已提交讀(READ COMMITTED)

爲了不髒讀,數據庫有了比讀未提交更高的隔離級別,即已提交讀

把當前事務隔離級別設置爲已提交讀(READ COMMITTED),開啓事務A,查詢account中id=1的數據

set session transaction isolation level read committed;
begin;
select * from account where id =1;

另開一個窗口打開mysql,也把事務隔離級別設置爲read committed,開啓事務B,執行如下操做

set session transaction isolation level read committed;
begin;
update account set balance=balance+20 where id =1;

接着回事務A的窗口,再查account數據,發現數據沒變:

咱們再去到事務B的窗口執行commit操做:

commit;

最後回到事務A窗口查詢,發現數據變了:

由此能夠得出結論,隔離級別設置爲已提交讀(READ COMMITTED) 時,已經不會出現髒讀問題了,當前事務只能讀取到其餘事務提交的數據。可是,你站在事務A的角度想一想,存在其餘問題嗎?

提交讀的隔離級別會有什麼問題呢?

在同一個事務A裏,相同的查詢sql,讀取同一條記錄(id=1),讀到的結果是不同的,即不可重複讀。因此,隔離級別設置爲read committed的時候,還會存在不可重複讀的併發問題。

可重複讀(Repeatable Read)

若是你的老闆要求,在同個事務中,查詢結果必須是一致的,即老闆要求你解決不可重複的併發問題,怎麼辦呢?老闆,臣妾辦不到?來實踐一下可重複讀(Repeatable Read) 這個隔離級別吧~

哈哈,步驟一、二、6的查詢結果都是同樣的,即repeatable read解決了不可重複讀問題,是否是內心美滋滋的呢,終於解決老闆的難題了~

RR級別是否解決了幻讀問題呢?

再來看看網上的一個熱點問題,有關於RR級別下,是否解決了幻讀問題?咱們來實踐一下:


由圖可得,步驟2和步驟6查詢結果集沒有變化,看起來RR級別是已經解決幻讀問題了~
可是呢,RR級別仍是存在這種現象

其實,上圖若是事務A中,沒有update account set balance=200 where id=5;這步操做,select * from account where id>2查詢到的結果集確實是不變,這種狀況沒有幻讀問題。可是,有了update這個騷操做,同一個事務,相同的sql,查出的結果集不一樣,這個是符合了幻讀的定義~

這個問題,親愛的朋友,你以爲它算幻讀問題嗎?

串行化(Serializable)

前面三種數據庫隔離級別,都有必定的併發問題,如今放大招吧,實踐SERIALIZABLE隔離級別。

把事務隔離級別設置爲Serializable,開啓事務A,查詢account表數據

set session transaction isolation level serializable;
select @@tx_isolation;
begin;
select * from account;

另開一個窗口打開mysql,也把事務隔離級別設置爲Serializable,開啓事務B,執行插入一條數據:

set session transaction isolation level serializable;
select @@tx_isolation;
begin;
insert into account(id,name,balance) value(6,'Li',100);

執行結果以下:

由圖可得,當數據庫隔離級別設置爲serializable的時候,事務B對錶的寫操做,在等事務A的讀操做。其實,這是隔離級別中最嚴格的,讀寫都不容許併發。它保證了最好的安全性,性能倒是個問題~

MySql隔離級別的實現原理

實現隔離機制的方法主要有兩種:

  • 讀寫鎖
  • 一致性快照讀,即 MVCC

MySql使用不一樣的鎖策略(Locking Strategy)/MVCC來實現四種不一樣的隔離級別。RR、RC的實現原理跟MVCC有關,RU和Serializable跟鎖有關。

讀未提交(Read Uncommitted)

官方說法:

SELECT statements are performed in a nonlocking fashion, but a possible earlier version of a row might be used. Thus, using this isolation level, such reads are not consistent.

讀未提交,採起的是讀不加鎖原理。

  • 事務讀不加鎖,不阻塞其餘事務的讀和寫
  • 事務寫阻塞其餘事務寫,但不阻塞其餘事務讀;

串行化(Serializable)

官方的說法:

InnoDB implicitly converts all plain SELECT statements to SELECT ... FOR SHARE if autocommit is disabled. If autocommit is enabled, the SELECT is its own transaction. It therefore is known to be read only and can be serialized if performed as a consistent (nonlocking) read and need not block for other transactions. (To force a plain SELECT to block if other transactions have modified the selected rows, disable autocommit.)

  • 全部SELECT語句會隱式轉化爲SELECT ... FOR SHARE,即加共享鎖。
  • 讀加共享鎖,寫加排他鎖,讀寫互斥。若是有未提交的事務正在修改某些行,全部select這些行的語句都會阻塞。

MVCC的實現原理

MVCC,中文叫多版本併發控制,它是經過讀取歷史版本的數據,來下降併發事務衝突,從而提升併發性能的一種機制。它的實現依賴於隱式字段、undo日誌、快照讀&當前讀、Read View,所以,咱們先來了解這幾個知識點。

隱式字段

對於InnoDB存儲引擎,每一行記錄都有兩個隱藏列DB_TRX_ID、DB_ROLL_PTR,若是表中沒有主鍵和非NULL惟一鍵時,則還會有第三個隱藏的主鍵列DB_ROW_ID

  • DB_TRX_ID,記錄每一行最近一次修改(修改/更新)它的事務ID,大小爲6字節;
  • DB_ROLL_PTR,這個隱藏列就至關於一個指針,指向回滾段的undo日誌,大小爲7字節;
  • DB_ROW_ID,單調遞增的行ID,大小爲6字節;

undo日誌

  • 事務未提交的時候,修改數據的鏡像(修改前的舊版本),存到undo日誌裏。以便事務回滾時,恢復舊版本數據,撤銷未提交事務數據對數據庫的影響。
  • undo日誌是邏輯日誌。能夠這樣認爲,當delete一條記錄時,undo log中會記錄一條對應的insert記錄,當update一條記錄時,它記錄一條對應相反的update記錄。
  • 存儲undo日誌的地方,就是回滾段

多個事務並行操做某一行數據時,不一樣事務對該行數據的修改會產生多個版本,而後經過回滾指針(DB_ROLL_PTR)連一條Undo日誌鏈

咱們經過例子來看一下~

mysql> select * from account ;
+----+------+---------+
| id | name | balance |
+----+------+---------+
|  1 | Jay  |     100 |
+----+------+---------+
1 row in set (0.00 sec)
  • 假設表accout如今只有一條記錄,插入該該記錄的事務Id爲100
  • 若是事務B(事務Id爲200),對id=1的該行記錄進行更新,把balance值修改成90

事務B修改後,造成的Undo Log鏈以下:

快照讀&當前讀

快照讀:

讀取的是記錄數據的可見版本(有舊的版本),不加鎖,普通的select語句都是快照讀,如:

select * from account where id>2;

當前讀:

讀取的是記錄數據的最新版本,顯示加鎖的都是當前讀

select * from account where id>2 lock in share mode;
select * from  account where id>2 for update;

Read View

  • Read View就是事務執行快照讀時,產生的讀視圖。
  • 事務執行快照讀時,會生成數據庫系統當前的一個快照,記錄當前系統中還有哪些活躍的讀寫事務,把它們放到一個列表裏。
  • Read View主要是用來作可見性判斷的,即判斷當前事務可見哪一個版本的數據~

爲了下面方便討論Read View可見性規則,先定義幾個變量

  • m_ids:當前系統中那些活躍的讀寫事務ID,它數據結構爲一個List。
  • min_limit_id:m_ids事務列表中,最小的事務ID
  • max_limit_id:m_ids事務列表中,最大的事務ID
  • 若是DB_TRX_ID < min_limit_id,代表生成該版本的事務在生成ReadView前已經提交(由於事務ID是遞增的),因此該版本能夠被當前事務訪問。
  • 若是DB_TRX_ID > m_ids列表中最大的事務id,代表生成該版本的事務在生成ReadView後才生成,因此該版本不能夠被當前事務訪問。
  • 若是 min_limit_id =<DB_TRX_ID<= max_limit_id,須要判斷m_ids.contains(DB_TRX_ID),若是在,則表明Read View生成時刻,這個事務還在活躍,尚未Commit,你修改的數據,當前事務也是看不見的;若是不在,則說明,你這個事務在Read View生成以前就已經Commit了,修改的結果,當前事務是能看見的。

注意啦!! RR跟RC隔離級別,最大的區別就是:RC每次讀取數據前都生成一個ReadView,而RR只在第一次讀取數據時生成一個ReadView

已提交讀(READ COMMITTED) 存在不可重複讀問題的分析歷程

我以爲理解一個新的知識點,最好的方法就是居於目前存在的問題/現象,去分析它的前因後果~ RC的實現也跟MVCC有關,RC是存在重複讀併發問題的,因此咱們來分析一波RC吧,先看一下執行流程

假設如今系統裏有A,B兩個事務在執行,事務ID分別爲100、200,而且假設存在的老數據,插入事務ID是50哈~

事務A 先執行查詢1的操做

# 事務A,Transaction ID 100
begin ;
查詢1:select *  from account WHERE id = 1;

事務 B 執行更新操做,id =1記錄的undo日誌鏈以下

begin;
update account set balance =balance+20 where id =1;


回到事務A,執行查詢2的操做

begin ;
查詢1:select *  from account WHERE id = 1; 
查詢2:select *  from account WHERE id = 1;

查詢2執行分析:

  • 事務A在執行到SELECT語句時,從新生成一個ReadView,由於事務B(200)在活躍,因此ReadView的m_ids列表內容就是[200]
  • 由上圖undo日誌鏈可得,最新版本的balance爲1000,它的事務ID爲200,在活躍事務列表裏,因此當前事務(事務A)不可見。
  • 咱們繼續找下一個版本,balance爲100這行記錄,事務Id爲50,小於活躍事務ID列表最小記錄200,因此這個版本可見,所以,查詢2的結果,就是返回balance=100這個記錄~~

咱們回到事務B,執行提交操做,這時候undo日誌鏈不變

begin;
update account set balance =balance+20 where id =1;
commit

再次回到事務A,執行查詢3的操做

begin ;
查詢1:select *  from account WHERE id = 1; 
查詢2:select *  from account WHERE id = 1; 
查詢3:select *  from account WHERE id = 1;

查詢3執行分析:

  • 事務A在執行到SELECT語句時,從新生成一個ReadView,由於事務B(200)已經提交,不載活躍,因此ReadView的m_ids列表內容就是空的了。
  • 因此事務A直接讀取最新紀錄,讀取到balance =120這個版本的數據。

因此,這就是RC存在不可重複讀問題的過程啦有不理解的地方能夠多讀幾遍哈

可重複讀(Repeatable Read)解決不可重複讀問題的一次分析

咱們再來分析一波,RR隔離級別是如何解決不可重複讀併發問題的吧~

你可能會以爲兩個併發事務的例子太簡單了,好的!咱們如今來點刺激的,開啓三個事務~


假設如今系統裏有A,B,C兩個事務在執行,事務ID分別爲100、200,300,存量數據插入的事務ID是50~

# 事務A,Transaction ID 100
begin ;
UPDATE account SET balance = 1000  WHERE id = 1;
# 事務B,Transaction ID 200
begin ; //開個事務,佔坑先

這時候,account表中,id =1記錄的undo日誌鏈以下:

# 事務C,Transaction ID 300
begin ;
//查詢1:select * from  account WHERE id = 1;

查詢1執行過程分析:

  • 事務C在執行SELECT語句時,會先生成一個ReadView。由於事務A(100)、B(200)在活躍,因此ReadView的m_ids列表內容就是[100, 200]。
  • 由上圖undo日誌鏈可得,最新版本的balance爲1000,它的事務ID爲100,在活躍事務列表裏,因此當前事務(事務C)不可見。
  • 咱們繼續找下一個版本,balance爲100這行記錄,事務Id爲50,小於活躍事務ID列表最小記錄100,因此這個版本可見,所以,查詢1的結果,就是返回balance=100這個記錄~~

接着,咱們把事務A提交一下:

# 事務A,Transaction ID 100
begin ;
UPDATE account SET balance = 1000  WHERE id = 1;
commit;

在事務B中,執行更新操做,把id=1的記錄balance修改成2000,更新完後,undo 日誌鏈以下:

# 事務B,Transaction ID 200
begin ; //開個事務,佔坑先
UPDATE account SET balance = 2000  WHERE id = 1;

回到事務C,執行查詢2

# 事務C,Transaction ID 300
begin ;
//查詢1:select * from  account WHERE id = 1;
//查詢2:select * from  account WHERE id = 1;

查詢2:執行分析:

  • 在RR 級別下,執行查詢2的時候,由於前面ReadView已經生成過了,因此直接服用以前的ReadView,活躍事務列表爲[100,200].
  • 由上圖undo日誌鏈可得,最新版本的balance爲2000,它的事務ID爲200,在活躍事務列表裏,因此當前事務(事務C)不可見。
  • 咱們繼續找下一個版本,balance爲1000這行記錄,事務Id爲100,也在活躍事務列表裏,因此當前事務(事務C)不可見。
  • 繼續找下一個版本,balance爲100這行記錄,事務Id爲50,小於活躍事務ID列表最小記錄100,因此這個版本可見,所以,查詢2的結果,也是返回balance=100這個記錄~~

鎖相關概念補充(附):

共享鎖與排他鎖

InnoDB 實現了標準的行級鎖,包括兩種:共享鎖(簡稱 s 鎖)、排它鎖(簡稱 x 鎖)。

  • 共享鎖(S鎖):容許持鎖事務讀取一行。
  • 排他鎖(X鎖):容許持鎖事務更新或者刪除一行。

若是事務 T1 持有行 r 的 s 鎖,那麼另外一個事務 T2 請求 r 的鎖時,會作以下處理:

  • T2 請求 s 鎖當即被容許,結果 T1 T2 都持有 r 行的 s 鎖
  • T2 請求 x 鎖不能被當即容許

若是 T1 持有 r 的 x 鎖,那麼 T2 請求 r 的 x、s 鎖都不能被當即容許,T2 必須等待T1釋放 x 鎖才能夠,由於X鎖與任何的鎖都不兼容。

記錄鎖(Record Locks)

  • 記錄鎖是最簡單的行鎖,僅僅鎖住一行。如:SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE
  • 記錄鎖永遠都是加在索引上的,即便一個表沒有索引,InnoDB也會隱式的建立一個索引,並使用這個索引實施記錄鎖。
  • 會阻塞其餘事務對其插入、更新、刪除

記錄鎖的事務數據(關鍵詞:lock_mode X locks rec but not gap),記錄以下:

RECORD LOCKS space id 58 page no 3 n bits 72 index `PRIMARY` of table `test`.`t` 
trx id 10078 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 4; hex 8000000a; asc     ;;
 1: len 6; hex 00000000274f; asc     'O;;
 2: len 7; hex b60000019d0110; asc        ;;

間隙鎖(Gap Locks)

  • 間隙鎖是一種加在兩個索引之間的鎖,或者加在第一個索引以前,或最後一個索引以後的間隙。
  • 使用間隙鎖鎖住的是一個區間,而不只僅是這個區間中的每一條數據。
  • 間隙鎖只阻止其餘事務插入到間隙中,他們不阻止其餘事務在同一個間隙上得到間隙鎖,因此 gap x lock 和 gap s lock 有相同的做用。

Next-Key Locks

  • Next-key鎖是記錄鎖和間隙鎖的組合,它指的是加在某條記錄以及這條記錄前面間隙上的鎖。

RC級別存在幻讀分析

由於RC是存在幻讀問題的,因此咱們先切到RC隔離級別,分析一波~

假設account表有4條數據。

  • 開啓事務A,執行當前讀,查詢id>2的全部記錄。
  • 再開啓事務B,插入id=5的一條數據。
  • 事務B插入數據成功後,再修改id=3的記錄
  • 回到事務A,再次執行id>2的當前讀查詢

  • 事務B能夠插入id=5的數據,卻更新不了id=3的數據,陷入阻塞。證實事務A在執行當前讀的時候在id =3和id=4這兩條記錄上加了鎖,可是並無對 id > 2 這個範圍加鎖~
  • 事務B陷入阻塞後,切回事務A執行當前讀操做時,死鎖出現。由於事務B在 insert 的時候,會在新紀錄(id=5)上加鎖,因此事務A再次執行當前讀,想獲取id> 3 的記錄,就須要在 id=3,4,5 這3條記錄上加鎖,可是 id = 5這條記錄已經被事務B 鎖住了,因而事務A被事務B阻塞,同時事務B還在等待 事務A釋放 id = 3上的鎖,最終產生了死鎖。

所以,咱們能夠發現,RC隔離級別下,加鎖的select, update, delete等語句,使用的是記錄鎖,其餘事務的插入依然能夠執行,所以會存在幻讀~

RR 級別解決幻讀分析

由於RR是解決幻讀問題的,怎麼解決的呢,分析一波吧~

假設account表有4條數據,RR級別。

  • 開啓事務A,執行當前讀,查詢id>2的全部記錄。
  • 再開啓事務B,插入id=5的一條數據。

    能夠發現,事務B執行插入操做時,阻塞了~由於事務A在執行select ... lock in share mode的時候,不只在 id = 3,4 這2條記錄上加了鎖,並且在id > 2 這個範圍上也加了間隙鎖。

所以,咱們能夠發現,RR隔離級別下,加鎖的select, update, delete等語句,會使用間隙鎖+ 臨鍵鎖,鎖住索引記錄之間的範圍,避免範圍間插入記錄,以免產生幻影行記錄。

參考與感謝

我的公衆號

  • 以爲寫得好的小夥伴給個點贊+關注啦,謝謝~
  • 若是有寫得不正確的地方,麻煩指出,感激涕零。
  • 同時很是期待小夥伴們可以關注我公衆號,後面慢慢推出更好的乾貨~嘻嘻
相關文章
相關標籤/搜索