前言
前面我們講了mysql數據庫底層的數據結構與算法、mysql性能優化篇一些內容。我們再來聊聊mysql的鎖與事務隔離級別,分上下兩篇,本篇重點講mysql的行鎖與事務隔離級別。
鎖定義
在數據庫中,除了傳統的計算資源(如CPU、RAM、I/O等)的爭用以外,數據也是一種供需要用戶共享的資源。如何保證數據并發訪問的一致性、有效性是所有數據庫必須解決的一個問題,鎖沖突也是影響數據庫并發訪問性能的一個重要因素。
鎖分類
- 從性能上分為樂觀鎖(用版本對比來實現)和 悲觀鎖;
- 從數據庫操作類型分為:讀鎖 和 寫鎖 (都屬于悲觀鎖)
- 讀鎖(共享鎖):針對同一份數據,多個讀操作可以同時進行而不會互相影響;
- 寫鎖(排它鎖):當前寫操作沒有完成之前,它會阻斷其它寫鎖和讀鎖。
- 從數據庫操作的粒度分為:表鎖 和 行鎖。
對于鎖深入的理解,可以查看《關于Java中鎖的理解》。
MySQL的鎖
-
行鎖(Record Locks)
-
間隙鎖(Gap Locks)
-
臨鍵鎖(Next-key Locks)
-
共享鎖/排他鎖(Shared and Exclusive Locks)
-
意向共享鎖/意向排他鎖(Intention Shared and Exclusive Locks)
-
插入意向鎖(Insert Intention Locks)
-
自增鎖(auto-inc Locks)
-
預測鎖,這種鎖主要用于存儲了空間數據的空間索引。
下篇來分別聊聊,本篇重點是行鎖以及事務隔離級別。
表鎖
每次操作鎖住整張表。
- 開銷小,加鎖快;
- 不會出現死鎖;
- 鎖粒度大,發生鎖沖突的概率最高;
- 并發度最低。
基本操作
示例表,如下:
# 建表SQLCREATE TABLE mylock ( id INT(11) NOT NULL AUTO_INCREMENT, NAME VARCHAR(20) DEFAULT NULL, PRIMARY KEY(id) ) ENGINE = MyISAM DEFAULT CHARSET = utf8; # 插入數據INSERT INTO`test`.`mylock`(`id`,`NAME`) VALUES ('1','a'); INSERT INTO`test`.`mylock`(`id`,`NAME`) VALUES ('2','b'); INSERT INTO`test`.`mylock`(`id`,`NAME`) VALUES ('3','c'); INSERT INTO`test`.`mylock`(`id`,`NAME`) VALUES ('4','d');復制代碼
- 手動增加表鎖
lock table 表名稱 read(write), 表名稱2 read(write);復制代碼
- 查看表上加過的鎖
show open tables;復制代碼
- 刪除表鎖
unlock tables;復制代碼
案例分析 — 加讀鎖
LOCK TABLE mylock read;復制代碼

當前 Session 和其他 seesion 都可以讀該表;
當前 session 中插入或者更新鎖定表都會報錯,其他 session 插入或者更新則會等待。

案例分析 — 加寫鎖
LOCK TABLE mylock WRITE;復制代碼

