內容來之以下博客:
https://blog.csdn.net/qq_38238296/article/details/88362999
https://www.cnblogs.com/zhoulujun/p/11710318.html
https://zhuanlan.zhihu.com/p/48269420
https://segmentfault.com/a/1190000014133576
加鎖的目的:
數據庫是一個多用戶使用的共享資源。當多個用戶并發(fā)地存取數據時聊浅,在數據庫中就會產生多個事務同時存取同一數據的情況寥闪。若對并發(fā)操作不加控制就可能會讀取和存儲不正確的數據恕汇,破壞數據庫的一致性涌韩。鎖是用于管理對公共資源的并發(fā)控制衷旅。也就是說在并發(fā)的情況下在扰,會出現資源競爭胚股,所以需要加鎖。
加鎖解決了 多用戶環(huán)境下保證數據庫完整性和一致性士八。
使用鎖的對象是事務容燕,事務用來鎖定數據庫的對象是表、頁婚度、行蘸秘。并且一般鎖定的對象僅在事務commit或rollback后進行釋放(不同事務隔離級別釋放的時間可能不同)。
鎖的分類
鎖粒度
一蝗茁、行鎖
行級鎖是Mysql中鎖定粒度最細的一種鎖醋虏,表示只針對當前操作的行進行加鎖。行級鎖能大大減少數據庫操作的沖突哮翘。其加鎖粒度最小颈嚼,但加鎖的開銷也最大。有可能會出現死鎖的情況忍坷,出現死鎖的解決辦法就是必須有一方事務回滾或者同時回滾粘舟。 另外,行級鎖按照使用方式分為共享鎖和排他鎖佩研。
二柑肴、表鎖
表級鎖是mysql鎖中粒度最大的一種鎖,表示當前的操作對整張表加鎖旬薯,資源開銷比行鎖少晰骑,不會出現死鎖的情況,但是發(fā)生鎖沖突的概率很大绊序。被大部分的mysql引擎支持硕舆,MyISAM和InnoDB都支持表級鎖,但是InnoDB默認的是行級鎖骤公。
Mysql的表級別鎖分為兩類:元數據鎖(Metadata Lock抚官,MDL)、表鎖阶捆。
元數據鎖(Metadata Lock凌节,MDL)
元數據鎖(MDL) 不需要顯式使用,在訪問一個表的時候會被自動加上洒试。這個特性需要MySQL5.5版本以上才會支持倍奢,當對一個表做增刪改查的時候,該表會被加MDL讀鎖當對表做結構變更的時候垒棋,加MDL寫鎖
- MDL鎖規(guī)則:
1卒煞、讀鎖之間不互斥
2、讀寫鎖叼架、寫鎖之間是互斥的畔裕,為了保證表結構變更的安全性衣撬,所以如果要多線程對同一個表加字段等表結構操作,就會變成串行化扮饶,需要進行鎖等待
3淮韭、MDL的寫鎖優(yōu)先級比MDL讀鎖的優(yōu)先級高
4、MDL的鎖釋放必須要等到事務結束才會釋放
MDL鎖的例子
若沒有MDL鎖的保護贴届,則事務2可以直接執(zhí)行DDL操作靠粪,并且導致事務1出錯,5.1版本即是如此毫蚓。5.5版本加入MDL鎖就在于保護這種情況的發(fā)生占键,由于事務1開啟了查詢,那么獲得了MDL鎖元潘,鎖的模式為SHARED_READ(讀鎖模式)畔乙,事務2要執(zhí)行DDL,則需獲得EXCLUSIVE鎖(寫鎖)翩概,兩者互斥牲距,所以事務2需要等待。
頁鎖
頁級鎖是MySQL中鎖定粒度介于行級鎖和表級鎖中間的一種鎖钥庇。表級鎖速度快牍鞠,但沖突多,行級沖突少评姨,但速度慢难述。所以取了折衷的頁級,一次鎖定相鄰的一組記錄吐句。
兼容性
共享鎖||讀鎖||S 鎖(share lock)
共享鎖||讀鎖||S 鎖(share lock):其他事務可以讀胁后,但不能寫。允許一個事務去讀一行嗦枢,阻止其他事務獲得相同數據集的排他鎖攀芯。
例如:若事務T對數據對象A加上S鎖,則事務T可以讀A但不能修改A文虏,其他事務只能再對A加S鎖侣诺,而不能加X鎖,直到T釋放A上的S鎖择葡。這保證了其他事務可以讀A紧武,但在T釋放A上的S鎖之前不能對A做任何修改剃氧。
select ... lock in share mode;
排他鎖||寫鎖||X 鎖(exclusive)
其他事務不能讀取敏储,也不能寫。允許獲得排他鎖的事務更新數據朋鞍,阻止其他事務取得相同數據集的共享讀鎖和排他寫鎖已添。
例如:若事務T對數據對象A加上X鎖妥箕,事務T可以讀A也可以修改A,其他事務不能再對A加任何鎖更舞,直到T釋放A上的鎖畦幢。這保證了其他事務在T釋放A上的鎖之前不能再讀取和修改A。
select ... for update缆蝉;
鎖模式
一宇葱、意向鎖
1、意向共享鎖(IS Lock/intent share lock)
作用是表明事務想要獲得一張表中某幾行的共享鎖
事務打算給數據行加行共享鎖刊头,那么就必須得先取得該表的 IS 鎖黍瞧。
2、意向排他鎖||互斥鎖(IX Lock/intent exclusive lock)
作用是表明事務想要獲得一張表中某幾行的排他鎖
事務打算給數據行加行排他鎖原杂,那么就必須得先取得該表的 IX 鎖印颤。
意向鎖的加鎖方式
意向鎖是 InnoDB 自動加的, 不需用戶干預穿肄。
意向鎖有什么用年局?
主要作用是處理行鎖和表鎖之間的矛盾,能夠顯示“某個事務正在某一行上持有了鎖咸产,或者準備去持有鎖”矢否,當我們需要加一個排他鎖時,需要根據意向鎖去判斷表中有沒有數據行被鎖定
比如事務A要在一個表上加S鎖脑溢,如果表中的一行已被事務B加了X鎖兴喂,那么該鎖的申請也應被阻塞。如果表中的數據很多焚志,逐行檢查鎖標志的開銷將很大衣迷,系統(tǒng)的性能將會受到影響。為了解決這個問題酱酬,可以在表級上引入新的鎖類型來表示其所屬行的加鎖情況壶谒,這就引出了“意向鎖”的概念。
舉個例子膳沽,如果表中記錄1億汗菜,事務A把其中有幾條記錄上了行鎖了,這時事務B需要給這個表加表級鎖挑社,如果沒有意向鎖的話陨界,那就要去表中查找這一億條記錄是否上鎖了。如果存在意向鎖痛阻,那么假如事務A在更新一條記錄之前菌瘪,先加意向鎖,再加X鎖,事務B先檢查該表上是否存在意向鎖俏扩,存在的意向鎖是否與自己準備加的鎖沖突糜工,如果有沖突,則等待直到事務A釋放录淡,而無須逐條記錄去檢測捌木。事務B更新表時,其實無須知道到底哪一行被鎖了嫉戚,它只要知道反正有一行被鎖了就行了刨裆。
二、行鎖的算法
1彬檀、Record Lock(單行記錄)
單條索引上加鎖崔拥,record lock 永遠鎖的是索引,而非數據本身凤覆,如果innodb表中沒有索引链瓦,那么會自動創(chuàng)建一個隱藏的聚集索引,鎖住的就是這個聚集索引盯桦。當一條sql沒有走任何索引時慈俯,那么將會在每一條聚集索引后面加X鎖,這個類似于表鎖拥峦,但原理上和表鎖應該是完全不同的贴膘。
- 記錄鎖的條件
1、命中單行記錄并且命中的條件字段是唯一索引或者主索引略号;
update user_info set name=’張三’ where id=1;//這里的id是唯一索引,使用了Record Lock
Record Lock總是會去鎖住索引記錄刑峡,如果InnoDB存儲引擎表在建立的時候沒有設置任何一個索引,那么這時InnoDB存儲引擎會使用隱式的主鍵來進行鎖定玄柠。
2突梦、Gap Lock(間隙鎖)
間隙鎖是封鎖索引記錄中的間隔,并且包括最前面的索引記錄之前的范圍(-infinity, index)和最后面的索引之后的范圍(index,+infinity)羽利。它能防止間隙內有新數據被插入宫患。
- 產生間隙的條件
1、使用普通索引鎖定这弧;
2娃闲、使用多列唯一索引;
3匾浪、使用唯一索引鎖定多行記錄皇帮。
我們先理解什么是間隙,如
CREATE TABLE `test` (
`id` int(1) NOT NULL AUTO_INCREMENT,
`name` varchar(8) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `test` VALUES ('1', '小羅');
INSERT INTO `test` VALUES ('5', '小黃');
INSERT INTO `test` VALUES ('7', '小明');
INSERT INTO `test` VALUES ('11', '小紅');
上面數據的間隙就是:
- (-infinity, 1)
- (1, 5)
- (5, 7)
- (7, 11)
- (11, +infinity)
在test中使用間隙鎖的話蛋辈,鎖的就是上面的范圍
基于上面的表属拾,看看下面的例子:
BEGIN;
/* 查詢 id 在 7 - 11 范圍的數據并加記錄鎖 */
SELECT * FROM `test` WHERE `id` BETWEEN 5 AND 7 FOR UPDATE;
/* 延遲30秒執(zhí)行,防止鎖釋放 */
SELECT SLEEP(30);
產生的間隙鎖范圍是,會鎖住 (5, 7)捌年、(7, 11) 這兩個區(qū)間。
BEGIN;
SELECT * FROM `test` WHERE `id` = 3 FOR UPDATE;
SELECT SLEEP(30);
由于3的記錄并不存在挂洛,所以也沒有單行記錄可言礼预,也會產生間隙鎖,范圍是(1,3)虏劲、(3,5)
BEGIN;
SELECT * FROM `test` WHERE `id` > 5 FOR UPDATE;
SELECT SLEEP(30);
會產生間隙鎖托酸,范圍是(5,+infinity)
理解了間隙,我們看看下面的例子:
mysql> select * from product_copy;
+----+--------+-------+-----+
| id | name | price | num |
+----+--------+-------+-----+
| 1 | 伊利 | 68 | 1 |
| 2 | 蒙牛 | 88 | 1 |
| 6 | tom | 2788 | 3 |
| 10 | 優(yōu)衣庫 | 488 | 4 |
+----+--------+-------+-----+
其中id為主鍵 num為普通索引
窗口A:
mysql> select * from product_copy where num=3 for update;
+----+------+-------+-----+
| id | name | price | num |
+----+------+-------+-----+
| 6 | tom | 2788 | 3 |
+----+------+-------+-----+
1 row in set
窗口B:
mysql> insert into product_copy values(5,'kris',1888,2);
這里會等待 直到窗口A commit才會顯示下面結果
Query OK, 1 row affected
但是下面是不需要等待的
mysql> update product_copy set price=price+100 where num=1;
Query OK, 2 rows affected
Rows matched: 2 Changed: 2 Warnings: 0
mysql> insert into product_copy values(5,'kris',1888,5);
Query OK, 1 row affected
通過上面的例子可以看出Gap 鎖的作用是在的范圍是(1,3)U(3,4)柒巫。
但是要記住励堡,使用主鍵索引/唯一索引條件的單行記錄不會使用間隙鎖,會使用記錄鎖堡掏,但是如果查詢出來的是多行記錄应结,使用的就是間隙鎖
下面的例子,id是主鍵索引泉唁,使用的是記錄鎖
窗口A:
mysql> select * from product_copy where id=6 for update;
+----+------+-------+-----+
| id | name | price | num |
+----+------+-------+-----+
| 6 | tom | 2788 | 3 |
+----+------+-------+-----+
窗口B:并不會發(fā)生等待
mysql> insert into product_copy values(5,'kris',1888,3);
Query OK, 1 row affected
窗口A:
mysql> select * from product_copy where id>6 for update;
+----+------+-------+-----+
| id | name | price | num |
+----+------+-------+-----+
| 10 | 優(yōu)衣庫 | 488 | 4 |
+----+------+-------+-----+
窗口B:會發(fā)生等待
mysql> insert into product_copy values(9,'kris',1888,3);
Query OK, 1 row affected
從上面的例子可以看到鹅龄,第一條sql是使用了主鍵索引的單行記錄,使用了記錄鎖亭畜,第二個sql即使使用了主鍵索引扮休,但是查詢的數據多于一條,使用的間隙鎖拴鸵,間隙鎖鎖的范圍是(6,+infinity)
牢記:記錄鎖是加在索引上的鎖玷坠,間隙鎖是加在索引之間的。
3劲藐、Next-Key Lock(Record Lock + Gap Lock八堡,臨鍵鎖)
臨鍵鎖,是記錄鎖與間隙鎖的組合聘芜,它的封鎖范圍秕重,既鎖住記錄本身還鎖住索引之間的間隙。
注:臨鍵鎖的主要目的厉膀,也是為了避免幻讀(Phantom Read)溶耘。如果把事務的隔離級別降級為RC,臨鍵鎖則也會失效服鹅。
一、SELECT ... FROM ... LOCK IN SHARE MODE
- 在所有索引掃描范圍的索引記錄上加上共享的next key鎖;如果是唯一索引企软,就是在相應記錄上加record lock庐扫。這些被共享lock住的行無法進行update/delete。
- 允許其它事務對這些記錄再加SHARE鎖
- 如果沒有使用到索引,則鎖住全表(表級的排他鎖)形庭,無法進行insert/update/delete铅辞。
二、SELECT ... FROM ... FOR UPDATE
- 在所有索引掃描范圍的索引記錄上加上排他的next key鎖萨醒。如果是唯一索引斟珊,就是在相應記錄上加record lock。
- 如果沒有利用到索引將鎖住全表(表級的排他鎖)富纸,其它事務無法進行insert/update/delete操作囤踩。
三、UPDATE ... WHERE ...
- 語句在所有索引掃描范圍的索引記錄上加上排他的next key鎖晓褪。如果是唯一索引堵漱,就是在相應記錄上加record lock。
- 如果沒有利用到索引將鎖住全表(表級的排他鎖),其它事務無法進行其它的insert/update/delete操作涣仿。
四勤庐、DELETE FROM ... WHERE ...
- 語句在所有索引掃描范圍的索引記錄上加上排他的next key鎖。如果是唯一索引好港,就是在相應記錄上加record lock埃元。
- 如果沒有利用到索引將鎖住全表(表級的排他鎖),其它事務無法進行其它的insert/update/delete操作。
五媚狰、INSERT
- 在插入的記錄上加一把排他鎖捉邢,這個鎖是一個record lock甚纲,并不是next-key 鎖稳诚,因此就沒有gap 鎖棕所,他將不會阻止其他會話在該條記錄之前的gap插入記錄。
next-key鎖的范圍
看下面的例子:
CREATE TABLE `test` (
`id` int(1) NOT NULL AUTO_INCREMENT,
`name` varchar(8) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `test` VALUES ('1', '小羅');
INSERT INTO `test` VALUES ('5', '小黃');
INSERT INTO `test` VALUES ('7', '小明');
INSERT INTO `test` VALUES ('11', '小紅');
上面數據的間隙就是:
- (-infinity, 1]
- (1, 5]
- (5, 7]
- (7, 11]
- (11, +infinity]
記得左開右閉
MVCC辨宠,多版本的并發(fā)控制遗锣,Multi-Version Concurrency Control。
MVCC的目的就是多版本并發(fā)控制嗤形,在數據庫中的實現精偿,就是為了解決讀寫沖突。使用鎖和鎖協(xié)議來實現相應的隔離級別來進行并發(fā)控制會因為鎖會造成事務阻塞赋兵。而多版本并發(fā)控制使得對同一行記錄做讀寫的事務之間不用相互阻塞等待笔咽,提高了事務的并發(fā)能力,可以認為MVCC是一種解決讀寫阻塞等待的行級鎖霹期。
MVCC的數據庫表中每一行數據都可能存在多個版本叶组,對數據庫的任何修改的提交都不會直接覆蓋之前的數據,而是產生一個新的版本與老版本共存历造,通過讀寫數據時讀不同的版本來避免加鎖阻塞
- 1甩十、MVCC只支持(已提交讀)和(可重復讀)隔離級別船庇。
- 2、MVCC能解決臟讀侣监、不可重復讀問題鸭轮,不能解決幻讀問題。
- 3橄霉、MVCC是用來解決讀寫操作之間的阻塞問題窃爷。
隱式字段
每行記錄除了我們自定義的字段外,還有數據庫隱式定義的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等字段
- DB_TRX_ID:數據行版本號:大小為6byte酪劫,記錄最近修改(修改/插入)事務ID吞鸭,記錄創(chuàng)建這條記錄/最后一次修改該記錄的事務ID
- DB_ROLL_PTR:刪除版本號:大小為7byte寺董,記錄回滾指針覆糟,指向當前記錄行的undo log信息(指向該數據的前一個版本數據)
- DB_ROW_ID:行數據隱式id:大小為6byte,隱含的自增ID(隱藏主鍵)遮咖,如果數據表沒有主鍵滩字,InnoDB會自動以DB_ROW_ID
產生一個聚簇索引
ReadView
read view是讀視圖,其實就相當于一種快照御吞,里面記錄了系統(tǒng)中當前活躍事務的ID以及相關信息麦箍,主要用途是用來做可見性判斷,判斷當前事務是否有資格訪問該行數據陶珠。read view有多個變量:
- trx_ids: 它里面的trx_ids變量存儲了活躍事務列表挟裂,也就是Read View開始創(chuàng)建時其他未提交的活躍事務的ID列表。例如事務A在創(chuàng)建read view(快照)時揍诽,數據庫中事務B和事務C還沒提交或者回滾結束事務诀蓉,此時trx_ids就會將事務B和事務C的事務ID記錄下來。
假設當前事務生成了一個ReadView暑脆,trx_ids列表里的事務id為[60,100]渠啤。
1、如果你要訪問的記錄版本的事務id為50添吗,比當前列表最小的id 60還小沥曹,那說明這個事務在ReadView生成之前就提交了,所以對當前活動的事務來說是可訪問的碟联。
2妓美、如果你要訪問的記錄版本的事務id為70,發(fā)現此事務在列表id最大值和最小值之間,那就再判斷一下 70 這個id是否在列表內鲤孵,如果在那就說明此事務還未提交部脚,所以版本不能被訪問。如果不在那說明事務已經提交裤纹,所以版本可以被訪問委刘。
3丧没、如果你要訪問的記錄版本的事務id為110,那比事務列表最大id100都大锡移,那說明這個版本是在ReadView生成之后才發(fā)生的呕童,所以不能被訪問。
Undo log
Undo log中存儲的是老版本數據淆珊,當一個事務需要讀取記錄行時夺饲,如果當前記錄行不可見,可以通過回滾指針順著undo log鏈找到滿足其可見性條件的記錄行版本施符。
在InnoDB里往声,undo log分為如下兩類:
①insert undo log : 事務對insert新記錄時產生的undo log, 只在事務回滾時需要, 并且在事務提交后就可以立即丟棄。
②update undo log : 事務對記錄進行delete和update操作時產生的undo log戳吝,在事務回滾時需要
實際還有一個刪除flag隱藏字段, 既記錄被更新或刪除并不代表真的刪除浩销,而是刪除flag變了
插入:獲取最新的事務版本號n,保存n到對應行的行版本號
刪除:獲取最新的事務版本號n,保存到對應行的刪除版本號
修改:變?yōu)閕nsert和delete操作的組合听哭,先獲取最新的事務版本號n慢洋,然后進行數據行拷貝,插入拷貝的數據陆盘,保存n到新插入數據行行版本號的字段中普筹,然后保存n到舊的數據行的刪除版本號字段中
查詢:獲取最新的事務版本號n,查詢行版本號小于或者等于n的行數據,防止讀到其他事務提交的數據
MVCC實現過程原理主要的原理:
參考:https://baijiahao.baidu.com/s?id=1629409989970483292&wfr=spider&for=pc
版本記錄都是去版本鏈里面找的隘马,然后根據不同隔離級別生成的ReadView就會有所不同
-
例如:在一個讀已提交或者是重復讀的級別事務中
有一個事務id為100的事務太防,修改了name,使得的name等于小明2,但是事務還沒提交酸员。則此時的版本鏈如下:
這時候之前那個select事務又執(zhí)行了一次查詢,要查詢id為1的記錄屁置。這時候會發(fā)生兩種情況
- 1焊夸、已提交讀隔離級別
會重新一個生成一個ReadView,那你的活動事務列表中的值就變了蓝角,變成了[110]阱穗,通過版本鏈查trx_id對比饭冬,查到的只能是小明2。 - 2揪阶、可重復讀隔離級別
ReadView還是第一次select時候生成的ReadView,也就是列表的值還是[100]昌抠。所以select的結果是小明1。所以第二次select結果和第一次一樣鲁僚,所以叫可重復讀炊苫!
也就是說已提交讀隔離級別下的事務在每次查詢的開始都會生成一個獨立的ReadView,而可重復讀隔離級別則在第一次讀的時候生成一個ReadView,之后的讀都復用之前的ReadView冰沙。
當前讀與快照讀
- 1侨艾、當前讀:
即加鎖讀,讀取記錄的最新版本拓挥,會加鎖保證其他并發(fā)事務不能修改當前記錄唠梨,直至獲取鎖的事務釋放鎖;
使用當前讀的操作主要包括:顯式加鎖的讀操作與插入/更新/刪除等寫操作撞叽,如下所示:
select * from table where ? lock in share mode;
select * from table where ? for update;
insert into table values (…);
update table set ? where ?;
delete from table where ?;
- 2姻成、快照讀:
即不加鎖讀插龄,讀取記錄的快照版本而非最新版本愿棋,通過MVCC實現;
當前讀
需要特別注意的是在MVCC下的可重復讀在讀操作是防止了幻讀均牢,讀操作下完全就是按照ReadView進行的快照讀糠雨。但是對于會對數據的操作例如:
select * from .... where ... for update
select * from .... where ... lock in share mode
update .... set .. where ...
delete from. . where ..
都是采用當前讀的模式。在執(zhí)行這幾個操作時會讀取最新的記錄徘跪,即使是別的事務提交的數據也可以查詢到甘邀。假設要update一條記錄,但是在另一個事務中已經delete掉這條數據并且commit了垮庐,如果update就會產生沖突松邪,所以在update的時候需要知道最新的數據。如果事務中都使用快照讀哨查,那么就不會產生幻讀現象逗抑,但是快照讀和當前讀混用就會產生幻讀。
解決幻讀的方案
事務A 按照一定條件進行數據讀取寒亥, 期間事務B 插入了相同搜索條件的新數據邮府,事務A再次按照原先條件進行讀取時,發(fā)現了事務B 新插入的數據 稱為幻讀
一溉奕、mvcc只進行快照讀(性能好)
在可重復讀的隔離級別下只要不發(fā)生當前讀就能避免幻讀二褂傀、next-key 鎖 (性能差)
由于next-key 鎖是記錄鎖與間隙鎖的組合,它的封鎖范圍加勤,既鎖住記錄本身還鎖住索引之間的間隙仙辟。這樣將當前數據行與上一條數據和下一條數據之間的間隙鎖定同波,保證此范圍內讀取的數據是一致的。
select * from T where number = 1 for update;
select * from T where number = 1 lock in share mode;