深入理解事務與鎖機制(上)


事務及其特性

首先看看什么是事務?事務具有哪些特性?關於事務,上大學的時候,你應該有接觸過相關的課程。簡單來說,事務是指作為單個邏輯工作單元執行的一系列操作,這些操作要么全做,要么全不做,是一個不可分割的工作單元。

 

一個邏輯工作單元要成為事務,在關系型數據庫管理系統中,必須滿足 4 個特性,即所謂的 ACID:原子性、一致性、隔離性和持久性。

  • 一致性:事務完成之后,事務所做的修改進行持久化保存,不會丟失。

  • 原子性:事務的所有操作,要么全部完成,要么全部不完成,不會結束在某個中間環節。

  • 持久性:事務開始之前和事務結束之后,數據庫的完整性限制未被破壞。

  • 隔離性:當多個事務並發訪問數據庫中的同一數據時,所表現出來的相互關系。

ACID 及它們之間的關系如下圖所示,比如 4 個特性中有 3 個與 WAL 有關系,都需要通過 Redo、Undo 日志來保證等。

 

 

一致性

首先來看一致性,一致性其實包括兩部分內容,分別是約束一致性和數據一致性。

  • 約束一致性:大家應該很容易想到數據庫中創建表結構時所指定的外鍵、Check、唯一索引等約束。可惜在 MySQL 中,是不支持 Check 的,只支持另外兩種,所以約束一致性就非常容易理解了。

  • 數據一致性:是一個綜合性的規定,或者說是一個把握全局的規定。因為它是由原子性、持久性、隔離性共同保證的結果,而不是單單依賴於某一種技術。

原子性

接下來看原子性,原子性就是前面提到的兩個“要么”,即要么改了,要么沒改。也就是說用戶感受不到一個正在改的狀態。MySQL 是通過 WAL(Write Ahead Log)技術來實現這種效果的。

 

可能你想問,原子性和 WAL 到底有什么關系呢?其實關系非常大。舉例來講,如果事務提交了,那改了的數據就生效了,如果此時 Buffer Pool 的臟頁沒有刷盤,如何來保證改了的數據生效呢?就需要使用 Redo 日志恢復出來的數據。而如果事務沒有提交,且 Buffer Pool 的臟頁被刷盤了,那這個本不應該存在的數據如何消失呢?就需要通過 Undo 來實現了,Undo 又是通過 Redo 來保證的,所以最終原子性的保證還是靠 Redo 的 WAL 機制實現的。

 

持久性

再來看持久性。所謂持久性,就是指一個事務一旦提交,它對數據庫中數據的改變就應該是永久性的,接下來的操作或故障不應該對其有任何影響。前面已經講到,事務的原子性可以保證一個事務要么全執行,要么全不執行的特性,這可以從邏輯上保證用戶看不到中間的狀態。但持久性是如何保證的呢?一旦事務提交,通過原子性,即便是遇到宕機,也可以從邏輯上將數據找回來后再次寫入物理存儲空間,這樣就從邏輯和物理兩個方面保證了數據不會丟失,即保證了數據庫的持久性。

隔離性

最后看下隔離性。所謂隔離性,指的是一個事務的執行不能被其他事務干擾,即一個事務內部的操作及使用的數據對其他的並發事務是隔離的。鎖和多版本控制就符合隔離性。

並發事務控制

單版本控制-鎖

先來看鎖,鎖用獨占的方式來保證在只有一個版本的情況下事務之間相互隔離,所以鎖可以理解為單版本控制。

 

在 MySQL 事務中,鎖的實現與隔離級別有關系,在 RR(Repeatable Read)隔離級別下,MySQL 為了解決幻讀的問題,以犧牲並行度為代價,通過 Gap 鎖來防止數據的寫入,而這種鎖,因為其並行度不夠,沖突很多,經常會引起死鎖。現在流行的 Row 模式可以避免很多沖突甚至死鎖問題,所以推薦默認使用 Row + RC(Read Committed)模式的隔離級別,可以很大程度上提高數據庫的讀寫並行度。

多版本控制-MVCC

多版本控制也叫作 MVCC,是指在數據庫中,為了實現高並發的數據訪問,對數據進行多版本處理,並通過事務的可見性來保證事務能看到自己應該看到的數據版本。

 

