MySQL學習筆記(二)—MySQL事務及鎖詳解

一、事務

數組庫的一組操作,要麼全部成功,要麼全部失敗

舉例:銀行轉賬 A賬戶向B賬戶轉100

  1. A賬戶餘額扣去100
  2. B賬戶餘額增加100

上述兩個操作要麼全部成功,要麼全部失敗,部分成功或失敗,數據就錯亂了

1. 事務的四大特徵

  • 原子性:事務是原子性操作,要麼全部成功,要麼全部失敗
  • 一致性:多個事務對數據庫操作會保證數據一致性
  • 隔離性:併發時,事務之間互不影響
  • 持久性:事務提交之後對數據庫的影響是持久性的,不會因爲數據庫宕機導致數據丟失

2. 併發事務帶來的問題

髒讀

在一個事務中,讀取了其他事務未提交的數據

不可重複讀

在一個事務中,同一行記錄被訪問了兩次卻得到了不同的結果

幻讀

在一個事務中,同一個範圍內的記錄被讀取時,其他事務向這個範圍添加了新的記錄。

前面髒讀和不可重複讀容易理解,幻讀稍微難一點

假設圖一test開始是空表,事物1第一次查詢得到空表,事物2在事物1執行期間插入一條數據,事物1第二次查詢由於滿足可重複讀,所以查詢結果依然爲空,但是事物1插入同樣一條數據,報重複主鍵錯誤

幻讀兩個要素:

  1. 可重複讀隔離級別下,快照讀看到的是一致性視圖,只有當前讀纔會產生幻讀
  2. 幻讀專指新插入發行,更新不算,將上述查詢後面加上For Update,就會將事務2插入的數據讀出來,這就是幻讀

3. 事務隔離級別

爲了解決上述併發事務問題,MySQL數據庫提供了事務隔離級別

事物隔離級別 髒讀 不可重複讀 幻讀
讀未提交(read-uncommitted)
讀已提交(read-committed)
可重複讀(repeatable-read)
串行化(serializable)

可重複讀是MySQL默認級別

二、重要概念

1. MVCC和事務隔離的實現
  • 同一數據庫記錄可以在系統中存在多個版本,這就是MVCC (多版本併發控制)

  • 不同時刻開啓的事務會創建不同的視圖,後續直接從視圖讀取數據,達到數據隔離,當然數據隔離還需要數據庫鎖的幫助

  • InnoDB 裏面每個事務有一個唯一的事務 ID,叫作 transaction id,在事務開始的時候向 InnoDB 的事務系統申請的,是按申請順序嚴格遞增。

MVCC實現: 在MySQL中,每條記錄的更新都會記錄一條undo Log,記錄上最新的值通過回滾可以,都可以得到前一個狀態的值。

上圖中,數據庫一行記錄有多個版本,每個版本有自己的 row trx_id,最新版本V4的k=22,是被row trx_id=25事務更新的,不同時刻啓動的事務看到不同的視圖,而V1,V2,V3不是物理上真實存在的,要想得到它們需要根據當前版本和undo Log(回滾日誌)計算,比如V1的值需要執行U3,U2,U1才能得到

undo Log日誌如果一直存在,可能會嚴重佔據磁盤空間,當系統沒有比undo Log更早的視圖時,就會把undo Log刪除掉

長事務一般會保存很老的事務視圖,導致其它事務的undo Log無法刪除,所以在這個事務提交前,可能會導致大量undo Log存在,我們需要避免使用長事務

2. 視圖
  1. 用查詢語句定義的虛擬表,在調用的時候執行查詢語句並生成結果。這是我們常說的視圖
  2. InnoDB 用來實現 MVCC 時用到的一致性讀視圖,即 consistent read view, 用於支持 RC(Read Committed,讀提交)和 RR(Repeatable Read,可重複讀)隔離級別的實現。沒有物理結構,僅僅是邏輯上用來定義在事務執行期間能看到什麼數據
3. 事務的起點

begin/start transaction 命令並不是一個事務的起點,在執行到它們之後的第一個操作 InnoDB 表的語句,事務才真正啓動, start transaction with consistent snapshot 該命令可以立即啓動事務

4. 隔離級別與視圖的關係
  1. “讀未提交”隔離級別下直接返回記錄上的最新值,沒有視圖概念

  2. “讀提交”隔離級別,這個視圖是在每個 SQL 語句開始執行的時候創建的

  3. “可重複讀”隔離級別: 視圖是在事務啓動 (執行第一條語句或者使用特定命令) 時創建的,整個事務存在期間都用同一個視圖

  4. “串行化”隔離級別下直接用加鎖的方式來避免並行訪問

5. 當前讀與快照讀
  • 當前讀,在事務執行過程中可以讀到其它已已提交事務的最新數據
  • 快照讀,在事務執行過程中只能看到從事務起點創建的一致性視圖,並不能讀到其它已提交數據

在RR(可重複讀)級別下,快照讀滿足以下兩個規則:

  • 讀取的記錄:更新的事務ID <= 當前事務ID
  • 讀取的記錄:刪除的事務ID > 當前事務ID(小於的話數據都刪了,肯定讀不到)

三、MySQL鎖分類

按照不同維度可分爲:

1)

  • 悲觀鎖
  • 樂觀鎖

2)

  • 共享鎖(寫鎖)
  • 排它鎖(讀鎖)

3)

  • 意向共享鎖
  • 意向互斥鎖

意向鎖其實不會阻塞全表掃描之外的任何請求

假設沒有意向鎖,兩個請求,一個修改數據某一行記錄,另一個需要修改該表所有行記錄,這時需要就需要對所有的行是否被鎖定進行掃描,引入意向鎖,只需要判斷該表有沒有意向鎖,等待修改單行事務提交,意向鎖釋放

4)

  • 全局鎖
  • 表鎖和元數據鎖(meta data lock 簡稱(MDL))
  • 行鎖

全局鎖:對整個數據庫實例加鎖

作用: MyISAM不支持事務拿不到一致性視圖,需要加全局讀鎖做邏輯備份。加讀鎖期間數據庫只能讀,不能寫。

表鎖:使用lock tables 命令來鎖住整個表,一般不使用

MDL: 當對錶做增刪改查操作時,需要加MDL讀鎖;當需要對錶做結構變更操作時需要加MDL寫鎖(見其它篇文章)

所以如果有兩個線程,一個對錶做讀操操作,一個需要給表加字段,第二個操作會被阻塞。

在給表加字段的時候,如果該表請求頻繁,這時會無法獲取MDL寫鎖,同時會阻塞後續業務請求拿讀鎖。

解決方法:在 alter table語句裏面設定等待時間,如果在指定的等待時間裏面能夠拿到 MDL 寫鎖最好,拿不到也不要阻塞後面的業務語句,先放棄。

行鎖: 在引擎層實現,MyISAM不支持行鎖。

重要概念: 兩階段加鎖

在數據庫更新時會給掃描的數據行加行鎖,更新結束不會立馬釋放行鎖,需要等到事務提交纔會釋放行鎖。

由於兩階段鎖的存在,所以在一個事務中,更新語句如果放在前面,會阻塞其它事務對錶的更新,影響併發。對於更新頻繁的語句儘量放在事務的靠後部分

死鎖

解決方案:

  1. 超時等待
  2. 發起死鎖檢測,主動回滾其中某個事務

超時等待的時間根據業務執行時間制定,太短誤傷,太長會影響併發量

死鎖檢測有額外負擔,在事務被鎖住,需要查看其依賴的線程是否被鎖住,一直循環,最後判斷出現死鎖,在多個線程併發修改同一行數據時,時間複雜度會變成O(n^2),會導致CPU利用率很高,卻執行不了幾個事務。一般通過控制併發來解決

5)

  • 記錄鎖(record Lock)
  • 間隙鎖(Gap lock)
  • next-key

在另一篇文章中詳細講解了加鎖情況

數據庫的行鎖實際上record Lock,會對掃描的行加鎖,如果沒有走索引,掃描全表,會鎖住整個表的所有行。

例:

CREATE TABLE `t` (
`id` int(11) NOT NULL,
`c` int(11) DEFAULT NULL,
`d` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `c` (`c`)
) ENGINE=InnoDB;

語句1:select * from t where id >3 for update;
語句2:select * from t where d > 3 for update;
語句1會走主鍵索引,對掃描到的行數加鎖
語句2不走索引,掃描全表,對所有行加record lock

在可重複讀的隔離級別下:
每次開啓事務,會生成一致性視圖,看不到其它事務已經提交的修改,在前面已經提過

更新語句先讀後寫,這個讀是當前讀,就算我們對所有數據加上record lock,也不能阻止數據的插入。這樣我們在當前讀中還是會讀到插入的數據,形成幻讀。

如何避免幻讀?

使用Gap lock + record lock

間隙鎖是對索引記錄中的一段連續區域的鎖
SELECT * FROM users WHERE id BETWEEN 10 AND 20 FOR UPDATE;
這個語句阻止其他事務向表中插入 id = 15 的記錄,因爲整個範圍都被間隙鎖鎖定

雖然間隙鎖中也分爲共享鎖和互斥鎖,不過它們之間並不是互斥的,也就是不同的事務可以同時持有一段相同範圍的共享鎖和互斥鎖,它唯一阻止的就是其他事務向這個範圍中添加新的記錄

間隙鎖的引入,可能會導致同樣的語句鎖住更大的範圍,但是它只在可重複讀級別下才會生效

Next-Key是記錄鎖和記錄前的間隙鎖的結合,每個 next-key lock 是前開後閉區間
select * from t where id = 5
會加上(4, 5]的next-key,同時會加上(5, 6]的間隙鎖
next-key的加鎖原則是鎖定的是當前值和前面的範圍

注:一般生產都會設置讀已提交級別,這個時候爲了防止binlog和數據庫數據不一致需要設置binlog格式爲row,在代碼中使用鎖來解決併發問題。數據庫應該儘可能簡單,不管是語句,還是隔離級別,保證數據庫的性能。

參考

丁奇老師 MySQL45講

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章