深入理解 MySQL 中的事務隔離級別

一、事務是什麼

先來看第一個問題:什麼是事務(Transaction)?

事務就是執行一組 SQL 語句。這些 SQL 語句就是一條繩上的螞蚱,要麼一起成功(Commit),要麼一起失敗(RollBack)。

  • 比如用戶 A 向用戶 B 轉賬 10 元;
  • 如果 A 先轉出了 10 元,但是在 B 收到之前服務器掛了,就會造成 A 的錢沒了,B 的錢也沒到賬;
  • 如果把這兩條語句封裝成事務的話,就不會出現上述情況,而是整個過程執行失敗,退回 A 的錢結束事務。

事務的特性:

  • Atomicity(原子性):像原子一樣不可分割;
  • Consistency(一致性):數據庫永遠保持一致;
  • Isolation(隔離性):事務之間是相互隔離的;
  • Durability(持久性):事務一旦提交,它的修改就是永久的。

這就是 ACID

下面來看一個栗子:

-- 開啓一個事務
start transaction;

insert into orders(customer_id, order_date, status)
values (1, '2020-05-10', 1);

insert into order_items
values (last_insert_id(), 1, 1, 1);

-- 提交就代表結束事務了
commit;

-- 回滾事務
rollback;

其中被transactioncommit包裹着的語句就是事務,也可以使用 rollback包裹,這樣的話之前的修改就全部失效。

MYSQL 中,一條 SQL 語句默認就是一個事務:

# 每一個 SQL 語句都被設置成自動提交
show variables like 'autocommit';

二、爲什麼使用隔離級別

然後是第二個問題:爲什麼會有事務的隔離級別?

既然已經有了事務,轉賬啥的問題也都挺好的解決了,那隔離級別優勢啥玩意?

什麼是併發

在瞭解這個之前我們先來介紹一下什麼是併發:

  • 併發就是多個用戶訪問數據庫,同時對相同的數據進行修改帶來的問題。
  • 比如用戶 A 創建了事務,還沒來得及提交,用戶 B 搶先提交了,關鍵是他們修改的是同一個事務,那麼到底應該聽誰的呢?

1、默認併發處理

那麼 MySQL 默認是怎麼處理併發的呢?

  • MySQL 默認會讓其中一個事務等待另一個事務先執行完成再執行該事務。
  • 他會先將數據鎖定,就是給行或者數據加鎖,確保在第一個事務執行完成之前,數據不會被其他事務修改。
  • 所以在默認的情況下是不用擔心事務的併發問題的。

2、常見併發問題

但是默認並不是萬能的,總有一些問題是它解決不來的,接下來我們就來看一下這些問題。

Lost Updates

第一個問題是丟失更新

什麼時候會出現?

兩個事物試圖更新相同的數據而我們不使用鎖時,就會出現這種情況。

造成的結果:後面提交的數據會覆蓋掉前面的提交。

但是這種情況 MySQL 會默認處理的,即放在隊列中依次執行,所以一般我們不用考慮。

Dirty Reads

第二個問題就是髒讀

什麼時候會出現?

A 事務讀取還沒有提交時:另一個事務 B 卻將 A 的還沒有提交的數據當成了真實存在的數據,如果 A 最後提交了還好,如果 A 回滾了,那麼 B 中的數據就是假的,也就是髒的,所以叫髒讀。

造成的結果:讀取了還未提交的數據,數據是髒的。

Non-repeating Reads

第三個問題是不可重複讀

什麼時候會出現?

事務 A 執行子查詢,在外層查詢中先獲取指定的值,這個時候事務 B 過來修改了 A 剛纔讀的數據,但是之後 A 的子查詢再次讀了這個數據而這個數據兩次讀到的值不一樣了,這就是不可重複讀。

造成的結果:兩次讀取的內容可能會不同。

Phantom Reads

第四個問題是幻讀

什麼時候會出現?

在事務 A 中使用 where 條件查詢,此時事務 B 修改了原來符合條件的數據,使得它現在不符合條件了,或者說增加了幾個符合條件的數據,但是事務 A 始終讀取的都是原來的數據。

造成的結果:丟失符合條件的某些行,就很玄幻,所以是幻讀。

三、怎麼做才能解決併發問題

那怎麼解決這些併發問題呢? 答案是使用隔離級別對事務進行隔離。