那個多版本是如何生成的呢?每一次對數據庫的修改,都會在 Undo 日志中記錄當前修改記錄的事務號及修改前數據狀態的存儲地址(即 ROLL_PTR),以便在必要的時候可以回滾到老的數據版本。例如,一個讀事務查詢到當前記錄,而最新的事務還未提交,根據原子性,讀事務看不到最新數據,但可以去回滾段中找到老版本的數據,這樣就生成了多個版本。

 

多版本控制很巧妙地將稀缺資源的獨占互斥轉換為並發,大大提高了數據庫的吞吐量及讀寫性能。

 

了解了單版本控制(鎖)和多版本控制(MVCC),相信你對數據庫的設置及並發性已經有了比較深入的理解。

特性背后的技術原理

接下來看一下每個特性是通過什么技術原理實現的。

原子性背后的技術

先來看看原子性,每一個寫事務,都會修改 Buffer Pool,從而產生相應的 Redo 日志,這些日志信息會被記錄到 ib_logfiles 文件中。因為 Redo 日志是遵循 Write Ahead Log 的方式寫的,所以事務是順序被記錄的。

 

在 MySQL 中,任何 Buffer Pool 中的頁被刷到磁盤之前,都會先寫入到日志文件中,這樣做有兩方面的保證。

  1. 如果 Buffer Pool 中的這個頁沒有刷成功,此時數據庫掛了,那在數據庫再次啟動之后,可以通過 Redo 日志將其恢復出來,以保證臟頁寫下去的數據不會丟失,所以必須要保證 Redo 先寫。

  2. 因為 Buffer Pool 的空間是有限的,要載入新頁時,需要從 LRU 鏈表中淘汰一些頁,而這些頁必須要刷盤之后,才可以重新使用,那這時的刷盤,就需要保證對應的 LSN 的日志也要提前寫到 ib_logfiles 中,如果沒有寫的話,恰巧這個事務又沒有提交,數據庫掛了,在數據庫啟動之后,這個事務就沒法回滾了。所以如果不寫日志的話,這些數據對應的回滾日志可能就不存在,導致未提交的事務回滾不了,從而不能保證原子性,所以原子性就是通過 WAL 來保證的。

持久性背后的技術

再來看持久性,如下圖所示,一個“提交”動作觸發的操作有:binlog 落地、發送 binlog、存儲引擎提交、flush_logs, check_point、事務提交標記等。這些都是數據庫保證其數據完整性、持久性的手段。      

 

那這些操作如何做到持久性呢?前面講過,通過原子性可以保證邏輯上的持久性,通過存儲引擎的數據刷盤可以保證物理上的持久性。這個過程與前面提到的 Redo 日志、事務狀態、數據庫恢復、參數 innodb_flush_log_at_trx_commit 有關,還與 binlog 有關。這里多提一句,在數據庫恢復時,如果發現某事務的狀態為 Prepare,則會在 binlog 中找到對應的事務並將其在數據庫中重新執行一遍,來保證數據庫的持久性。

隔離性背后的技術

接下來看隔離性,InnoDB 支持的隔離性有 4 種,隔離性從低到高分別為:讀未提交、讀提交、可重復讀、可串行化。

  1. 讀未提交(RU,Read Uncommitted)。它能讀到一個事務的中間過程,違背了 ACID 特性,存在臟讀的問題,所以基本不會用到,可以忽略。

  2. 讀提交(RC,Read Committed)。它表示如果其他事務已經提交,那么我們就可以看到,這也是一種最普遍適用的級別。但由於一些歷史原因,可能 RC 在生產環境中用的並不多。

  3. 可重復讀(RR,Repeatable Read),是目前被使用得最多的一種級別。其特點是有 Gap 鎖、目前還是默認的級別、在這種級別下會經常發生死鎖、低並發等問題。

  4. 可串行化,這種實現方式,其實已經並不是多版本了,又回到了單版本的狀態,因為它所有的實現都是通過鎖來實現的。

具體說到隔離性的實現方式,我們通常用 Read View 表示一個事務的可見性。前面講到 RC 級別的事務可見性比較高,它可以看到已提交的事務的所有修改。而 RR 級別的事務,則沒有這個功能,一個讀事務中,不管其他事務對這些數據做了什么修改,以及是否提交,只要自己不提交,查詢的數據結果就不會變。這是如何做到的呢?

 

