MySQL事務與鎖機制
什麼是數據庫的事務
數據庫事務( transaction)是訪問並可能操作各種數據項的一個數據庫操作序列,這些操作要麼全部執行,要麼全部不執行,是一個不可分割的工作單位。事務由事務開始與事務結束之間執行的全部數據庫操作組成。
事務性質ACID
-
原子性(Atomicity)
事務中的全部操作在數據庫中是不可分割的,要麼全部完成,要麼全部不執行。
-
一致性(Consistency)
幾個並行執行的事務,其執行結果必須與按某一順序 串行執行的結果相一致。
事務前後數據的完整性必須保持一致。事務操作成功後,數據庫所處的狀態和他的業務規則是一致的,即數據不會被破壞。如A賬戶轉賬100元到B賬戶,不管操作成功與否,A和B賬戶的存款總額是不變的。
-
隔離性(Isolation)
事務的執行不受其他事務的干擾,事務執行的中間結果對其他事務必須是透明的。
-
持久性(Durability)
對於任意已提交事務,系統必須保證該事務對數據庫的改變不被丟失,即使數據庫出現故障。
開啓和關閉事務
show GLOBAL VARIABLES LIKE ‘autocommit’
可以查詢當前數據庫是否開啓自動提交功能
-- BEGIN 用於開啓事務,COMMIT,ROLLBACK用來結束事務,客戶端中斷也會結束事務
BEGIN; -- 開啓事務
UPDATE USER t set t.age=12 WHERE t.id=1;
COMMIT; -- 提交事務
ROLLBACK; -- 回滾事務
併發下引起有事務問題
select @@tx_isolation; – 查看當前數據的隔離級別
保證數據庫隔離級別爲
READ-UNCOMMITTED
否則事務將無法測試。
-
髒讀:一個事務讀取到另一個事務未提交的數據。
從圖中可以看到事務A在事務B提交修改數據之前讀到修改的數據。
-
幻讀:在同一事務中多次進行,由於其他提交事務所做的新增,每次返回不同的結果集。
從圖中可以看到事務A讀取到事務B新增的數據。
-
不可重複讀:在同一事務中多次進行,由於其他提交事務所做的修改和刪除,每次返回不同的結果集。
從圖中可以看到事務A讀取到事務B修改的數據。
事務隔離級別(SQL92)
如何解決讀一致性問題?
- 在讀取數據前,對其加鎖,阻止其他事務對數據進行修改——Lock Based Concurrency Control (LBCC)
- 生成一個數據請求時間點的一致性數據快照(Snapshout),並用這個快照來提供一定級別(語句級或事務級)的一致性讀取——Multi-Version Concurrency Control (MVCC)
鎖的粒度及區別
表鎖與行鎖的區別
鎖定粒度:表鎖 > 行鎖
加鎖效率:表鎖 > 行鎖
衝突概率:表鎖 > 行鎖
併發性能:表鎖 > 行鎖
MyISAM 鎖的粒度爲表鎖,InnoDB 鎖的數度爲行鎖
鎖的模式
-- 查詢鎖的使用情況
SELECT * FROM information_schema.INNODB_LOCKS t;
SELECT * from information_schema.INNODB_LOCK_WAITS t;
- 共享鎖(行鎖):Shared Locks
- 排它鎖(行鎖):Exclusive Locks
- 意向共享鎖(表鎖):Intention Shared Locks
- 意向排它鎖(表鎖):Intention Exclusive Locks
- 插入意向鎖(Insert Intention Locks)
- 自增鎖(AUTO-INC Locks)
鎖的算法
- 記錄鎖:(Record Locks)
- 間隙鎖:(Gap Locks)
- 臨鍵鎖:(Next-key Locks)
共享鎖(行鎖)
共享鎖(S鎖)又稱爲讀鎖,顧名思義,共享鎖就是多個事務對於同一數據可以共享一把鎖,都能訪問到數據,但是只能讀不能修改。
加鎖方式:select … Lock in share mode
釋放鎖:commit/rollback
-- TRANSACTION A
BEGIN;
SELECT * from `user` LOCK in SHARE MODE;
COMMIT;
-- TRANSACTION B
BEGIN;
SELECT * from `user` LOCK in SHARE MODE;
COMMIT;
事務A和事務B都可以查詢到數據因此共享鎖可以讀。
-- TRANSACTION A
BEGIN;
SELECT * from `user` LOCK in SHARE MODE;
-- COMMIT;
-- TRANSACTION B
BEGIN;
DELETE FROM `user`;
ROLLBACK;
可以看到事務B再執行的時候處於阻塞狀態只有事務A提交後,事務B纔會繼續執行。因此共享鎖只能讀不能修改。
排他鎖(行鎖)
排它鎖(X鎖)又稱爲寫鎖,排他鎖不能與其他鎖並存,如一個事務獲取了一個數據行的排他鎖,其他事務就不能再獲取該行的鎖(共享鎖,排他鎖),只有獲取了排他鎖的事務可以對數據行進行讀取和修改。
加鎖方式:
自動:delete/update/insert 默認加上X鎖
手動:select … FOR UPDATE
-- TRANSACTION A
BEGIN;
-- 自動加寫鎖
UPDATE `user` t set t.age=45 WHERE t.id=1
-- COMMIT;
-- TRANSACTION B
BEGIN;
-- 共享鎖
SELECT * from `user` LOCK IN SHARE MODE;
-- 手動加排他鎖
SELECT * FROM `user` FOR UPDATE;
-- 自動加排他鎖
DELETE FROM `user` WHERE id=1;
ROLLBACK;
可以看到事務B再執行的時候無論是共享鎖還是排他鎖都是阻塞狀態,因此排他鎖不能與其他鎖並存。
意向共享鎖/意向排它鎖(表鎖)
意向鎖是由數據引擎自己維護的,用戶無法手動操作意思鎖。
- 意向共離鎖(Intention Shared Locks,簡稱IS鎖)表示事務準備給數據行加入共享鎖,也就是說一個數據行加共享鎖前必須先取得該有的IS鎖。
- 意向排他鎖(Intention Exclusive Locks,簡稱IX鎖)表示事務準備給數據加入排他鎖,說明事務在一個數據行加排他鎖前必須先取處該表的IX鎖。
爲什麼意向鎖是表級別的?
當我們需要加一個排他鎖時,需要根據意向鎖去判斷表中有沒有數據行被鎖定(行鎖);
- 如果意向鎖是行鎖,則需要遍歷每一行數據去確認
- 如果意向鎖是表鎖,則只需要判斷一次即可知道有沒數據行被鎖定,提升性能。
共享鎖/排他鎖與意向共享鎖/意向排他鎖的兼容性關係
X | IX | S | IS | |
---|---|---|---|---|
X | 互斥 | 互斥 | 互斥 | 互斥 |
IX | 互斥 | 兼容 | 互斥 | 兼容 |
s | 互斥 | 互斥 | 兼容 | 兼容 |
IS | 互斥 | 兼容 | 兼容 | 兼容 |
這裏需要重點關注的是IX鎖和IX鎖是相互兼容的,這可能會造成死鎖。
插入意向鎖(行鎖)
插入意向鎖是一種特殊的間隙鎖,但不同於間隙鎖的是,該鎖只用於併發插入操作。如果說間隙鎖鎖住的是一個區間,那麼插入意向鎖鎖住的就是一個點。因而從這個角度來說,插入意向鎖確實是一種特殊的間隙鎖。與間隙鎖的另一個非常重要的差別是:儘管插入意向鎖也屬於間隙鎖,但兩個事務卻不能在同一時間內一個擁有間隙鎖,另一個擁有該間隙區間內的插入意向鎖(當然,插入意向鎖如果不在間隙鎖區間內則是可以的)。這裏我們再回顧一下共享鎖和排他鎖:共享鎖用於讀取操作,而排他鎖是用於更新或刪除操作。也就是說插入意向鎖、共享鎖和排他鎖涵蓋了常用的增刪改查四個動作。
自增鎖(表鎖)
AUTO-INC鎖是一種特殊的表級鎖,發生涉及AUTO_INCREMENT列的事務性插入操作時產生。
鎖的算法詳解
表如圖:
現有user表ID爲主鍵,其中四條記錄ID分別爲1,4,7,10
記錄鎖
唯一性索引(唯一/主鍵)等值查詢,精確匹配。數據存在
-- 鎖住:ID=4
BEGIN;
SELECT * from `user` t WHERE t.id=4 for update;
-- COMMIT;
-- 查看鎖有類型爲排他鎖(X),
SELECT * FROM information_schema.INNODB_LOCKS t;
間隙鎖
記錄不存在的情況,間隙鎖解決幻讀。
-- TRANSACTION A 鎖定(4,7)
BEGIN;
SELECT * from `user` t WHERE t.id>4 AND t.id<7 for update;
-- COMMIT;
-- TRANSACTION B 鎖定(4,7)
BEGIN;
SELECT * from `user` t WHERE t.id=6 for update;
-- COMMIT;
兩個事務鎖定的空間都 是(4,7)所以GAP鎖之間不衝突。
-- TRANSACTION A 鎖定(4,7)
BEGIN;
SELECT * from `user` t WHERE t.id>4 AND t.id<7 for update;
-- COMMIT
-- TRANSACTION B 插入id爲6的記錄
BEGIN;
INSERT INTO `user` VALUES(6,'six',22);
COMMIT ;
因爲事務A鎖定了(4,7)所以事務B處理等待狀態,只有事務A提交後事務B纔可以後續操作。
-- 查看鎖有類型爲排他鎖(X),
SELECT * FROM information_schema.INNODB_LOCKS t;
臨鍵鎖
範圍查詢,包含記錄和區間。
-- TRANSACTION A 鎖定(4,7] (7,10] 左開右閉
BEGIN;
SELECT * from `user` t WHERE t.id>5 AND t.id<9 for update;
-- COMMIT;
-- TRANSACTION B
BEGIN;
INSERT INTO `user` VALUES(6,'six',22);
COMMIT ;
因爲事務A鎖定了(4,7] (7,10]所以事務B處理等待狀態,只有事務A提交後事務B纔可以後續操作。
-- 查看鎖有類型爲排他鎖(X),
SELECT * FROM information_schema.INNODB_LOCKS t;
死鎖示例
CREATE TABLE `test` (
`id` int(20) NOT NULL,
`name` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
session1 | session2 |
---|---|
begin; | |
begin; | |
select * from test where id = 12 for update; 先請求IX鎖併成功獲取 再請求X鎖,但因行記錄不存在,故得到的是間隙鎖(10,15) |
|
select * from test where id = 13 for update; 先請求IX鎖併成功獲取 再請求X鎖,但因行記錄不存在,故得到的是間隙鎖(10,15) |
|
insert into test(id, name) values(12, “test1”); 請求插入意向鎖(12),因事務二已有間隙鎖,請求只能等待 |
|
鎖等待中 | insert into test(id, name) values(13, “test2”); 請求插入意向鎖(13),因事務一已有間隙鎖,請求只能等待 |
鎖等待解除 | 死鎖,session 2的事務被回滾 |
在場景中,因爲IX鎖是表鎖且IX鎖之間是兼容的,因而事務一和事務二都能同時獲取到IX鎖和間隙鎖。另外,需要說明的是,因爲我們的隔離級別是RR,且在請求X鎖的時候,查詢的對應記錄都不存在,因而返回的都是間隙鎖。接着事務一請求插入意向鎖,這時發現事務二已經獲取了一個區間間隙鎖,而且事務一請求的插入點在事務二的間隙鎖區間內,因而只能等待事務二釋放間隙鎖。這個時候事務二也請求插入意向鎖,該插入點同樣位於事務一已經獲取的間隙鎖的區間內,因而也不能獲取成功,不過這個時候,MySQL已經檢查到了死鎖,於是事務二被回滾,事務一提交成功。