該圖左邊是事務的隔離等級,右邊是這個隔離等級可以解決的問題。標準的 SQL 定義了四個隔離級別,他們分別是:

  • Read Uncommitted,不提交讀:他什麼問題也解決不了;
  • Read Committed,提交讀:可以解決髒讀,隔離級別是一次讀取的範圍,所以無法解決一次事務兩次讀取中間可能出現的問題;
  • Repeatable Read,可重複讀:可以解決前三個問題,可以解決大部分的問題 ;
  • Serializable,序列化:可以解決所有問題,他會等待其他事務執行完畢之後再執行。

有一些注意點:

  • MySQL 默認是可重複讀的隔離等級,之前我們也提到了這個默認情況。
  • 隔離界別越高,性能損耗越大,所以要權衡利弊選擇隔離級別。

    好,我們現在來回顧總結一下:
# 多個用戶同時修改同一個數據稱爲併發, MYSQL 會自動鎖定 update 的內容

# 事務帶來的常見的問題
/*
    - 1.丟失更新: 兩個事物同時修改數據,後面提交的數據會覆蓋掉前面的提交
    - 2.髒讀(無效數據讀取): 讀取到未提交的數據
        A事務讀取還沒有提交時-另一個事務B卻將A的還沒有提交的數據當成了真實存在的,如果A最後提交了還好,如果A回滾了,那麼B中的數據就是假的;
        即讀取了還未提交的數據,數據是髒的;
        -- 爲了解決這個問題,我們可以提供一定的隔離度,即事務修改的數據不會立即被其他事物看到,除非他已經提交了
        -- 標準的 SQL 定義了四個隔離級別 - 針對髒讀的就是 提交讀 (Read Committed) 只能讀取已經提交的數據
    - 3.不可重複讀取: 兩次讀取的內容可能會不同
        事務A執行子查詢,在外層查詢中先獲取指定的值,這個時候事務B過來修改了A剛纔讀的數據,但是之後A的子查詢再次讀了這個數據
        而這個數據兩次讀到的值不一樣了.這就是不可重複讀
        -- 這個時候應該增加隔離級別: 可重複讀,達到這個目的 -> 以最開始的那個數據爲準,不受事務B修改的影響,隔離級別是事務的級別.
    - 4.幻讀: 丟失符合條件的某些行
        在事務A中使用 where 查詢,此時事務 B 修改了原來符合條件的數據,使得它現在不符合條件了.
        需要的隔離等級: 序列化 Serializable 等級,使該事務可以知道其他事物正在修改數據.
        這樣他就會等待其他事務修改完之後纔會執行事務,缺點就是如果併發比較多,執行會很慢

    等級越高,性能消耗越大;
    MySQL默認是可重複讀的隔離等級;
*/

大部分情況下都保持默認,只在特殊情況下修改隔離等級。

如何設置隔離等級?

-- 查看隔離等級
show variables like 'transaction_isolation';

-- 設置隔離等級
set transaction isolation level SERIALIZABLE ;

-- 在當前會話中設置
set session transaction isolation level serializable ;

四、MySQL 中的鎖

數據庫遵循的是兩段鎖協議,將事務分爲兩個階段:

  • 加鎖階段;
    • 加鎖階段只允許事務加鎖,在對任何事務進行 讀操作 之前一定要先申請並獲得 S鎖(共享鎖) ,進行 寫操作 之前要申請並獲得 X鎖(排它鎖),如果獲得失敗,即加鎖不成功則必須等待,直到加鎖成功才繼續執行。
  • 解鎖階段;
    • 當事務釋放了一個封鎖之後,事務進入解鎖階段,該階段只能進行解鎖操作而不能進行加鎖操作。

舉一個栗子:

MySQL 中的鎖分爲行鎖表鎖,表鎖通常是將整個的一張表鎖住,會降低併發處理能力,所以一般只在 DDL(數據庫定義語言) 中使用。這裏主要討論行鎖:

1. Read Committed

RC 級別中,數據的查詢是不需要加鎖的,但是數據的增刪改是需要加鎖的。

如果事務一直得不到鎖則會一直等待,直到 wait 超時。

如果條件語句沒有索引,MySQL 會將該表中的所有數據行加行鎖。

但是在實際的應用中,會在 MySQL Server 中進行過濾,調用unlock_row 方法,從而將無關的數據項解鎖(但是這違背了兩段鎖協議)。