隨着時間的推移,讀提交每一條讀操作語句都會獲取一次 Read View,每次更新之后,都會獲取數據庫中最新的事務提交狀態,也就可以看到最新提交的事務了,即每條語句執行都會更新其可見性視圖。而反觀下面的可重復讀,這個可見性視圖,只有在自己當前事務提交之后,才去更新,所以與其他事務是沒有關系的。

 

這里需要提醒大家的是:在 RR 級別下,長時間未提交的事務會影響數據庫的 PURGE 操作,從而影響數據庫的性能,所以可以對這樣的事務添加一個監控。

 

最后我們來講下可串行化的隔離級別,前面已經提到了,可串行化是通過鎖來實現的,所以實際上並不是多版本控制,它的特點也很明顯:讀鎖、單版本控制、並發低。

一致性背后的技術

接下來是一致性。一致性可以歸納為數據的完整性。根據前文可知,數據的完整性是通過其他三個特性來保證的,包括原子性、隔離性、持久性,而這三個特性,又是通過 Redo/Undo 來保證的,正所謂:合久必分,分久必合,三足鼎力,三分歸晉,數據庫也是,為了保證數據的完整性,提出來三個特性,這三個特性又是由同一個技術來實現的,所以理解 Redo/Undo 才能理解數據庫的本質。

 

 

 如上圖所示,邏輯上的一致性,包括唯一索引、外鍵約束、check 約束,這屬於業務邏輯范疇,這里就不做贅述了。

MVCC 實現原理

前文多次提到了 MVCC 這個概念,這里我們來講解 MVCC 的實現原理。MySQL InnoDB 存儲引擎,實現的是基於多版本的並發控制協議——MVCC,而不是基於鎖的並發控制。

 

MVCC 最大的好處是讀不加鎖,讀寫不沖突。在讀多寫少的 OLTP(On-Line Transaction Processing)應用中,讀寫不沖突是非常重要的,極大的提高了系統的並發性能,這也是為什么現階段幾乎所有的 RDBMS(Relational Database Management System),都支持 MVCC 的原因。 

快照讀與當前讀

在 MVCC 並發控制中,讀操作可以分為兩類: 快照讀(Snapshot Read)與當前讀 (Current Read)。

  • 快照讀:讀取的是記錄的可見版本(有可能是歷史版本),不用加鎖。

  • 當前讀:讀取的是記錄的最新版本,並且當前讀返回的記錄,都會加鎖,保證其他事務不會再並發修改這條記錄。 

注意:MVCC 只在 Read Commited 和 Repeatable Read 兩種隔離級別下工作。

 

如何區分快照讀和當前讀呢? 可以簡單的理解為:

  • 快照讀:簡單的 select 操作,屬於快照讀,不需要加鎖。 

  • 當前讀:特殊的讀操作,插入/更新/刪除操作,屬於當前讀,需要加鎖。 

MVCC 多版本實現

為了讓大家更直觀地理解 MVCC 的實現原理,這里舉一個“事務對某行記錄更新的過程”的案例來講解 MVCC 中多版本的實現。

  • 假設 F1~F6 是表中字段的名字,1~6 是其對應的數據。后面三個隱含字段分別對應該行的隱含ID、事務號和回滾指針,如下圖所示。

 

 

  • 隱含 ID(DB_ROW_ID),6 個字節,當由 InnoDB 自動產生聚集索引時,聚集索引包括這個 DB_ROW_ID 的值。

  • 事務號(DB_TRX_ID),6 個字節,標記了最新更新這條行記錄的 Transaction ID,每處理一個事務,其值自動 +1。

  • 回滾指針(DB_ROLL_PT),7 個字節,指向當前記錄項的 Rollback Segment 的 Undo log記錄,通過這個指針才能查找之前版本的數據。

具體的更新過程,簡單描述如下。

 

首先,假如這條數據是剛 INSERT 的,可以認為 ID 為 1,其他兩個字段為空。

 

然后,當事務 1 更改該行的數據值時,會進行如下操作,如下圖所示。

 

 

  • 用排他鎖鎖定該行;記錄 Redo log;

  • 把該行修改前的值復制到 Undo log,即圖中下面的行;

  • 修改當前行的值,填寫事務編號,使回滾指針指向 Undo log 中修改前的行。

接下來,與事務 1 相同,此時 Undo log 中有兩行記錄,並且通過回滾指針連在一起。因此,如果 Undo log 一直不刪除,則會通過當前記錄的回滾指針回溯到該行創建時的初始內容,所幸的是在 InnoDB 中存在 purge 線程,它會查詢那些比現在最老的活動事務還早的 Undo log,並刪除它們,從而保證 Undo log 文件不會無限增長,如下圖所示。

 

 

