1. 程式人生 > 其它 >資料庫基礎知識詳解一:事務、併發一致性問題與隔離級別

資料庫基礎知識詳解一:事務、併發一致性問題與隔離級別

寫在文章前:本系列文章用於博主自己歸納複習一些基礎知識,同時也分享給可能需要的人,因為水平有限,肯定存在諸多不足以及技術性錯誤,請大佬們及時指正。

1.事務

1.1、事務的定義與特性

事務(Transaction)是一個操作序列,邏輯上不可分割的工作單位,以BEGIN TRANSACTION開始,以ROLLBACK/COMMIT結束。

四大特性(即常說的ACID特性):

  • 原子性(Atomicity):一個事務是一個邏輯上不可分割的工作單位,事務中包括的操作要麼都做,要麼都不做。即事務的所有操作要麼全部提交成功,要麼全部失敗回滾。
  • 一致性(Consistency):事務的執行必須使資料庫保持一致性狀態,即使資料庫從一個一致性狀態變到另一個一致性狀態。在一致性狀態下,所有事務對一個數據的讀取結果都是相同的。
  • 隔離性(Isolation):一個事務的執行不能被其他事務干擾。即一個事務內部的操作及使用的資料對併發的其他事務是隔離的,併發執行的各個事務之間不能互相干擾。也就是說一個事務所做的修改在最終提交以前,對其它事務是不可見的。
  • 永續性(Durability):一旦事務提交成功,它對資料庫中資料的修改是永久性的。

1.2、事務特性的實現原理

因為MySql的引擎中只有InnoDB支援事務,所以這裡的實現都講述的是InnoDB實現事務特性的原理。

首先介紹一下Innodb中的兩個事務日誌:redo log和undo log。redo log是重做日誌,提供前滾操作,undo log是回滾日誌,提供回滾操作。undo log不是redo log的逆向過程,它們都算是用來恢復的日誌。

  • redo log通常是物理日誌,記錄的是資料頁的物理修改,而不是某一行或某幾行修改成怎樣怎樣,它用來恢復提交後的物理資料頁(恢復資料頁,且只能恢復到最後一次提交的位置)。(用於實現永續性)它包括兩部分,一是記憶體中的日誌緩衝(redo log buffer),該部分日誌是易失性的;二是磁碟上的重做日誌檔案(redo log file),該部分日誌是持久的。
  • undo log用來回滾行記錄到某個版本。undo log一般是邏輯日誌,根據每行記錄進行記錄。(用於實現原子性和隔離性)除它之外還有binlog(二進位制日誌)也記錄了很多innodb表的操作,也能實現重做的功能,但是他們之間有較大區別,這個後續再講。

簡單介紹一下對資料庫的操作步驟:
因為資料庫資訊儲存在磁碟當中,所以並不能直接操作它們。需要先載入資料庫資訊的快取到記憶體中,再進行操作,再寫回磁碟的檔案當中。

永續性的實現:事務每次成功提交,代表該次操作已經寫入了資料庫資訊的快取中,但該快取並不會馬上寫到磁碟當中。所以我們人為規定必須還要將此次對快取的操作寫到磁碟中的redo log中去,才算該事務提交成功,那麼即使發生了斷電/系統崩潰/其它的軟硬體錯誤等情況,導致資料沒有被完整寫入到磁碟的資料庫資訊中,也能根據redo log檔案將未完成的操作進行重做,以保證永續性。

問題:為什麼不直接每次都必須把資料庫資訊寫回磁碟才算事務提交成功,這樣不就不用redo log了?
理由:因為寫入到redo log只需寫入一條記錄並且是順序I/O(追加到檔案末尾),而對資料庫資訊的修改是隨機I/O(大概率涉及多個表的修改,而它們並不一定是相鄰的),每個事務提交前都要等待一次後者的時間是無法接受的,所以選用速度很快的寫入redo log。

原子性的實現:和前面永續性的實現有些類似,每次操作資料庫之前也都會把操作記錄到undo log中,如果發生了斷電/系統崩潰/其它的軟硬體錯誤等情況,導致有一些事務沒有提交但是已經寫到了快取當中,就可以通過undo log中的記錄進行操作的回滾,以達到原子性。

隔離性的實現:這裡是需要分步來說,首先是未提交讀(Read Uncommited)級別,該隔離級別不需要其他操作就能達到。然後在MySQL中,使用MVCC來實現提交讀(Read Commited)和可重複讀(Repeatable Read)這兩個隔離級別,使用MVCC+Next-Key Lock來解決幻讀的問題。

MVCC是什麼?
簡單來說就是在資料表每行中設有隱藏的列,一列是上一次更新過該行資料的事務id,一列是一個指標,指向本資料上一個版本的undo log。(記錄了值)

ReadView機制
就是當你執行一個事務的時候,會生成幾個數值。
trx_ids:一個代表此時哪些事務在MySQL中還未提交;
creator_trx_id:一個代表建立本ReadView事務的id;
low_limit_id:一個代表此時未提交的事務中的最小id;
up_limit_id:一個代表MySQL下個生成的事務的id。

  • 實現提交讀:每次讀取資料時,使用ReadView機制,如果該資料最近更新的事務id在未提交事務中,那就根據表中隱藏的指標找到上一次提交了的事務對應的資料並讀取。
  • 實現可重複讀:每次select的時候檢查所讀資料最近更新的事務id號,如果在本次事務之後,那麼不會讀取,會一直沿指標追溯到更新事務id號小於本事務id號的資料再讀取。
  • 解決幻讀:使用MVCC+Next-Key Lock。

InnoDB有三種行鎖的演算法:

(1)Record Lock:行鎖,即單個行記錄上的鎖。

(2)Gap Lock:間隙鎖,鎖定一個範圍,但不包括記錄本身。GAP鎖的目的,是為了防止同一事務的兩次當前讀,出現幻讀的情況。

(3)Next-Key Lock:行鎖加兩邊的間隙鎖,鎖定一個範圍,並且鎖定記錄本身。對於行的查詢,都是採用該方法,主要目的是解決幻讀的問題。(前開後閉的區間)

以我個人的理解來看,保證了原子性、永續性與隔離性,就做到了一致性

2.常出現的併發一致性問題

  • 丟失修改:一個事務對資料進行了修改,在事務提交之前,另一個事務對同一個資料進行了修改,覆蓋了之前的修改。

    例如:事務A把變數a的值從5修改到10,還未提交時另一個事務B又把變數a的值修改為15併成功提交,然後事務A提交,最後變數a的值還是10。相當丟失了事務B的這次修改。

  • 髒讀:一個事務讀取了被另一個事務修改、但未提交(有可能進行了回滾)的資料,造成兩個事務得到的資料不一致。

    例如:事務A修改變數a的值為10,但未提交。此時事務B讀取變數a的值為10,然後進行操作。但是事務A之後進行了回滾,變數a的值變回了5。相當於事務B讀取到變數錯誤的值,即讀取了髒資料。

  • 不可重複讀:在同一個事務中,查詢操作在某個時間讀取某一行資料和之後一個時間讀取該行資料,發現數據已經發生修改(針對的update操作)。

    例如:計算變數a+變數a,前面那個a讀取的值是50,後面那個讀取到的值是100。

  • 幻讀:當同一查詢多次執行時,由於其它事務在這個資料範圍內執行了插入操作,會導致每次返回不同的結果集。(和不可重複讀的區別:幻讀針對一個數據整體/範圍,並且針對的是insert操作)。

    例如:多次查詢score>60的學生,第一次查詢到三個學生,第二次查詢到五個學生。

3.資料庫的隔離級別

  • 未提交讀(Read Uncommited):在一個事務提交之前,它的執行結果對其它事務也是可見的。會導致髒讀、不可重複讀、幻讀。(比如某程式更新資料,但是並沒有提交,別的程式就可以讀取到它。)

    該級別下,select語句不加鎖,雖然併發性最高,但是隔離性最差,所以該隔離級別一般不會被使用。

  • 提交讀(Read Commited):一個事務只能看見已經提交的事務所作的改變。可避免髒讀問題。

  • 可重複讀(Repeatable Read):可以確保同一個事務在多次讀取同樣的資料時得到相同的結果。可以理解為事務在讀取資料的時候獲取了一次當前時刻資料的快照,後續的讀取都以快照為參照,所以不受其餘事務的影響。但是該隔離級別可能會導致幻讀,後續在講解MVCC的時候會說明不能避免幻讀的理由。

    MySql預設隔離級別。可避免髒讀 、不可重複讀的發生。

  • 可序列化(Serializable):強制事務序列執行,使之不可能相互衝突,從而解決幻讀問題。每次讀都需要獲得表級共享鎖,讀寫相互都會阻塞。

    雖然可以避免髒讀、不可重複讀、幻讀的發生。但是因為可能導致大量的超時現象和鎖競爭,實際很少使用。