點贊再看,養成習慣,微信搜一搜【一角錢小助手】關注更多原創技術文章。
本文 GitHub org_hejianhui/JavaStudy 已收錄,有我的系列文章。
前言
- MySQL索引底層資料結構與算法
- MySQL性能優化原理-前篇
- MySQL性能優化-實踐篇1
- MySQL性能優化-實踐篇2
前面我們講了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)
- 預測鎖,這種鎖主要用于存儲了空間資料的空間索引。
下篇來分别聊聊,本篇重點是行鎖以及事務隔離級别。
表鎖
每次操作鎖住整張表。
- 開銷小,加鎖快;
- 不會出現死鎖;
- 鎖粒度大,發生鎖沖突的機率最高;
- 并發度最低。
基本操作
示例表,如下:
# 建表SQL
CREATE 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');
- 手動增加表鎖
- 檢視表上加過的鎖
- 删除表鎖
案例分析 — 加讀鎖
目前 session 和其他 seesion 都可以讀該表;
目前 session 中插入或者更新鎖定表都會報錯,其他 session 插入或者更新則會等待。
案例分析 — 加寫鎖
目前 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送出的新增資料,不符合隔離性。
事務隔離級别
“髒讀”、“不可重複讀”、“幻讀”,其實都是資料庫讀一緻性問題,必須由資料庫提供一定的事務隔離機制來解決。
資料庫的事務隔離越嚴格,并發副作用越小,但付出的代價也就越大,因為事務隔離實質上就是使事務在一定程度上“串行化”進行,這顯然與“并發”是沖突的。
同時,不同應用對讀一緻性和事務隔離程度的要求也是不同的,比如許多應用對“不可重複讀”和“幻讀” 并不敏感,可能更關系資料并發通路的能力。
檢視目前資料庫的事務隔離級别
設定事務隔離級别
資料庫版本是5.7,隔離級别是Repeatable-Read(可重複讀),不同的資料庫版本和隔離級别對語句的執行結果影響很大。是以需要說明版本和隔離級别
行鎖與隔離級别案例分析
事務控制語句
-
或BEGIN
;顯式地開啟一個事務;START TRANSACTION
-
;也可以使用COMMIT
,不過二者是等價的。COMMIT會送出事務,并使已對資料庫進行的所有修改稱為永久性的;COMMIT WORK
-
;有可以使用ROLLBACK
,不過二者是等價的。復原會結束使用者的事務,并撤銷正在進行的所有未送出的修改;ROLLBACK WORK
-
;SAVEPOINT允許在事務中建立一個儲存點,一個事務中可以有多個SAVEPOINT;SAVEPOINT identifier
-
;删除一個事務的儲存點,當沒有指定的儲存點時,執行該語句會抛出一個異常;RELEASE SAVEPOINT identifier
-
;把事務復原到标記點;ROLLBACK TO identifier
-
;用來設定事務的隔離級别。InnoDB存儲引擎提供事務的隔離級别有SET TRANSACTION
、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 的初始化值
(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 的所有記錄
(2)在用戶端A的事務送出之前,打開另一個用戶端B,更新表 user
(3)這時,用戶端B的事務還沒送出,用戶端A不能查詢到B已經更新的資料,解決了髒讀問題。
(4)用戶端B的事務送出
(5)用戶端A執行與上一步相同的查詢,結果與上一步不一緻,即産生了不可重複讀的問題。
可重複讀
(1)打開一個用戶端A,并設定目前的事務模式為
repeatable read
,查詢表 user 的所有記錄。
(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 的初始值
(2)打開一個用戶端B,并設定目前事務模式為
serializable
,插入一條記錄報錯,表被鎖了插入失敗,MySQL 中事務隔離級别為
serializable
時會鎖表,是以不會出現幻讀的情況,這種隔離級别并發性極低,開發中很少會用到。
案例結論
InnoDB 存儲引擎由于實作了行級鎖定,雖然在鎖定機制的實作方面所帶來的性能損耗可能比表級鎖定會更高一下,但是在整體并發處理能力方面要遠遠優于 MyISAM 的表級鎖定的。當系統并發量最高的時候,InnoDB 的整體性能和 MyISAM 相比就會有比較明顯的優勢。
但是,InnoDB 的行級鎖定同樣也有其脆弱的一面,當我們使用不當的時候,可能會讓 InnoDB 的整體性能表現不僅不能比 MyISAM 高,甚至可能會更差。
行鎖分析
通過檢查
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(等待總時長)
尤其是當等待次數很高,而且每次等待時長也不小的時候,我們就需要分析系統 中為什麼會有如此多的等待,然後根據分析結果着手制定優化計劃。
死鎖
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;
檢視近期死鎖日志資訊:
大多數情況mysql可以自動檢測死鎖并復原産生死鎖的那個事務,但是有些情況 mysql沒法自動檢測死鎖
優化建議
- 盡可能讓所有資料檢索都通過索引來完成,避免無索引行鎖更新為表鎖;
- 合理設計索引,盡量縮小鎖的範圍;
- 盡可能減少檢索條件範圍,避免間隙鎖;
- 盡量控制事務大小,減少鎖定資源量和時間長度,涉及事務加鎖的sql盡量放在事務最後執行;
- 盡可能低級别事務隔離。
問答
- MySQL 預設級别是
,有什麼辦法可以解決幻讀媽?repeatable-read
間隙鎖(Gap Lock)在某些情況下可以解決幻讀問題,它是 Innodb 在 可重複讀 送出下為解決幻讀問題時引入的鎖機制。要避免幻讀可以用間隙鎖在Session_1 下面執行
update user set name = 'hjh' where id > 10 and id <= 20;
,則其他 Session 沒法在這個範圍鎖包含的間隙裡插入或修改任何資料。
如:user 表有3條資料,
id > 2 and id <=3
會把第三條記錄鎖住,其他會話對則無法對第三條記錄做操作。
- 無索引鎖會更新為表鎖,鎖主要是加在索引上,如果對非索引字段更新,行鎖可能會變變鎖。
用戶端A執行:
update user set balance = 800 where name = 'zhangsan';
用戶端B對該表任一行執行修改、删除操作都會阻塞
InnoDB 的行鎖是針對索引加的鎖,不是針對記錄加的鎖。并且該索引不能失效,否則都會從行鎖更新為表鎖。
- 鎖定某一行還可以用
和local in share mode(共享鎖)
,例如:for update(排它鎖)
這樣其他 session 隻能讀這行資料,修改則會被阻塞,直到鎖定行的 session 送出。select * from test_innodb_lock where a = 2 for update;
部分圖檔來源于網絡,版權歸原作者,侵删。