當前 session 對該表的增刪改查都沒有問題,其他 session 對該表的所有操作都會被阻塞 。
案例結論
MyISAM 在執行查詢語句(select)前,會自動給涉及的所有表加讀鎖;在執行增刪改查操作前,會自動給涉及的表加寫鎖。
- 對 MyISAM 表的讀操作(加讀鎖),不會阻塞其他進程同一表的讀請求,但會阻塞對同一表的寫請求。只有當讀鎖釋放后,才會執行其他進程的寫操作。
- 對 MyISAM 表的寫操作(加寫鎖),會阻塞其他進程對同一表的讀和寫操作,只有當寫鎖釋放后,才會執行其他進程的讀寫操作。
總結:讀鎖會阻塞寫,但不會阻塞讀;而寫鎖則會把讀和寫都阻塞。
行鎖
每次操作鎖住一行數據。
- 開銷大,加鎖慢;
- 會出現死鎖;
- 鎖定粒度最小,發生鎖沖突的概率最低;
- 并發度最高。
InnoDB 和 MyISAM 的最大不同點:
- 支持事務(TRANSACTION)
- 支持行級鎖
行鎖支持事務
事務(Transaction)及其 ACID 屬性
事務是由一組 SQL 語句組成的邏輯處理單元,事務具有以下四個屬性,通常簡稱為事務的 ACID屬性。
- 原子性(Atomicity):事務是一個原子操作單元,其對數據的修改,要么全部執行,要么全部不執行。
- 一致性(Consistent):在事務開始和完成時,數據都必須保持一致狀態。這意味著所有相關的數據規則都必須應用于事務的修改,以保持數據的完整性;事務結束時,所有的內部數據結構(如B+樹索引或雙向鏈表)也都必須是正確的。
- 隔離性(Lsolation):數據庫系統提供一定的隔離機制,保障事務在不受外部并發操作影響的“獨立”環境執行。這意味著事務處理過程中的中間狀態對外部是不可見的,反之亦然。
- 持久性(Durable):事務完成之后,它對于數據的修改是永久性的,即使出現系統故障也能保持。
并發事務處理帶來的問題
- 更新丟失(Lost Update)
當兩個或多個事務選擇同一行,然后基于最初選定的值更新該行值,由于每個事務都不知道其他事務的存在,就會發生丟失更新問題,最后的更新覆蓋來其他事務所做的更新。
- 臟讀(Dirty Reads)
一個事務正在對一條記錄做修改,在這個事務完成并提交前,這個條記錄的數據就處于不一致的狀態;這時另外一個事務也來讀取同一條記錄,如果不加控制,第二個事務讀取來這些“臟”數據,并據此做進一步的處理,就會產生未提交的數據依賴關系。這種現象被形象的叫做“臟讀”。
總結:事務A讀取到來事務B已經修改但尚未提交的數據,還在這個數據基礎上做來操作。此時,如果事務B回滾,事務A讀取的數據無效,不符合一致性要求。
- 不可重復讀(Non-Repeatable Reads)
一個事務在讀取某些數據后的某個時間,再次讀取以前讀過的數據,卻發現其讀出的數據已經發生來改變、或某些記錄已經被刪除了,這種現象就叫做“不可重復讀”。
總結:事務A讀取到了事務B已經提交的修改數據,不符合隔離性。
- 幻讀(Phantom Reads)
一個事務按相同的查詢條件重新讀取以前檢索過的數據,卻發現其他事務插入了滿足其查詢條件的新數據,這種現象就稱為“幻讀”。
總結:事務A讀取到了事務B提交的新增數據,不符合隔離性。
事務隔離級別
“臟讀”、“不可重復讀”、“幻讀”,其實都是數據庫讀一致性問題,必須由數據庫提供一定的事務隔離機制來解決。

數據庫的事務隔離越嚴格,并發副作用越小,但付出的代價也就越大,因為事務隔離實質上就是使事務在一定程度上“串行化”進行,這顯然與“并發”是矛盾的。
同時,不同應用對讀一致性和事務隔離程度的要求也是不同的,比如許多應用對“不可重復讀”和“幻讀” 并不敏感,可能更關系數據并發訪問的能力。
查看當前數據庫的事務隔離級別
show?variables?like?'tx_isolation';復制代碼

