Mysql事務和鎖的深入研究(親測權威版,看完後徹底搞懂)

本博文以思想指導實踐來驗證論點並加以總結歸納,切忌死記硬背。本博文的所有demo都很詳細,各位可以自行在自己的數據庫中做測試驗證。

1、入門準備工作

1.1、#建表語句

CREATE TABLE `student` (
    `id` int(16) NOT NULL AUTO_INCREMENT COMMENT '自增ID',
    `sno` VARCHAR(16) DEFAULT NULL COMMENT '學號',
    `sname` VARCHAR(64) DEFAULT NULL COMMENT '姓名',
    `company` VARCHAR(128) DEFAULT NULL COMMENT '公司',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='學生表';

1.2、查看數據庫的相關配置

#數據庫版本
SELECT VERSION();
#使用的存儲引擎:InnoDB
SHOW VARIABLES LIKE '%engine';
#數據庫隔離級別:可重複讀
SHOW GLOBAL VARIABLES LIKE '%tx_isolation';

1.3、常規事務的基礎知識:

#有事務?
update student set name='沐風111' where id=1;
#YES:若未手工開啓,則會話層面會自動的開啓事務 
SHOW VARIABLES LIKE 'Autocommit';
SET SESSION autocommit = ON;    #off 建議不要隨便修改設置


BEGIN; #START TRANSACTION 等價的手工開啓事務
UPDATE student SET sname='沐風222' where id=1;
ROLLBACK; 

BEGIN; #START TRANSACTION 等價的手工開啓事務
UPDATE student SET sname='沐風333' where id=1;
COMMIT; 

#結束事務 兩種方式 ROLLBACK/COMMIT

2、事務併發三大問題(髒讀、不可重複讀、幻讀)

其實都是數據庫讀一致性的問題,需要依靠數據庫提供一定的事務隔離機制解決。

3、髒讀、不可重複讀(虛讀)、幻讀的解釋:(其他很多博文都表述有誤

  • 髒讀:A事務可以讀取了B事務未提交的數據,一旦B事務做了回滾,那麼A事務拿到的就是錯誤的髒數據去執行接下來的業務,後果比較嚴重。
  • 不可重複讀:A事務可以讀取B事務update/delete操作已經提交的數據,即A事務多次重複讀取的可能會是不同的數據。
  • 幻讀:A多次事務讀取中,穿插了B事務執行的insert操作,導致多次查詢得到的數據行數變化了,像幻覺一樣。

注:不可重複讀(虛讀)和幻讀的差別: 
從總的結果來看, 似乎兩者都表現爲兩次讀取的結果不一致。但如果你從控制的角度來看, 兩者的區別就比較大: 
對於前者, 只需要鎖住滿足條件的記錄 
對於後者, 要鎖住滿足條件及其相近的記錄

4、Mysql隔離機制

參考SQL92的AMSI/ISO標準(很多數據庫廠商並未嚴格按照這些規範全盤實現):http://www.contrib.andrew.cmu.edu/~shadow/sql/sql1992.txt,截圖如下:

  • Read Uncommitted(讀取未提交內容)  未解決任何併發問題

在該隔離級別,所有事務都可以看到其他未提交事務的執行結果。讀取未提交的數據,也被稱之爲髒讀(Dirty Read)。

  • Read Committed(讀取提交內容)  解決髒讀問題

這是大多數數據庫系統的默認隔離級別(但不是MySQL默認的)。它滿足了隔離的簡單定義:一個事務只能看見已經提交事務所做的改變。這種隔離級別仍然會出現不可重複讀(虛讀)。

  • Repeatable Read(可重讀) 解決不可重複讀的問題 + (依靠MVCC)幻讀的問題

MySQL的默認事務隔離級別!它確保同一事務的多個實例在併發讀取數據時,會看到同樣的數據行。不過理論上,這會導致另一個棘手的問題:幻讀 (Phantom Read)。InnoDB和Falcon存儲引擎通過多版本併發控制(MVCC)機制解決了該問題。

  • Serializable(可串行化)  解決所有問題

這是最高的隔離級別,它通過強制事務排序,使之不可能相互衝突,從而解決幻讀問題。簡言之,它是在每個讀的數據行上加上共享鎖。在這個級別,可能導致大量的超時現象和鎖競爭,低效。

5、事務隔離級別的實現(LBCC和MVCC)

  • 基於鎖的併發控制LBCC(Lock Based Concurrency Control):

在讀取數據前,對其加鎖,阻止其他事務對數據進行修改。

  • 多版本併發控制MVCC(Muilti Version Concurrency Control):

生成一個數據請求時間點的一致性數據快照,並用這個快照來提供一定級別(語句級和事務級)的一致性讀取。

5.1、基於鎖的併發控制LBCC的詳解

鎖:用於管理不通事務對共享資源(行數據,表數據,頁數據使用的引擎太少不討論)的併發訪問

表鎖VS行鎖:
    鎖定粒度:表鎖>行鎖
    加鎖效率:表鎖>行鎖
    衝突概率:表鎖>行鎖
    併發性能:表鎖<行鎖

MyISAM:只支持表鎖
InnoDB:支持表鎖和行鎖(更強大通用)

1、鎖類型

  • 共享鎖(行鎖):Shared Locks

又稱爲讀鎖,簡稱S鎖(英文首字母),共享鎖就是多個事務對於同一數據可以共享一把鎖,都能訪問數據,但是隻能讀不能修改。

加鎖釋鎖方式:

select * from xx_db where id=1 Lock IN SHARE MODE;
commit/rollback

驗證:

#Session 1
BEGIN;
SELECT * FROM student WHERE id=1 LOCK IN SHARE MODE;
ROLLBACK;


#Session 2
BEGIN;
SELECT * FROM student WHERE id=1 LOCK IN SHARE MODE;
DELETE from student WHERE id=1;
ROLLBACK;
  • 排它鎖(行鎖):Exclusive Locks

又稱爲寫鎖,簡稱X鎖(英文第二個字母),排它鎖不能和其他鎖共存。如一個事務獲取了一個數據行的排它鎖,其他事務就不能再獲取該行的鎖(共享鎖和排它鎖),只有獲取了排它鎖的事務纔可以對數據行進行讀取和修改。

加鎖釋鎖方式:

#自動:delete/update/insert的DML操作會默認加上X鎖
#手動:select * from xx_db where id=1 FOR UPDATE;

commit/rollback

驗證:

#Session 1
BEGIN;
update student SET sname='沐風雨林555' WHERE id=1;
ROLLBACK;
COMMIT;


#Session 2
BEGIN;
SELECT * FROM student WHERE id=1 LOCK IN SHARE MODE;
SELECT * FROM student WHERE id=1 FOR UPDATE;
DELETE from student WHERE id=1;
ROLLBACK;
  • 意向共享鎖(表鎖):Intension Shared Locks

簡稱IS鎖,表示事務準備給數據行加入共享鎖,也就是說:一個數據行加共享鎖之前必須先取得該表的IS鎖。

  • 意向排它鎖(表鎖):Intension  Exclusive Locks

簡稱IX鎖,表示事務準備給數據行加入排他鎖,也就是說:一個數據行加排他鎖之前必須先取得該表的IX鎖。

IS和IX鎖的歸納:

(1)意向鎖是由數據庫引擎自己維護的,用戶無法手動操作意向鎖。

(2)既然都有行級別的鎖,爲何仍需要表級別的意向鎖?因爲表鎖加鎖效率高,行鎖併發度高,可以取得一個較好的組合效果。

(3)一個事務能給一張表加上鎖表的前提:沒有其他任何一個事務鎖定這張表中的任意一行。

(4)驗證

#Session 1
BEGIN;
#加上了排它鎖,說明一定有意向排它鎖了
SELECT * FROM student WHERE id=1 FOR UPDATE;
ROLLBACK;
COMMIT;


#Session 2
BEGIN;
#檢測到已有了被上了意向鎖的標識符,則加鎖失敗
LOCK TABLES student WRITE;
UNLOCK TABLES;
  • 其他鎖本文暫不討論

2、鎖住了什麼?

#t1的建表語句
#不使用索引
CREATE TABLE `t1` (
	`id` int(11) DEFAULT NULL,
	`name` VARCHAR(255) DEFAULT NULL COMMENT '姓名'
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='t1';



#Session 1
BEGIN;
#表中無顯示的主鍵索引,mysql就會幫忙新建一個隱藏的主鍵索引。where條件未命中索引導致整個表都被鎖定了
SELECT * FROM t1 WHERE id=1 FOR UPDATE;


#Session 2
BEGIN;
#會阻塞,因爲where條件未命中索引導致整張表都被鎖定了
SELECT * FROM t1 WHERE id=3 FOR UPDATE;
INSERT INTO `t1` (`id`, `name`) values(5, '5');
#t2的建表語句
#主鍵索引
CREATE TABLE `t2` (
	`id` int(11) NOT NULL,
	`name` VARCHAR(255) DEFAULT NULL COMMENT '姓名',
	PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='t2';



#Session 1
BEGIN;
SELECT * FROM t2 WHERE id=1 FOR UPDATE;
COMMIT;


#Session 2
#BEGIN;不影響,因爲會自動獲取事務
#失敗 同一行數據加排它鎖導致衝突了
SELECT * FROM t2 WHERE id=1 FOR UPDATE;
#成功 兩個事務的排它鎖,各自佔據的是不同的行的數據,所以不衝突
SELECT * FROM t2 WHERE id=4 FOR UPDATE;
#t3的建表語句
#唯一索引
CREATE TABLE `t3` (
	`id` int(11) NOT NULL,
	`name` VARCHAR(255) DEFAULT NULL COMMENT '姓名',
	PRIMARY KEY (`id`),
	UNIQUE KEY `uk_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='t3';



#Session 1
BEGIN;
SELECT * FROM t3 WHERE `name`='4' FOR UPDATE;
COMMIT;


#Session 2
#BEGIN;不影響,因爲會自動獲取事務
#失敗 同一行數據加排它鎖導致衝突了
SELECT * FROM t3 WHERE `name`='4' FOR UPDATE;
#失敗 不僅鎖住了name='4'對應的索引還鎖定了唯一索引對應的主鍵索引(可以參考B+Tree的輔助索引最終依賴於主鍵索引的關聯方式)
SELECT * FROM t3 WHERE id=4 FOR UPDATE;
#成功 兩個事務的排它鎖,各自佔據的是不同的行的數據,所以不衝突
SELECT * FROM t3 WHERE id=1 FOR UPDATE;

3、行鎖算法 

#初始化一下
DROP TABLE IF EXISTS `t2`;
CREATE TABLE `t2` (
  `id` int(11) NOT NULL,
  `name` varchar(255) DEFAULT NULL COMMENT '姓名',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='t2';


INSERT INTO `t2` (`id`, `name`) values(1, '1');
INSERT INTO `t2` (`id`, `name`) values(4, '4');
INSERT INTO `t2` (`id`, `name`) values(7, '7');
INSERT INTO `t2` (`id`, `name`) values(10, '10');

  • 記錄鎖 Record Lock

記錄鎖是鎖定記錄。唯一索引=精準匹配,退化成Record鎖。Record Lock:唯一性索引(唯一索引/主鍵索引)等值查詢,精準匹配。例如:select * from t2 where id = 4 for update;鎖住的就是:id=4。

  • 間隙鎖 Gap Lock

查詢記錄不存在的時候,臨鍵鎖就會退化成間隙鎖。

例如:select * from t2 where id >20 for update; 因爲沒有查到數據,所以鎖住的是(10,+\infty)。間隙鎖之間是不衝突的,也意味着兩個事務之間可以獲取一個完全相同的間隙鎖。

  • 臨鍵鎖 Next-key Lock 

InnoDB默認的行鎖算法,對索引進行範圍查詢時會觸發鎖定範圍加記錄,即:Next-key Locks  = Gap Lock + Record Lock。例如:select * from t2 where id > 3 and id < 9 for update;鎖住的就是(1,4]和(4,7]和(7,10)。解釋:鎖住的是當前條件命中的區間+下一個區間,鎖住下一個區間的好處在於塞滿了所有的where條件對應的左右空隙(即where條件能夠涉及到的所有的可能的數據區間)可以避免發生幻讀的發生。有Record Lock的另一個事務不能查詢也不能插入,Gap Lock區間另一個事務不能插入但是可以查詢(不阻塞,查出來空的結果,原因是上面提到的:兩個事務之間可以獲取一個完全相同的間隙鎖)。

此處提一下:找到滿足條件的記錄,但是記錄無效(InnoDB上刪除一條記錄,並不是真正意義上的物理刪除,而是將記錄標識爲刪除狀態。後續會由後臺的Purge操作進行回收,物理刪除。但是,刪除狀態的記錄會在索引中存放一段時間。),也會對記錄加next key鎖(同時鎖住記錄本身,以及記錄前後的Gap);

總結【必讀必品】:以上三大行鎖算法的本質在於,儘可能的避免同一個A事務的多次同樣的查詢條件出現不一致的查詢行數(即避免幻讀),也就意味着要儘可能的鎖住可能存在其他事務在A事務的臨界的空隙中插入數據的情況,不讓其他事務鑽空子。所以才能費盡心思的去考慮各種情況去鎖住對應的範圍區間。把握了本質也就對LBCC設計和原理了如指掌了,切忌死記硬背的那些算法規則,規則是人定的,出發點都是爲了解決問題,當你抓住了問題本質也就能夠想到該怎麼設計了。

  • 關於Mutex(保護內部的共享變量操作)和RWLock(又稱之爲Latch,保護內部的頁面讀取與修改)鎖,此文不做介紹。

利用鎖如何解決三大問題:

  1. 髒讀:因爲B事務的update操作會自動的有一個排他鎖,A事務只能拿到B事務已提交的結果,從而避免髒讀。
  2. 不可重複讀:A事務上第一次讀取操作會加上共享鎖,B事務就不能再去操作該數據。如果B事務的update條件坐落在間隙中雖然不阻塞但是沒有該數據行相當於沒執行。如果B事務的update條件坐落在A事務的共享鎖的數據上,則排他鎖的排他性生效直接阻塞。
  3. 幻讀:臨鍵鎖的作用,A事務的查詢會觸發臨鍵鎖,B事務就不能操作相鄰的間隙,保證了A事務的兩次查詢結果一致。
  4. 上述鎖住的區間都是where條件對應的索引的範圍,例如,where條件的是name,name主鍵id只會鎖住命中的記錄行,並不會鎖住主鍵id周圍的間隙。參考下圖的例子:
#初始化一下
DROP TABLE IF EXISTS `t5`;
CREATE TABLE `t5` (
  `id` int(11) NOT NULL,
	`age` int(5) DEFAULT NULL COMMENT '年齡',
  `name` varchar(255) DEFAULT NULL COMMENT '姓名',
  PRIMARY KEY (`id`),
	UNIQUE KEY `uk_age` (`age`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='t5';

INSERT INTO `t5` (`id`, `age`, `name`) values(1, 1, '1');
INSERT INTO `t5` (`id`, `age`, `name`) values(4, 4, '4');
INSERT INTO `t5` (`id`, `age`, `name`) values(7, 5, '7');
INSERT INTO `t5` (`id`, `age`, `name`) values(10, 9, '10');
INSERT INTO `t5` (`id`, `age`, `name`) values(18, 33, '10');

#==============================================
#Session 1
BEGIN;
UPDATE t5 SET name = 'xxxxx' WHERE age=9;

select * from t5 where age > 6 and age < 32 for update;
#(5,9],(9,33),命中9,對應的id=1
COMMIT;
ROLLBACK;


#Session 2
BIGINT;
#失敗,是因爲age=8被鎖住了
INSERT INTO `t5` (`id`, `age`, `name`) VALUES(8, 8, '8');
#成功,是因爲id=8雖然在id=9周圍,但是並沒有被鎖住id=9的間隙
INSERT INTO `t5` (`id`, `age`, `name`) VALUES(8, 99, '8');
ROLLBACK;

設計上的啓發:儘量避免使用範圍查詢,因爲範圍查詢越大,鎖住的區間越大,導致併發度越低。

5.2、多版本併發控制MVCC

先來一個demo暖暖場:

#Session 1
BEGIN;
SELECT * from t2 WHERE id=4 for UPDATE;


#Session 2
BEGIN;
#成功 讀取的是一個快照,MVCC機制
SELECT * from t2 WHERE id=4 ;
#失敗 排它鎖衝突
SELECT * from t2 WHERE id=4 FOR UPDATE;
COMMIT;

1、MVCC是爲了解決什麼問題?

MVCC是爲了實現數據庫的併發控制而設計的一種協議。從我們的直觀理解上來看,要實現數據庫的併發訪問控制,最簡單的做法就是加鎖訪問,即讀的時候不能寫(允許多個線程同時讀,即共享鎖,S鎖),寫的時候不能讀(一次最多隻能有一個線程對同一份數據進行寫操作,即排它鎖,X鎖)。這樣的加鎖訪問,其實並不算是真正的併發,或者說它只能實現併發的讀,因爲它最終實現的是讀寫串行化,這樣就大大降低了數據庫的讀寫性能。爲了提出比LBCC更優越的併發性能方法,MVCC便應運而生。

幾乎所有的RDBMS都支持MVCC。它的最大好處便是,讀不加鎖,讀寫不衝突。在MVCC中,讀操作可以分成兩類,快照讀(Snapshot read)和當前讀(current read)。

  • 快照讀

讀取的是記錄的可見版本(可能是歷史版本,即最新的數據可能正在被當前執行的事務併發修改),不用對該返回的記錄加鎖;在MySQL InnoDB中,簡單的select操作,如 select * from table where ? 都屬於快照讀;

  • 當前讀

讀取的是記錄的最新版本,並且對該返回的紀錄加鎖,保證其他事務不會併發修改這條記錄。屬於當前讀的包含以下操作:

#要加S鎖的場景
select * from table where ? lock in share mode; 
#要加X鎖的場景
select * from table where ? for update; 
insert, update, delete操作

   針對一條當前讀的SQL語句,InnoDB與MySQL Server的交互,是一條一條進行的,因此,加鎖也是一條一條進行的。先對一條滿足條件的記錄加鎖,返回給MySQL Server,做一些DML操作;然後再讀取下一條加鎖,直至讀取完畢。需要注意的是,以上需要加X鎖的都是當前讀,而普通的select(除了for update)都是快照讀,每次insert、update、delete之前都是會進行一次當前讀的,這個時候會上鎖,防止其他事務對某些行數據的修改,從而造成數據的不一致性。我們廣義上說的幻讀現象是通過MVCC解決的,意思是通過MVCC的快照讀可以使得事務返回相同的數據集。如下圖所示:

2、實現原理(以InnoDB的REPEATABLE READ隔離級別下爲例)

InnoDB的MVCC,是通過在每行記錄後面保存兩個隱藏的列來實現的,這兩個列分別保存了這個行的創建時間和刪除時間。這裏存儲的並不是實際的時間值,而是系統版本號(可以理解爲事務的ID),每開始一個新的事務,系統版本號就會自動遞增,事務開始時刻的系統版本號會作爲事務的ID.

核心在於:

InnoDB會根據以下a,b兩個條件檢查每行記錄,只有a,b同時滿足的記錄,才能返回作爲查詢結果。

  • a.InnoDB只會查找版本早於當前事務版本的數據行(也就是,行的系統版本號小於或等於事務的系統版本號),這樣可以確保事務讀取的行,要麼是在事務開始前已經存在的,要麼是事務自身插入或者修改過的。
  • b.行的刪除版本要麼未定義,要麼大於當前事務版本號,這可以確保事務讀取到的行,在事務開始之前未被刪除。

具體的實現細節可以參考:https://blog.csdn.net/whoamiyang/article/details/51901888 寫的比較通熟易懂,我就不再班門弄斧了。但是補充一點如下圖所示:

#Session 1 
BEGIN;
SELECT * from t2 WHERE id=4; #step1先執行完這句,先瞅瞅數據
SELECT * from t2 WHERE id=4; #step3執行完這句,效果仍然是老數據,因爲還未commit。
SELECT * from t2 WHERE id=4; #step5執行完這句,效果仍然是老數據,MVCC
SELECT * from t2 WHERE id=4 for UPDATE; #step6執行完這句,生效讀取到name=new4的新數據,排它鎖打破了原有的無鎖,使得mvcc降級成LBCC
COMMIT;
ROLLBACK;

#Session 2
BEGIN;
update `t2` SET `name`='new4' where id=4; #step2執行完這句
COMMIT; #step4執行完這句

如有疑問和批評歡迎隨時交流,純手碼辛苦不易,請支持原創勿轉載。

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