【進階之路】詳解資料庫事物與隔離級別

南橘ryc發表於2021-03-29

這段時間忙於上線和重構、寫文章的是減少了很多,更新不得不變得遲緩起來~

一、事務的特性

事務是指作為單個邏輯工作單元執行的一系列操作,要麼都執行成功,要麼都執行失敗。資料庫事物有四種特徵:即原子性、一致性、隔離性和永續性,也就是我們俗稱的 ACID 特性。事務處理可以確保除只有本事務單元內的所有操作都成功完成,否則不會永久更新面向資料的資源。事務是資料庫執行中的一個邏輯工作單位,由DBMS中的事務管理子系統負責事務的處理。
事務主要用於複雜度高、重要且會出現併發的任務。比如銀行扣款、生成訂單、轉賬匯款、裝備強化(大霧)等任務的的過程需要封裝成一個事務。

  • 原子性(Atomicity)
    原子性是指事務包含的所有操作,要麼全部成功,要麼全部失敗,不會出現部分提交部分未提交的情況。

  • 一致性(Consistency)
    一致性是指事務必須使資料庫從一個一致性的狀態變到另一個一致性的狀態,也就是事務執行前和執行後,資料庫內容都處於一致性狀態

  • 隔離線(Isolation)
    隔離線是指當有多個使用者併發訪問資料庫時,資料庫為每個使用者開啟一個事務,事務間不能互相影響,具體的隔離效果,需要根據事務的隔離級別來確定(Mysql中InnoDB 支援事務,MyISAM 不支援事務)。

  • 永續性(Durability)
    永續性是指一個事務一旦提交,那麼事務對資料修改的效果是永久性的,不會由於資料庫故障而丟失

二、事物的實現原理

原子性、一致性、永續性通過資料庫的redo log和undo log來完成。

redo log是InnoDB儲存引擎層的日誌,又稱重做日誌檔案,用於記錄事務操作的變化,記錄的是資料修改之後的值,不管事務是否提交都會記錄下來。在一條更新語句進行執行的時候,InnoDB引擎會把更新記錄寫到redo log日誌中,然後更新記憶體,此時算是語句執行完了,然後在空閒的時候或者是按照設定的更新策略將redo log中的內容更新到磁碟中

undo log一般是邏輯日誌,根據每行記錄進行記錄,主要儲存的是資料的邏輯變化日誌,比如說我們要insert一條資料,那麼undo log就會生成一條對應的delete日誌。簡單點說,undo log記錄的是資料修改之前的資料,因為需要支援回滾。

那麼當需要回滾時,只需要利用undo log的日誌就可以恢復到修改前的資料。

undo log另一個作用是實現多版本控制(MVCC),undo記錄中包含了記錄更改前的映象,如果更改資料的事務未提交,對於隔離級別大於等於read commit的事務而言,不應該返回更改後資料,而應該返回老版本的資料。

處理流程大概如下:

image.png

bin log是儲存所有資料變更的情況,理論上只要記錄在bin log上的資料,都可以恢復。而redo log不會儲存歷史所有的資料的變更,當記憶體資料重新整理到磁碟中,redo log的資料就失效

事務的隔離性通過鎖機制來實現

三、資料庫的隔離級別

在我們實際的工作中,資料庫中的資料經常被多個使用者共同訪問的,在多個使用者同時操作相同的資料時,可能就會出現一些事務的併發問題:丟失更新、不可重複讀、髒讀和幻讀。

  1. 讀未提交(Read Uncommitted):一個事務在執行過程中,既可以訪問其他事務未提交的新插入的資料,又可以訪問未提交的修改資料。如果一個事務已經開始寫資料,則另外一個事務不允許同時進行寫操作,但允許其他事務讀此行資料。此隔離級別可防止丟失更新。

  2. 讀已提交(Read Committed):一個事務在執行過程中,既可以訪問其他事務成功提交的新插入的資料,又可以訪問成功修改的資料。讀取資料的事務允許其他事務繼續訪問該行資料,但是未提交的寫事務將會禁止其他事務訪問該行。此隔離級別可有效防止髒讀。

