一、事務
數組庫的一組操作,要麼全部成功,要麼全部失敗
舉例:銀行轉賬 A賬戶向B賬戶轉100
- A賬戶餘額扣去100
- B賬戶餘額增加100
上述兩個操作要麼全部成功,要麼全部失敗,部分成功或失敗,數據就錯亂了
1. 事務的四大特徵
- 原子性:事務是原子性操作,要麼全部成功,要麼全部失敗
- 一致性:多個事務對數據庫操作會保證數據一致性
- 隔離性:併發時,事務之間互不影響
- 持久性:事務提交之後對數據庫的影響是持久性的,不會因爲數據庫宕機導致數據丟失
2. 併發事務帶來的問題
髒讀
在一個事務中,讀取了其他事務未提交的數據
不可重複讀
在一個事務中,同一行記錄被訪問了兩次卻得到了不同的結果
幻讀
在一個事務中,同一個範圍內的記錄被讀取時,其他事務向這個範圍添加了新的記錄。
前面髒讀和不可重複讀容易理解,幻讀稍微難一點
假設圖一test開始是空表,事物1第一次查詢得到空表,事物2在事物1執行期間插入一條數據,事物1第二次查詢由於滿足可重複讀,所以查詢結果依然爲空,但是事物1插入同樣一條數據,報重複主鍵錯誤
幻讀兩個要素:
- 可重複讀隔離級別下,快照讀看到的是一致性視圖,只有當前讀纔會產生幻讀
- 幻讀專指新插入發行,更新不算,將上述查詢後面加上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. 視圖
- 用查詢語句定義的虛擬表,在調用的時候執行查詢語句並生成結果。這是我們常說的視圖
- InnoDB 用來實現 MVCC 時用到的一致性讀視圖,即 consistent read view, 用於支持 RC(Read Committed,讀提交)和 RR(Repeatable Read,可重複讀)隔離級別的實現。沒有物理結構,僅僅是邏輯上用來定義在事務執行期間能看到什麼數據
3. 事務的起點
begin/start transaction 命令並不是一個事務的起點,在執行到它們之後的第一個操作 InnoDB 表的語句,事務才真正啓動, start transaction with consistent snapshot 該命令可以立即啓動事務
4. 隔離級別與視圖的關係
-
“讀未提交”隔離級別下直接返回記錄上的最新值,沒有視圖概念
-
“讀提交”隔離級別,這個視圖是在每個 SQL 語句開始執行的時候創建的
-
“可重複讀”隔離級別: 視圖是在事務啓動 (執行第一條語句或者使用特定命令) 時創建的,整個事務存在期間都用同一個視圖
-
“串行化”隔離級別下直接用加鎖的方式來避免並行訪問
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不支持行鎖。
重要概念: 兩階段加鎖
在數據庫更新時會給掃描的數據行加行鎖,更新結束不會立馬釋放行鎖,需要等到事務提交纔會釋放行鎖。
由於兩階段鎖的存在,所以在一個事務中,更新語句如果放在前面,會阻塞其它事務對錶的更新,影響併發。對於更新頻繁的語句儘量放在事務的靠後部分
死鎖
解決方案:
- 超時等待
- 發起死鎖檢測,主動回滾其中某個事務
超時等待的時間根據業務執行時間制定,太短誤傷,太長會影響併發量
死鎖檢測有額外負擔,在事務被鎖住,需要查看其依賴的線程是否被鎖住,一直循環,最後判斷出現死鎖,在多個線程併發修改同一行數據時,時間複雜度會變成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講