並發事務問題及解決方案

 

上文講述了 MVCC 的原理及其實現。那么隨着數據庫並發事務處理能力的大大增強,數據庫資源的利用率也會大大提高,從而提高了數據庫系統的事務吞吐量,可以支持更多的用戶並發訪問。但並發事務處理也會帶來一些問題,如:臟讀、不可重復讀、幻讀。下面一一解釋其含義。

臟讀

一個事務正在對一條記錄做修改,在這個事務完成並提交前,這條記錄的數據就處於不一致狀態;這時,另一個事務也來讀取同一條記錄,如果不加控制,第二個事務讀取了這些“臟”數據,並據此做進一步的處理,就會產生未提交的數據依賴關系。這種現象被形象的叫作"臟讀"(Dirty Reads)。

不可重復讀

一個事務在讀取某些數據后的某個時間,再次讀取以前讀過的數據,卻發現其讀出的數據已經發生了改變、或某些記錄已經被刪除了!這種現象就叫作“ 不可重復讀”(Non-Repeatable Reads)。

幻讀

一個事務按相同的查詢條件重新讀取以前檢索過的數據,卻發現其他事務插入了滿足其查詢條件的新數據,這種現象就稱為“幻讀”(Phantom Reads)。

解決方案

產生的這些問題,MySQL 數據庫是通過事務隔離級別來解決的,上文已經詳細講解過,這里再進行簡單的說明。

 

在上文講 MySQL 事務特性的隔離性的時候就已經詳細地講解了事務的四種隔離級別。這里要求大家能夠記住這種關系的矩陣表;記住各種事務隔離級別及各自都解決了什么問題,如下圖所示。

 

 

這里舉例說明“臟讀”和“不可重復讀”的問題。

 

MySQL 中默認的事務隔離級別是 RR,這里設置成 RC 隔離級別,此時提交事務 B 修改 id=1 的數據之后,事務 A 進行同樣的查詢操作,后一次和前一次的查詢結果不一樣,這就是不可重復讀(重新讀取產生的結果不一樣了)。這里事務 A 讀到了事務 B 提交的數據,即是“臟讀”。

 

 上文講解了不可重復讀的情況,下面我們來看看在RR隔離級別下的情況。當 teacher_id=1時,事務 A 先進行一次讀取操作,事務 B 中間修改了 id=1 的數據並提交,事務 C 也插入了一條數據並提交。事務 A 第二次讀到的數據和第一次完全相同。所以說它是可重讀的。

 

 

這里我們舉個例子來說明“幻讀”的問題。

 

行鎖可以防止不同事務版本的數據在修改提交時造成數據沖突的情況。但如何避免別的事務插入數據造成的問題呢。我們先來看看在 RC 隔離級別下的處理過程。

 

如下圖所示,事務 A 修改了所有 teacher_id=30 的數據,但是當事務 B INSERT 新數據后,事務 A 發現莫名其妙的多了一行 teacher_id=30 的數據, 而且沒有被之前的 UPDATE語句所修改,這就是“當前讀”的幻讀問題。

 

 跟上面的例子一樣,也是在 RC 事務隔離級別下,這時事務 B INSERT 了一條數據,並提交,而事務 A  讀到了事務 B 新插入的數據。這也是幻讀,如下圖所示。

 

 

這里就需要重點注意不可重復讀和幻讀的區別了。前面講了它們的含義,這個提醒大家的是:不可重復讀重點在於 UPDATA 和 DELETE,而幻讀的重點在於 INSERT。它們之間最大的區別是如何通過鎖機制來解決它們產生的問題。這里說的鎖只是使用悲觀鎖機制。

 

那么在 RR 隔離級別下,事務 A 在 UPDATE 后加鎖,事務 B 無法插入新數據,這樣事務 A在 UPDATE 前后讀的數據保持一致,避免了幻讀。

 

跟上面的案例一樣,也是在 RR 事務隔離級別下,事務 A 在 UPDATE 后加鎖,對於其他兩個事務,事務 B 和事務 C 的 INSERT 操作,就必須等事務 A 提交后,才能繼續執行。這里就用到了“鎖”,這里使用的是 Gap 鎖,后面會詳細講解。它和上面的情況一樣,解決了“幻讀”的發生,如下圖所示。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM