一、前言
隨着業務發展,對數據庫的併發性能要求也越來越高,不僅要做到高併發還需要在保障數據安全,那麼今天我們聊一聊 MySQL 在高併發下事務、MVCC、鎖機制是如何在高併發情況下維護數據的安全。
二、事務 ACID
- 爲什麼需要事務:事務是爲了
保障
用戶的數據操作對數據是安全的
。比如我們的銀行卡餘額,我們希望對它的操作是穩定準確
的,而且絕對安全
。 ACID
:事務的四大特性原子性
、一致性
、隔離性
、持久性
。原子性
:原子性是指一個事務
要麼全部執行
,要麼完全不執行
。主要是由 innodb 引擎中的undo
回滾日誌來維護
。隔離性
:事務在操作過程
中不會受到其它事務
操作的影響
。主要由事務的隔離級別
和鎖機制
共同維護。持久性
:事務操作的結果是具體持久性的,通俗來講就是提交事務後會持久化存儲
(落盤)。主要是由redo log
來維護。一致性
:事務在開始和結束時,數據始終保持一致。由原子性、隔離性、持久性共同維護。
三、多版本併發控制 MVCC
-
介紹:數據庫的核心方向就是
高併發
,MySQL 通過併發控制技術
來維護
高併發的環境下數據的一致性
和數據安全
。MySQL 併發控制有兩種
技術方案鎖機制(Locking)
和多版本併發控制 (MVCC)
。 -
鎖機制:通過
鎖機制
可以維護數據的一致性
,但是整體業務場景大多是讀-讀
、讀-寫
、寫-寫
,三類併發場景,看似容易融合到業務場景後也比較複雜
。通過鎖機制
主要可以幫助我們解決寫-寫
和讀-讀
場景下的併發安全
問題 則MVCC
主要幫助解決讀-寫
問題。 -
MVCC:
多版本併發控制
,側重優化讀-寫
業務的高併發環境。可以解決寫操作
時堵塞讀操作
的併發
問題。 -
一致性非鎖定讀
:指 innodb 引擎通過多版本併發控制
的方式來讀取,當前執行時間數據庫中的行數據。讀取正在進行 update 或 delete 操作的行,不會等待
鎖釋放,而是會讀取
該行的快照數據
。快照就是指該行之前的版本
數據,主要靠undo
日誌來實現,而 undo 是用於數據庫回滾
,因此快照讀本身是沒有開銷
的。後臺 Purge 線程也會自動清理一些不需要的 undo 數據。 -
MVCC 兩類讀操作:分爲兩類讀情況
快照讀(Snapshot Read)
和當前讀(Current Read)
快照讀是讀取數據的可見版本
而當前讀則是讀取當前數據的最新版本
需要加鎖
從而保障其它事務不會修改當前數據。 -
MVCC 實現策略:我們在設計表過程中通過不會
直接刪除
數據而是設定一個字段來標記,從實現邏輯
意義上的刪除。MVCC 的實現方式也與此類似。這種數據管理方式叫數據生命週期管理
,其中兩個指標就是標記數據的變化
和標記數據可用狀態
。 -
MVCC 下的
DML
過程演示:Insert:
進行 insert 操作,事務 id 假設爲 1
id name create version delete version 1 test 1 Update:
MVCC 會先將當前記錄標記爲已刪除在 delete version 字段下設置版本號(原來爲空),然後新增一行數據,寫入相應的版本號,此時爲新版本號爲 2 和上一條數據的 delete version 一致,比如將 name 修改爲 fantasy,如下表:
id name create version delete version 1 test 1 2 1 fantasy 2 Delete:
直接將當前數據的 delete version 打上版本號標記爲刪除
id name create version delete version 1 test 1 2 1 fantasy 2 3 -
MVCC 解析:剛纔只是在
邏輯層面
上介紹 MVCC 的運作方式 create version 和 delete version 維護的是數據的版本信息和數據可用狀態
,而實際上
還有一個字段是用戶undo
回滾的指針,接下來我們介紹源碼
中 MVCC 的實現方式。默認
會給每張表加入三個隱藏
字段(內部屬性
)DB_TRX_ID:佔 6 個字節,記錄每一行最近一次修改它的事務 ID
DB_ROLL_OIR:佔 7 個字節,記錄指向回滾段 undo 日誌的指針
DB_ROW_ID:佔 6 個字節,當寫入數據時,自動維護自增列將三個字段結合就可以標記數據的週期性和,並定位到對應的事務。這就引出 innodb 中實現 MVCC 兩個重要模塊 undo 日誌用來存儲數據的變化 Read View 用來做可見性判斷的, 裏面保存了
對本事務不可見的其他活躍事務
-
注意:對於 innodb 來講,無論是
更新
還是刪除
,都只是設置行記錄上的deldte BIT
來標記,而並不是真正的刪除記錄
,後續這些記錄的清理就需要Purge
線程來做。還需要注意的是 MVCC 只能在RC
和RR
隔離級別下使用,RU
是讀未提交
狀態,所以不存在版本問題
,而串行化
則會對讀取的數據行加鎖。
四、隔離級別
-
爲什麼需要
隔離
級別?
事務之間如果不互相隔離
,那麼就會出現髒讀
、不可重複讀
和幻讀
。 -
簡單概括髒讀、不可重複讀和幻讀:
寫
在前,讀
在後:髒讀;
讀
在前,寫
在後:不可重複讀;
讀
在前,寫
在後,再讀
:幻讀。 -
隔離級別與併發問題的關係如下:
級別 髒讀 不可重複讀 幻讀 讀未提交(READ-UNCOMMITTED) ✅ ✅ ✅ 不可重複讀(READ-COMMITTED) ❌ ✅ ✅ 可重複讀(REPEATABLE-READ) ❌ ❌ 🆚 串行化(SERIALIZABLE) ❌ ❌ ❌ 其中
串行化
隔離級別雖然解決了所有數據問題,但是卻帶來了併發的性能
問題,而讀未提交
的隔離級別違反了基礎事務的安全
處理要求,所以我們在選擇隔離級別時都會在 RC 和 RR 中選擇,MySQL默認隔離級
別爲 RR 級別。-- 查詢數據庫中的隔離級別 select @@transaction_isolation; -- 臨時設置MySQL數據庫中的隔離級別 set global transaction_isolation='READ-COMMITTED';
-
RC 和 RR 的區別:RC 在事務可以讀取到
其它事務
提交的事務數據,而對於 RR 級別來講,它會保證在一個事務中數據多次的查詢
結果是不變的,儘管其它事務已經提交了改動。從鎖的角度來講 RC 的性能
會優於 RR。 -
RC 和 RR 級別下的
快照讀
:RC 級別下的快照讀總是會讀取被鎖定
行的最新版本
的一份快照
數據,而 RR 級別下的快照讀總是會讀取事務開始
時行版本的數據。這個怎麼理解呢?請看如下案例:
首先打開 MySQL 會話
Session 1
開啓一個事務然後查詢一條數據
開啓另一個 MySQL 會話
Session 2
模擬併發場景,開啓事務修改
id1 = 3 中的 id2 爲 8023
在
Session 2
中我們修改了 id2 20170831 爲 8023 但是還未提交
,此時 id1 = 3 的行已經加上了一個X 排它鎖
,此時再讀取Session 1
會話中的 id1 = 3 的記錄根據 innodb 引擎的特性,即在 RR 和 RC 事務隔離級別下會使用“非鎖定一致性讀”
也就是快照讀。接者Session 1
未提交的事務再此運行查詢 id1 = 3 的 SQL 語句,此時無論
此時隔離級別是 RR 和 RC 結果都如下圖:
接者我們提交
Session 2
中的事務。
在
Session 2
中的事務提交後,在Session 1
中再次執行查詢 id1 = 3 的 SQL 語句,此時在RR
和RC
級別下運行的結果就不同
了,RC 事務的隔離級別
,總是讀取最新版本
的快照
數據,因爲Session2
提交了事務更新
了快照版本
,所以 Session 1 在事務中可以讀取 Session 2 中已經提交
的改動,結果如下:
因爲
RR
級別讀取數據快照總是讀取開始事務前
的行版本的快照數據,所以儘管Session 2
更新了快照版本,RR
級別下事務未提交之前不會
受到影響,所以RR
級別下兩次讀取數據的結果都相同
:
五、鎖機制
-
什麼是鎖?
鎖是計算機協調
多個進程或線程併發
訪問某一資源的機制。 -
innodb 兩種鎖:
共享鎖 S
:允許一個事務去讀
一行,阻止其它事務獲得相同數據集的排它鎖
。通俗來講就是可以重複讀,沒讀完時不允許寫。
排它鎖 X
:允許獲得排它鎖的事務更新
數據,阻止其它事務獲得相同數據集的排他鎖
和共享鎖
。通俗來講就是寫的時候不允許其它事務寫和讀。 -
發現
問題
:有兩個事務 A 和 B,事務 A 鎖住了表中的一行數據,加上行鎖 S
,即這一行只能讀不能寫。之後事務 B 又申請整張表
的寫鎖 (mysql 中可以使用 lock table xxx write 鎖表),正常邏輯來將,事務 B 就可以修改
表中任意行數據,包括事務 A 鎖住的那一行數據,實際情況則會發生鎖衝突
,現在就需要一種機制來判斷是否有行鎖
,比如鎖表前先判斷每一行數據是否有行鎖
,但是這種方案在隨着數據量增大代價
會無限放大,肯定是不取的,而意向鎖
就是來解決衝突的協調者
。 -
意向鎖工作流程:
事務A
首先需要申請表的意向鎖
,成功後申請一行的行鎖。
事務B
申請排它鎖
,但是發現表中已經有意向共享鎖
,說明表中的某行數據已經被鎖定,此時申請的寫鎖會被堵塞
。 -
意向共享鎖
IS
:事務給某行數據加入共享鎖
前,需要先申請意向共享鎖
。通俗來講,就是一個數據行加共享鎖前必須要先取得該表的意向共享鎖。 -
意向排它鎖
IX
:與上類似,加入排它鎖
前需要先獲得意向排它鎖
。 -
innodb
行鎖
:行鎖是通過給索引
加鎖來實現的,不用擔心表中是否創建
了索引,如果有主鍵 MySQL 會在主鍵
上創建聚簇索引
用於回表查詢
,如果沒有主鍵則考慮unique 約束
的字段,如果前面兩種都不滿足,則會創建隱藏列 RowID 作爲聚簇索引。如果不通過索引
檢索數據,那麼 innodb 引擎會對錶中所有的數據加鎖,實際效果與表鎖
相同,所以要儘可能讓所有數據都通過索引來完成,避免行鎖升級爲表鎖。innodb 下的三種行鎖:
行鎖(Record Lock)
:對索引加鎖,即鎖定一行記錄。
間隙鎖(Gap Lock)
:對索引項之間的間隙、對第一條記錄前的間隙或最後一條記錄後的間隙加鎖,即鎖定一個範圍的記錄,不包含記錄本身。
Next-Key Lock
:鎖定一個範圍的記錄幷包含記錄本身。