髒讀是指事務T1讀取了事務T2未提交的資料,事務T2回滾後導致T1讀到的資料是髒資料庫。髒讀的粒度是錶行。解決髒讀的方法是修改隔離級別為“未讀提交”以上。

  1. 可重複讀(Repeatable Read):一個事務在執行過程中,可以訪問其他事務成功提交的新插入的資料,但不可以訪問成功修改的資料。讀取資料的事務將會禁止寫事務(但允許讀事務),寫事務則禁止任何其他事務。此隔離級別可有效防止不可重複讀和髒讀。這是Mysql資料庫的預設隔離級別。

不可重複讀是指事務T1多次查詢同一條記錄,返回了不同的資料,這是由於在兩次查詢間隔過程中,資料庫記錄被事務T2修改了導致的。不可重複讀的粒度是錶行。

  1. 序列化(Serializable):事務執行的時候不允許別的事務併發執行,而是完全序列化的讀,只要存在讀就禁止寫,但可以同時讀,消除了幻讀。這是事務隔離的最高階別,雖然最安全,但是效率太低,一般不會用。

幻讀是指事務T1讀取了表中的一類資料進行操作時,在操作過程中,事務T2修改(新增、修改或刪除)了批量中的某條記錄,導致T1發現資料不一致。不可重複讀和幻讀的主要區別是:不可重複讀側重於資料被修改,幻讀側重於資料被新增或刪除。

具體情況如下表格:

髒讀 不可重複讀 幻讀
讀未提交
讀已提交 ×
可重複讀 × ×
序列化 × × ×

3.1 間隙鎖(Gap Lock)

間隙鎖是Innodb在可重複度的級別中為了解決幻讀問題時引入的鎖機制。幻讀的問題存在是因為新增或者更新操作,這時如果進行範圍查詢的時候(加鎖查詢),會出現不一致的問題,這時使用不同的行鎖已經沒有辦法滿足要求,需要對一定範圍內的資料進行加鎖,間隙鎖就是解決這類問題的。在可重複讀隔離級別下,資料庫是通過行鎖和間隙鎖共同組成的(next-key lock),來實現的。

間隙鎖是封鎖索引記錄中的間隔,或者第一條索引記錄之前的範圍,又或者最後一條索引記錄之後的範圍。

間隙鎖產生的條件:

  • 1.使用普通索引鎖定;
  • 2.使用多列唯一索引;
  • 3.使用唯一索引鎖定多行記錄。

間隙鎖特性:

  • 1.加鎖的基本單位是(next-key lock),他是左開右閉原則。
  • 2.插敘過程中訪問的物件會增加鎖。
  • 3.索引上的等值查詢--給唯一索引加鎖的時候,next-key lock升級為行鎖。
  • 4.索引上的等值查詢--向右遍歷時最後一個值不滿足查詢需求時,next-key lock 退化為間隙鎖。
  • 5.唯一索引上的範圍查詢會訪問到不滿足條件的第一個值為止。

四、MVCC多版本併發控制

MVCC,全稱Multi-Version Concurrency Control,即多版本併發控制。MVCC是一種併發控制的方法,一般在資料庫管理系統中,實現對資料庫的併發訪問,在程式語言中實現事務記憶體。

MVCC的使用特點:

  • 應對高併發事務, MVCC比單純的加鎖更高效。
  • MVCC只在讀已提交(Read Committed) 和可重複讀(Repeatable Read) 兩個隔離級別下工作。
  • MVCC可以使用 樂觀(optimistic)鎖 和 悲觀(pessimistic)鎖來實現。

在併發運算元據庫的過程中,讀操作可能會讀取到不一致的資料(髒讀),為了避免這種情況,就要對資料庫的併發訪問進行控制,比如加鎖處理(如 for upudate,這些都被稱為當前讀,對應之後的快照讀)。但是,加鎖會導致讀寫操作變為序列化,導致讀操作會被寫操作阻塞,大幅降低讀效能。

在java的concurrent包中,有copyonwrite系列的類,它的作用就是用於優化讀操作遠大於寫操作的情況。在進行寫操作時,將會將資料copy一份,不會影響原有資料,然後進行修改,修改完成後原子替換掉舊的資料,而讀操作只會讀取原有資料。通過這種方式實現寫操作不會阻塞讀操作,從而優化讀效率。

MVCC的原理與copyonwrite類似。在MVCC協議下,每個讀操作會看到一個一致性的快照(snapshot),並且可以實現非阻塞的讀。MVCC允許資料具有多個版本,這個版本可以是時間戳或者是全域性遞增的事務ID,在同一個時間點,不同的事務看到的資料是不同的。

4.1 MVCC的實現原理

MVCC的目的就是多版本併發控制,在資料庫中的實現,就是為了解決讀寫衝突,它的實現原理主要是依賴記錄中的 3個隱式欄位,undo日誌 ,Read View 來實現的。

資料庫的隱式欄位:

  • DB_TRX_ID(當前事務ID):記錄建立這條記錄/最後一次修改該記錄的事務ID。
  • DB_ROLL_PTR(回滾指標):指向這條記錄的上一個版本,回滾指標指向寫入回滾段的undo log記錄,讀取記錄的時候會根據指標去讀取undo log中的記錄。

因為MySQL中undo log中會維護一個歷史資料記錄,所以我們應該養成定期提交事務的習慣,否則回滾段會越來越大,甚至佔滿了表空間。

  • DB_ROW_ID (隱藏主鍵):如果資料表沒有主鍵,InnoDB會自動以DB_ROW_ID產生一個聚簇索引。

Read View是事務進行快照讀操作的時候生產的讀檢視(Read View),在該事務執行的快照讀的那一刻,會生成資料庫系統當前的一個快照,記錄並維護系統當前活躍事務的ID(當每個事務開啟時,都會被分配一個ID, 這個ID是遞增的,所以最新的事務,ID值越大)

所以我們知道 Read View主要是用來做可見性判斷的, 即當我們某個事務執行快照讀的時候,對該記錄建立一個Read View讀檢視,把它比作條件用來判斷當前事務能夠看到哪個版本的資料,既可能是當前最新的資料,也有可能是該行記錄的undo log裡面的某個版本的資料

Read View遵循一個可見性演算法,主要是將要被修改的資料的最新記錄中的DB_TRX_ID(當前事務ID)取出來,與系統當前其他活躍事務的ID去對比(由Read View維護),如果DB_TRX_ID跟Read View的屬性做了某些比較,不符合可見性,那就通過DB_ROLL_PTR(回滾指標)去取出Undo Log中的DB_TRX_ID再比較,即遍歷連結串列的DB_TRX_ID(從鏈首到鏈尾,即從最近的一次修改查起),直到找到滿足特定條件的DB_TRX_ID, 那麼這個DB_TRX_ID所在的舊記錄就是當前事務能看見的最新老版本。

那麼MVCC機制到底如何查詢的呢?

根據網上的資料,可以得出這樣的結論:

  • 1、查詢DB_TRX_ID小於等於當前事務ID的資料。(這裡要等於是因為假如自己的事務插入了一條資料,會生成一條當前事務ID的資料,所以必須包含本事務自己插入的資料)
  • 2、查詢未刪除(回滾指標為空)或者回滾指標大於當前事務ID的資料。(這裡不能等於是因為假如自己的事務刪除了一條資料,會生成資料的回滾指標為當前事務ID,所以必須排除掉自己刪除的資料)

大家好,我是練習java兩年半時間的南橘,下面是我的微信,需要之前的導圖或者想互相交流經驗的小夥伴可以一起互相交流哦。

相關文章