設置事務隔離級別
set?tx_isolation='REPEATABLE-READ';復制代碼
數據庫版本是5.7,隔離級別是Repeatable-Read(可重復讀),不同的數據庫版本和隔離級別對語句的執行結果影響很大。所以需要說明版本和隔離級別
行鎖與隔離級別案例分析
事務控制語句
- BEGIN 或 START TRANSACTION;顯式地開啟一個事務;
- COMMIT;也可以使用 COMMIT WORK,不過二者是等價的。COMMIT會提交事務,并使已對數據庫進行的所有修改稱為永久性的;
- ROLLBACK;有可以使用 ROLLBACK WORK,不過二者是等價的。回滾會結束用戶的事務,并撤銷正在進行的所有未提交的修改;
- SAVEPOINT identifier;SAVEPOINT允許在事務中創建一個保存點,一個事務中可以有多個SAVEPOINT;
- RELEASE SAVEPOINT identifier;刪除一個事務的保存點,當沒有指定的保存點時,執行該語句會拋出一個異常;
- ROLLBACK TO identifier;把事務回滾到標記點;
- SET TRANSACTION;用來設置事務的隔離級別。InnoDB存儲引擎提供事務的隔離級別有READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ和SERIALIZABLE。
事務處理方法
MYSQL 事務處理主要有兩種方法:
- 用 BEGIN, ROLLBACK, COMMIT來實現
- BEGIN 開始一個事務
- ROLLBACK 事務回滾
- COMMIT 事務確認
- 直接用 SET 來改變 MySQL 的自動提交模式:
- SET AUTOCOMMIT=0 禁止自動提交
- SET AUTOCOMMIT=1“ 開啟自動提交
示例表,如下:
CREATE?TABLE?`user`?(????`id`?INT?(11)?NOT?NULL?AUTO_INCREMENT,????`name`?VARCHAR?(255)?DEFAULT?NULL,????`balance`?INT?(11)?DEFAULT?NULL, ????PRIMARY?KEY?(`id`) )?ENGINE?=?INNODB?DEFAULT?CHARSET?=?utf8;INSERT?INTO?`test`.`user`?(`name`,`balance`)?VALUES?('zhangsan','450');INSERT?INTO?`test`.`user`?(`name`,`balance`)?VALUES?('lisi',?'16000');INSERT?INTO?`test`.`user`?(`name`,`balance`)?VALUES?('wangwu','2400');復制代碼
行鎖演示
一個 session 開啟事務更新不提交,另一個 seesion 更新同一條記錄會阻塞,更新不同記錄u會阻塞。


讀未提交
(1)打開一個客戶端A,并設置當前事務模式為 read uncommitted?(讀未提交),查詢表 user 的初始化值
set?tx_isolation='read-uncommitted';復制代碼

(2)在客戶端A的事務提交之前,打開另一個客戶端B,更新表 user

(3)這時,雖然客戶端B的事務還沒提交,但是在客戶端A就可以查詢到B已經更新的數據

(4)一旦客戶端B的事務因為某種原因回滾,所有的操作都將會被撤銷,那么客戶端A查詢到的數據其實就是臟數據。

(5)在客戶端A執行更新語句 update user set balance = balance – 50 where id = 1;?zhangsan 的 balance沒有變成350,居然是400,是不是很奇怪,數據不一致啊。如果你這么想就太天真了,在應用程序中,我們會用400-50=350,并不知道其他會話回滾了,要想解決這個問題可以采用讀已提交的隔離級別。

讀已提交
(1)打開一個客戶端A,并設置當前事務模式為 read committed?(讀已提交),查詢表 user 的所有記錄
set?tx_isolation='read-committed';復制代碼

(2)在客戶端A的事務提交之前,打開另一個客戶端B,更新表 user

(3)這時,客戶端B的事務還沒提交,客戶端A不能查詢到B已經更新的數據,解決了臟讀問題。

(4)客戶端B的事務提交

(5)客戶端A執行與上一步相同的查詢,結果與上一步不一致,即產生了不可重復讀的問題。

可重復讀
(1)打開一個客戶端A,并設置當前的事務模式為 repeatable read?,查詢表 user 的所有記錄。
set?tx_isolation='repeatable-read';復制代碼

(2)在客戶端A的事務提交之前,打開另一個客戶端B,更新表 user 并提交。

(3)在客戶端A查詢表 user 的所有記錄,與步驟(1)查詢結果一直,沒有出現不可重復讀的問題。

(4)在客戶端A,接著執行 update user set balance = balance – 50 where id = 1?, balance 沒有變成 400 – 50 = 350, zhangsan 的 balance 的值用的是步驟(2) 中的 350 來計算的,所以是300,數據的一致性倒是沒有被破壞。可重復讀的隔離級別下使用了 MVCC(multi-version concurrency control)機制,select 操作不會更新版本號,是快照讀(歷史版本);insert、update、delete 會更新版本號,是當前讀(當前版本)。
我們下篇來講 MVCC。

(5)重新打開客戶端B,插入一條新數據后提交。

(6)在客戶端A查詢表user 的所有記錄,沒有查出新增數據,所以沒有出現幻讀。

