MySQL(一):MySQL資料庫事務與鎖

方塊人發表於2020-12-12

基本概念

事務是指滿足ACID特性的的一組操作,可以通過Commit提交事務,也可以也可以通過Rollback進行回滾。會存在中間態和一致性狀態(也是真正在資料庫表中存在的狀態)

ACID

  • Atomicity【原子性】:事務被視為不可分割的最小單元,事務的所有操作要麼全部提交成功,要麼全部失敗回滾。回滾可以用回滾日誌(undo Log)來實現,回滾日誌記錄著事務所執行的修改操作,在回滾時反向執行這些修改操作即可

undoLog:為了滿足事務的原子性,在操作任何資料之前,首先將資料備份到Undo Log,然後進行資料的修改。如果出現了錯誤或者使用者執行了ROLLBACK語句,系統可以利用Undo Log中的備份將資料恢復到事務開始之前的狀態。與redo log不同的是,磁碟上不存在單獨的undo log檔案,它存放在資料庫內部的一個特殊段(segment)中,這稱為undo段(undo segment),undo段位於共享表空間內。Innodb為每行記錄都實現了三個隱藏欄位:6位元組的事務ID(DB_TRX_ID)7位元組的回滾指標(DB_ROLL_PTR)隱藏的ID

  • Consistency【一致性】:資料庫在事務執行前後都保持一致性狀態,在一致性狀態下,所有事務對同一個資料的讀取結果都是相同的
  • Isolation【隔離性】:一個事務所做的修改在最終提交前,對其他事務是不可見的
  • Durability【永續性】:一旦事務提交,則其所做的修改將會永遠儲存到資料庫中,即使系統發生崩潰,事務執行的結果也不能丟失。系統發生崩潰可以用redoLog進行恢復,從而實現永續性。與undoLog記錄資料的邏輯修改不同,redoLog記錄的是資料頁的物理修改
  • 小結:
    1. 只有滿足一致性,事務的執行結果才是正確的。
    2. 在無併發的情況下,事務序列執行,隔離性一定能夠滿足。此時只要能夠滿足原子性,就一定能滿足一致性。
    3. 在併發情況下,多個事務並行執行,事務不僅要滿足原子性,還需要滿足隔離性,才能滿足一致性
    4. 事務滿足持久化是為了能夠應對系統崩潰的情況

AutoCommit

  • MySQL預設採用自動提交模式,也就是說,如果不顯示使用start transaction語句來開始一個事務,那麼每個操作都會被當做一個事務並自動提交

事務隔離級別

  • 未提交讀【read uncommitted】:事務中的修改,即使沒有提交,對其他事務也是可見的
  • 提交讀【read committed】:一個事務只能讀取已經提交的事務所做的修改,換句話說,一個事務所做的修改在提交之前對其他事務是不可見的
  • 可重複讀【repeatable read】:保證在同一個事務中多次讀取同一資料的結果是一樣的
  • 可序列化【serializable】:強制事務序列執行,這樣多個事務互不干擾,不會出現併發一致性問題,該隔離級別需要加鎖實現,因為要使用加鎖機制保證同一時間只有一個事務執行,也就是保證事務序列執行

併發一致性問題

背景

在併發環境下,事務的隔離性很難保證,因此會出現很多併發一致性問題

主要場景

  • 丟失修改:丟失修改指一個事務更新操作被另外一個事務的更新操作替換。例如:T1 和 T2 兩個事務都對一個資料進行修改,T1 先修改並提交生效,T2 隨後修改,T2 的修改覆蓋了 T1 的修改。
    業務場景:使用者修改地址有修改地址資訊和設定預設地址或者刪除地址,這三個場景都是呼叫的同一個update語句。提供給使用者更新地址的介面需要支援使用者可設定預設地址,而不能將更新地址資訊和設定預設地址分開提供介面,如果分開提供,上層服務呼叫實際上是一下子呼叫兩個更新介面,這樣很容易會出現丟失修改的場景。
  • 讀髒資料:讀髒資料指在不同的事務下,當前事務可以讀取到另外事務未提交的資料,例如:T1 修改一個資料但未提交,T2 隨後讀取這個資料。如果 T1 撤銷了這次修改,那麼 T2 讀取的資料是髒資料。
  • 不可重複讀:不可重複讀指在一個事務內多次讀取同一資料集合,在這一事務還未結束前,另一個事務也訪問了該同一資料集合並做了修改,由於第二個事務的修改,第一次事務的兩次讀取的資料可能不一致。例如:T2 讀取一個資料,T1 對該資料做了修改。如果 T2 再次讀取這個資料,此時讀取的結果和第一次讀取的結果不同。
  • 幻影讀:幻讀本質上也屬於不可重複讀的情況,T1讀取某個範圍的資料,T2在這個範圍內插入新的資料,T1再次讀取這個範圍的資料,此時讀取的結果和第一次讀取的結果不同
  • 小結
    產生併發不一致性問題的主要原因是破壞了事務的隔離性,解決方法是通過併發控制來保證隔離性。併發控制可以通過封鎖來實現,但是封鎖操作需要使用者自己控制,相當複雜。資料庫管理系統提供了事務的隔離級別,讓使用者以一種更輕鬆的方式處理併發一致性問題。

封鎖粒度:

  • 行級鎖:只封鎖需要修改的那部分資料或那行,不是封鎖所有資源,發生鎖爭用的可能小,系統併發程度高
  • 表級鎖:封鎖整個表,鎖定資料量太大,發生鎖爭用的概率大大加大,系統併發效能直線下滑

注意:加鎖就會消耗資源,鎖的各種操作【包括獲取鎖,釋放鎖,檢查鎖狀態都會增加系統開銷,因此封鎖的粒度越小,系統開銷越大】,在選擇封鎖粒度時,需要在鎖開銷和併發程度之間做一個權衡

封鎖型別

讀寫鎖

  • 互斥鎖,簡寫為X鎖,又稱寫鎖。
    一個事務對資料物件 A 加了 X 鎖,就可以對 A 進行讀取和更新。加鎖期間其它事務不能對 A 加任何鎖。
  • 共享鎖,簡寫為S鎖,又稱讀鎖。
    一個事務對資料物件 A 加了 S 鎖,可以對 A 進行讀取操作,但是不能進行更新操作。加鎖期間其它事務能對 A 加 S 鎖,但是不能加 X 鎖。

意向鎖

主要是表鎖,但是不會真的鎖

  • 在存在行級鎖和表級鎖的情況下,事務 T 想要對錶 A 加 X 鎖,就需要先檢測是否有其它事務對錶 A 或者表 A 中的任意一行加了鎖,那麼就需要對錶 A 的每一行都檢測一次,這是非常耗時的。
    意向鎖在原來的 X/S 鎖之上引入了 IX/IS,IX/IS 都是表鎖,用來表示一個事務想要在表中的某個資料行上加 X 鎖或 S 鎖。
    有以下兩個規定:
    一個事務在獲得某個資料行物件的 S 鎖之前,必須先獲得表的 IS 鎖或者更強的鎖;
    一個事務在獲得某個資料行物件的 X 鎖之前,必須先獲得表的 IX 鎖。

    通過引入意向鎖,事務 T 想要對錶 A 加 X 鎖,只需要先檢測是否有其它事務對錶 A 加了 X/IX/S/IS 鎖,如果加了就表示有其它事務正在使用這個表或者表中某一行的鎖,因此事務 T 加 X 鎖失敗。
  • 任意 IS/IX 鎖之間都是相容的,因為它們只表示想要對錶加鎖,而不是真正加鎖
    這裡相容關係針對的是表級鎖,而表級的 IX 鎖和行級的 X 鎖相容,兩個事務可以對兩個資料行加 X 鎖。
    (事務 T1 想要對資料行 R1 加 X 鎖,事務 T2 想要對同一個表的資料行 R2 加 X 鎖,兩個事務都需要對該表加 IX 鎖,但是 IX 鎖是相容的,並且 IX 鎖與行級的 X 鎖也是相容的,因此兩個事務都能加鎖成功,對同一個表中的兩個資料行做修改。)

MySQL隱式與顯示鎖定

  • 隱式鎖定:MySQL 的 InnoDB 儲存引擎採用兩段鎖協議,會根據隔離級別在需要的時候自動加鎖,並且所有的鎖都是在同一時刻被釋放,這被稱為隱式鎖定

兩段鎖協議:加鎖和解鎖分為兩個階段進行,可序列化排程是指通過併發控制,使得併發執行的事務結果與某個序列執行的事務結果相同,序列執行的事務互不干擾,不會出現併發一致性問題

  • 或者使用特定語句進行顯示鎖定SELECT ... LOCK In SHARE MODE;(共享鎖)SELECT ... FOR UPDATE;(排他鎖)事務完成提交自動釋放鎖

MySQL三級封鎖協議

  • 一級封鎖協議:事務T要修改資料A時必須加X鎖,知道事務T結束才釋放鎖可以解決丟失修改問題。這時候不能同時有兩個事務對同一個資料進行修改,那麼事務的修改就不會被覆蓋
  • 二級封鎖協議:在一級基礎上,要求讀取資料A時必須加S鎖,讀取完馬上釋放S鎖可以解決讀髒資料問題。因為如果有一個事務在對資料A進行修改,根據1級封鎖協議,會加X鎖,那麼就不能再加S鎖了,也就是不會讀入髒資料
  • 三級封鎖協議:在二級基礎上,要求讀取資料時必須加S鎖,直到事務結束了才能釋放S鎖可以解決不可重複讀的問題,因為讀A時,其他事務不能對A加X鎖,從而避免了在讀期間資料發生改變

InnoDB引擎的鎖實現