對一個數據量很大的表做批量修改的時候,如果無法使用相應的索引,MySQL Server 過濾數據的的時候特別慢,就會出現雖然沒有修改某些行的數據,但是它們還是被鎖住了的現象。

2. Repeatable Read

RR 解決了在一次事務中,兩次讀取的內容不一致。

那麼他是怎麼做得到呢?

2.1 不可重複讀和幻讀的區別

這裏穿插出一個知識點,也是大家容易記混的地方。

  • 不可重複讀重點在於 updatedelete
  • 而幻讀的重點在於 insert

幻讀與不可重複讀類似,幻讀是查詢到了另一個事務已提交的新插入數據,而不可重複讀是查詢到了另一個事務已提交的更新數據。

簡單來說,不可重複讀是由於數據修改引起的,幻讀是由數據插入或者刪除引起的。

在可重複讀中,該 sql 第一次讀取到數據後,就將這些數據加鎖,其它事務無法修改這些數據,就可以實現可重複讀了。

但這種方法卻無法鎖住 insert 的數據,所以當事務 A 先前讀取了數據,或者修改了全部數據,事務 B 還是可以 insert 數據提交,這時事務 A 就會發現莫名其妙多了一條之前沒有的數據,這就是幻讀,不能通過行鎖來避免。

需要 Serializable 隔離級別 ,讀用讀鎖,寫用寫鎖,讀鎖和寫鎖互斥,這麼做可以有效的避免幻讀、不可重複讀、髒讀等問題,但會極大的降低數據庫的併發能力。

但是 MySQL 使用了以樂觀鎖爲理論基礎的MVCC(多版本併發控制)來避免這兩種問題。

2.2 悲觀鎖和樂觀鎖

悲觀鎖 指的是對於數據被外界修改持保守態度,在整個數據處理過程中,將數據處於鎖定狀態,它依賴於數據庫底層提供的鎖機制,讀取數據時加鎖,其它事務無法修改這些數據。修改刪除數據時也要加鎖,其它事務無法讀取這些數據。Serializable 就是使用的悲觀鎖。

樂觀鎖 採取更加寬鬆的策略,相對於悲觀鎖的高性能開銷樂觀鎖性能很好。大多是基於數據版本記錄機制實現的。一般就是在數據表中增加一個字段 version 字段,讀取數據時將此版本號一併讀出,之後每更新一次,版本號加一。然後將提交的數據版本信息與數據表記錄的那個版本信息進行比對,如果大於等於則更新原來的版本號,否則認爲是過期數據。

很多文章都說 RR 級別是可重複讀的,但無法解決幻讀,而只有在 Serializable 級別才能解決幻讀。但是其實 RR 級別是可以解決幻讀的讀問題的。這得益於樂觀鎖的實現,即 MVCC 。

2.3 “讀”與“讀”的區別

MySQL中的讀,和事務隔離級別中的讀,是不一樣的。

有的時候我們讀取到的數據是歷史數據,是不及時的,這種我們叫它 快照讀 ,而讀取數據庫當前版本數據的方式叫做 當前讀

  • 快照讀:就是 select * from table ….
  • 當前讀:特殊的讀操作,插入/更新/刪除操作,屬於當前讀,處理的都是當前的數據,需要加鎖。

事務的隔離級別實際上就是定義了當前讀的級別,MySQL 爲了減少鎖處理(包括等待其它鎖)的時間,提升併發能力,引入了快照讀的概念,使得select不用加鎖。

2.4 Next-Key 鎖

MySQL 使用了 Next-Key 鎖來解決當前讀中的幻讀問題(也就是寫)。

Next-Key 鎖是行鎖GAP(間隙鎖)的合併。

那麼 GAP 鎖又是什麼呢?

RR 級別中,事務 A 在 update 後加鎖,事務 B 無法插入新數據,這樣事務 A 在 update 前後讀的數據保持一致,就避免了幻讀。這個鎖,就是Gap鎖。

行鎖防止別的事務修改或刪除,GAP 鎖防止別的事務新增,行鎖GAP鎖 結合形成的的 Next-Key 鎖共同解決了 RR 級別在寫數據時的幻讀問題。

3. Serializable

這個級別很簡單,讀加共享鎖,寫加排他鎖,讀寫互斥。使用的悲觀鎖的理論,實現簡單,數據更加安全,但是併發能力非常差。如果你的業務併發的特別少或者沒有併發,同時又要求數據及時可靠的話,可以使用這種模式。

參考資料:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章