(7)驗證幻讀 在客戶端A執行 update user set balance = 8888 where id = 4;?,能更新成功,再次查詢到客戶端B新增的數據。
串行化
(1)打開一個客戶端A,并設置當前事務模式為 serializable?,查詢表 user 的初始值
set?tx_isolation='serializable';復制代碼

(2)打開一個客戶端B,并設置當前事務模式為 serializable?,插入一條記錄報錯,表被鎖了插入失敗,MySQL 中事務隔離級別為 serializable?時會鎖表,因此不會出現幻讀的情況,這種隔離級別并發性極低,開發中很少會用到。

案例結論
InnoDB 存儲引擎由于實現了行級鎖定,雖然在鎖定機制的實現方面所帶來的性能損耗可能比表級鎖定會更高一下,但是在整體并發處理能力方面要遠遠優于 MyISAM 的表級鎖定的。當系統并發量最高的時候,InnoDB 的整體性能和 MyISAM 相比就會有比較明顯的優勢。
但是,InnoDB 的行級鎖定同樣也有其脆弱的一面,當我們使用不當的時候,可能會讓 InnoDB 的整體性能表現不僅不能比 MyISAM 高,甚至可能會更差。
行鎖分析
通過檢查 innodb_row_lock 狀態變量來分析系統上的行鎖的競爭情況:
show?status?like?'innodb_row_lock%';復制代碼

對各個狀態量的說明如下:
- Innodb_row_lock_current_waits :當前正在等待鎖定的數量
- Innodb_row_lock_time :從系統啟動到現在鎖定總時間長度
- Innodb_row_lock_time_avg :每次等待所花平均時間
- Innodb_row_lock_time_max :從系統啟動到現在等待最長的一次所花時間
- Innodb_row_lock_waits :系統啟動后到現在總共等待的次數
對于這5個狀態變量,比較重要的主要是:
- Innodb_row_lock_time_avg (等待平均時長)
- Innodb_row_lock_waits (等待總次數)
- Innodb_row_lock_time(等待總時長)
尤其是當等待次數很高,而且每次等待時長也不小的時候,我們就需要分析系統 中為什么會有如此多的等待,然后根據分析結果著手制定優化計劃。
死鎖
set?tx_isolation='repeatable-read';復制代碼
Session_1執行:select?*?from?user?where?id=1?for?update; Session_2執行:select?*?from?user?where?id=2?for?update; Session_1執行:select?*?from?user?where?id=2?for?update; Session_2執行:select?*?from?user?where?id=1?for?update;復制代碼
查看近期死鎖日志信息:
show?engine?innodb?statusG;復制代碼
大多數情況mysql可以自動檢測死鎖并回滾產生死鎖的那個事務,但是有些情況 mysql沒法自動檢測死鎖
優化建議
- 盡可能讓所有數據檢索都通過索引來完成,避免無索引行鎖升級為表鎖;
- 合理設計索引,盡量縮小鎖的范圍;
- 盡可能減少檢索條件范圍,避免間隙鎖;
- 盡量控制事務大小,減少鎖定資源量和時間長度,涉及事務加鎖的sql盡量放在事務最后執行;
- 盡可能低級別事務隔離。
問答
- MySQL 默認級別是 repeatable-read,有什么辦法可以解決幻讀媽?
間隙鎖(Gap Lock)在某些情況下可以解決幻讀問題,它是 Innodb 在 可重復讀 提交下為解決幻讀問題時引入的鎖機制。要避免幻讀可以用間隙鎖在Session_1 下面執行 update user set name = ‘hjh’ where id > 10 and id
如:user 表有3條數據, id > 2 and id


- 無索引鎖會升級為表鎖,鎖主要是加在索引上,如果對非索引字段更新,行鎖可能會變變鎖。
客戶端A執行: update user set balance = 800 where name = ‘zhangsan’;?

客戶端B對該表任一行執行修改、刪除操作都會阻塞

InnoDB 的行鎖是針對索引加的鎖,不是針對記錄加的鎖。并且該索引不能失效,否則都會從行鎖升級為表鎖。
- 鎖定某一行還可以用 local in share mode(共享鎖)?和 for update(排它鎖)?,例如: select * from test_innodb_lock where a = 2 for update;?這樣其他 session 只能讀這行數據,修改則會被阻塞,直到鎖定行的 session 提交。