MVCC

  • 多版本併發控制是MySQL的innoDB儲存引擎實現隔離級別的一種具體方式,可用於實現提交讀和可重複讀這兩種隔離級別,而未提交讀隔離級別總是讀取最新的資料行,要求很低,無需使用MVCC
  • 在封鎖一節中提到,加鎖能解決多個事務同時執行時出現的併發一致性問題。在實際場景中讀操作往往多於寫操作,因此又引入了讀寫鎖來避免不必要的加鎖操作,例如讀和讀沒有互斥關係。讀寫鎖中讀和寫操作仍然是互斥的,而 MVCC 利用了多版本的思想,寫操作更新最新的版本快照,而讀操作去讀舊版本快照,沒有互斥關係,這一點和 CopyOnWrite 類似
  • 在 MVCC 中事務的修改操作(DELETE、INSERT、UPDATE)會為資料行新增一個版本快照。髒讀和不可重複讀最根本的原因是事務讀取到其它事務未提交的修改。在事務進行讀取操作時,為了解決髒讀和不可重複讀問題,MVCC 規定只能讀取已經提交的快照。當然一個事務可以讀取自身未提交的快照,這不算是髒讀
  • 系統版本號 SYS_ID:是一個遞增的數字,每開始一個新的事務,系統版本號就會自動遞增。
  • 事務版本號 TRX_ID :事務開始時的系統版本號。
  • MVCC的多版本指的是多個版本的快照,快照儲存在Undo日誌中,該日誌通過回滾指標ROLL_PTR把一個資料行的所有快照連線起來
  • INSERT、UPDATE、DELETE 操作會建立一個日誌,並將事務版本號 TRX_ID 寫入。DELETE 可以看成是一個特殊的 UPDATE,還會額外將 DEL 欄位設定為 1

ReadView

  • MVCC維護了一個ReadView結構,主要包含了當前系統未提交的事務列表,還有該列表的最小值和最大值
  • 在進行 SELECT 操作時,根據資料行快照的 TRX_ID 與 TRX_ID_MIN 和 TRX_ID_MAX 之間的關係,從而判斷資料行快照是否可以使用。
    • TRX_ID < TRX_ID_MIN,表示該資料行快照時在當前所有未提交事務之前進行更改的,因此可以使用。
    • TRX_ID > TRX_ID_MAX,表示該資料行快照是在事務啟動之後被更改的,因此不可使用。
    • TRX_ID_MIN <= TRX_ID <= TRX_ID_MAX,需要根據隔離級別再進行判斷
      • 提交讀:如果 TRX_ID 在 TRX_IDs 列表中,表示該資料行快照對應的事務還未提交,則該快照不可使用。否則表示已經提交,可以使用。
      • 可重複讀:都不可以使用。因為如果可以使用的話,那麼其它事務也可以讀到這個資料行快照並進行修改,那麼當前事務再去讀這個資料行得到的值就會發生改變,也就是出現了不可重複讀問題。在資料行快照不可使用的情況下,需要沿著 Undo Log 的回滾指標 ROLL_PTR 找到下一個快照,再進行上面的判斷。

快照讀和安全讀

  • 快照讀:MVCC的select操作是快照中的資料,不需要進行加鎖操作
  • 當前讀:MVCC其它會對資料庫進行修改的操作就需要進行加鎖操作,從而讀取最新的資料,可以看到MVCC並不是完全不用加鎖,而只是避免了select的加鎖操作

如果需要select進行加鎖,就可以強制指定加鎖操作,如之前提到的共享鎖和排他鎖

Next-Key Locks

  • 概念:Next-Key Locks 是 MySQL 的 InnoDB 儲存引擎的一種鎖實現。MVCC 不能解決幻影讀問題,Next-Key Locks 就是為了解決這個問題而存在的。在可重複讀(REPEATABLE READ)隔離級別下,使用 MVCC + Next-Key Locks 可以解決幻讀問題。
  • Record Locks:鎖定一個記錄上的索引,而不是記錄本身,如果表沒有設定索引,InnoDB會自動在主鍵上建立隱藏的聚簇索引
  • Gap Locks:鎖定索引之間的間隙,但是不包含索引本身。例如當一個事務執行以下語句SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;
  • Next-Key Locks:它是 Record Locks 和 Gap Locks 的結合,不僅鎖定一個記錄上的索引,也鎖定索引之間的間隙。它鎖定一個前開後閉區間,例如一個索引包含以下值:10, 11, 13, and 20,那麼就需要鎖定以下區間:(-∞, 10](10, 11](11, 13](13, 20](20, +∞)

總結

上述理論較多,但是也是這些理論支撐整個研發過程,遇到多種業務場景時,需要根據資料庫的隔離級別判斷事務會不會出現死鎖,資料不一致等等理論性問題。MySQL最厲害的地方也是在RR【可重複讀】級別下避免了幻讀,即兼顧效能又保證資料安全。
在開發微服務或者分佈使專案時。儘量將事務寫的簡單些,讓事務不會長時間鎖住對應的行。這樣也是保證了資料的一致性

相關文章