內(nèi)容來之以下博客:
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
加鎖的目的:
數(shù)據(jù)庫是一個多用戶使用的共享資源。當(dāng)多個用戶并發(fā)地存取數(shù)據(jù)時,在數(shù)據(jù)庫中就會產(chǎn)生多個事務(wù)同時存取同一數(shù)據(jù)的情況金闽。若對并發(fā)操作不加控制就可能會讀取和存儲不正確的數(shù)據(jù),破壞數(shù)據(jù)庫的一致性。鎖是用于管理對公共資源的并發(fā)控制。也就是說在并發(fā)的情況下胸嘁,會出現(xiàn)資源競爭,所以需要加鎖凉逛。
加鎖解決了 多用戶環(huán)境下保證數(shù)據(jù)庫完整性和一致性性宏。
Lock的對象是事務(wù),用來鎖定的是數(shù)據(jù)庫中的對象状飞,如表毫胜、頁蝌借、行。并且一般lock的對象僅在事務(wù)commit或rollback后進(jìn)行釋放(不同事務(wù)隔離級別釋放的時間可能不同)指蚁。
鎖的分類
鎖粒度
一、行鎖
行級鎖是Mysql中鎖定粒度最細(xì)的一種鎖自晰,表示只針對當(dāng)前操作的行進(jìn)行加鎖凝化。行級鎖能大大減少數(shù)據(jù)庫操作的沖突。其加鎖粒度最小酬荞,但加鎖的開銷也最大搓劫。有可能會出現(xiàn)死鎖的情況。 行級鎖按照使用方式分為共享鎖和排他鎖混巧。
二枪向、表鎖
表級鎖是mysql鎖中粒度最大的一種鎖,表示當(dāng)前的操作對整張表加鎖咧党,資源開銷比行鎖少秘蛔,不會出現(xiàn)死鎖的情況,但是發(fā)生鎖沖突的概率很大傍衡。被大部分的mysql引擎支持深员,MyISAM和InnoDB都支持表級鎖,但是InnoDB默認(rèn)的是行級鎖蛙埂。
Mysql的表級別鎖分為兩類:元數(shù)據(jù)鎖(Metadata Lock倦畅,MDL)、表鎖绣的。
元數(shù)據(jù)鎖(Metadata Lock叠赐,MDL)
元數(shù)據(jù)鎖(MDL) 不需要顯式使用,在訪問一個表的時候會被自動加上屡江。這個特性需要MySQL5.5版本以上才會支持芭概,當(dāng)對一個表做增刪改查的時候,該表會被加MDL讀鎖當(dāng)對表做結(jié)構(gòu)變更的時候盼理,加MDL寫鎖
MDL鎖規(guī)則:
1谈山、讀鎖之間不互斥
2、讀寫鎖宏怔、寫鎖之間是互斥的奏路,為了保證表結(jié)構(gòu)變更的安全性,所以如果要多線程對同一個表加字段等表結(jié)構(gòu)操作臊诊,就會變成串行化鸽粉,需要進(jìn)行鎖等待
3、MDL的寫鎖優(yōu)先級比MDL讀鎖的優(yōu)先級
4抓艳、MDL的鎖釋放必須要等到事務(wù)結(jié)束才會釋放
MDL鎖的例子
若沒有MDL鎖的保護(hù)触机,則事務(wù)2可以直接執(zhí)行DDL操作,并且導(dǎo)致事務(wù)1出錯,5.1版本即是如此儡首。5.5版本加入MDL鎖就在于保護(hù)這種情況的發(fā)生片任,由于事務(wù)1開啟了查詢,那么獲得了MDL鎖蔬胯,鎖的模式為SHARED_READ对供,事務(wù)2要執(zhí)行DDL,則需獲得EXCLUSIVE鎖氛濒,兩者互斥产场,所以事務(wù)2需要等待。
頁鎖
頁級鎖是MySQL中鎖定粒度介于行級鎖和表級鎖中間的一種鎖舞竿。表級鎖速度快京景,但沖突多,行級沖突少骗奖,但速度慢确徙。所以取了折衷的頁級,一次鎖定相鄰的一組記錄执桌。BDB支持頁級鎖
兼容性
共享鎖||讀鎖||S 鎖(share lock)
共享鎖||讀鎖||S 鎖(share lock):其他事務(wù)可以讀米愿,但不能寫。允許一個事務(wù)去讀一行鼻吮,阻止其他事務(wù)獲得相同數(shù)據(jù)集的排他鎖育苟。
例如:若事務(wù)T對數(shù)據(jù)對象A加上S鎖,則事務(wù)T可以讀A但不能修改A椎木,其他事務(wù)只能再對A加S鎖违柏,而不能加X鎖,直到T釋放A上的S鎖香椎。這保證了其他事務(wù)可以讀A漱竖,但在T釋放A上的S鎖之前不能對A做任何修改。
select...lockinshare mode;
排他鎖||寫鎖||X 鎖(exclusive)
其他事務(wù)不能讀取畜伐,也不能寫馍惹。允許獲得排他鎖的事務(wù)更新數(shù)據(jù),阻止其他事務(wù)取得相同數(shù)據(jù)集的共享讀鎖和排他寫鎖玛界。
例如:若事務(wù)T對數(shù)據(jù)對象A加上X鎖万矾,事務(wù)T可以讀A也可以修改A,其他事務(wù)不能再對A加任何鎖慎框,直到T釋放A上的鎖良狈。這保證了其他事務(wù)在T釋放A上的鎖之前不能再讀取和修改A。
select...forupdate笨枯;
鎖模式
一薪丁、意向鎖
1遇西、意向共享鎖(IS Lock/intent share lock)
事務(wù)想要獲得一張表中某幾行的共享鎖
事務(wù)務(wù)打算給數(shù)據(jù)行加行共享鎖,事務(wù)在給一個數(shù)據(jù)行加共享鎖前必須先取得該表的 IS 鎖
2严嗜、意向排他鎖||互斥鎖(IX Lock/intent exclusive lock)
事務(wù)想要獲得一張表中某幾行的排他鎖
事務(wù)打算給數(shù)據(jù)行加行排他鎖粱檀,事務(wù)在給一個數(shù)據(jù)行加排他鎖前必須先取得該表的 IX 鎖。
意向鎖的加鎖方式
意向鎖是 InnoDB 自動加的漫玄, 不需用戶干預(yù)梧税。
意向鎖有什么用?
主要作用是處理行鎖和表鎖之間的矛盾称近,能夠顯示“某個事務(wù)正在某一行上持有了鎖,或者準(zhǔn)備去持有鎖”
當(dāng)我們需要加一個排他鎖時哮塞,需要根據(jù)意向鎖去判斷表中有沒有數(shù)據(jù)行被鎖定
比如事務(wù)A要在一個表上加S鎖刨秆,如果表中的一行已被事務(wù)B加了X鎖,那么該鎖的申請也應(yīng)被阻塞忆畅。如果表中的數(shù)據(jù)很多衡未,逐行檢查鎖標(biāo)志的開銷將很大,系統(tǒng)的性能將會受到影響家凯。為了解決這個問題缓醋,可以在表級上引入新的鎖類型來表示其所屬行的加鎖情況,這就引出了“意向鎖”的概念绊诲。
舉個例子送粱,如果表中記錄1億,事務(wù)A把其中有幾條記錄上了行鎖了掂之,這時事務(wù)B需要給這個表加表級鎖抗俄,如果沒有意向鎖的話,那就要去表中查找這一億條記錄是否上鎖了世舰。如果存在意向鎖动雹,那么假如事務(wù)A在更新一條記錄之前,先加意向鎖跟压,再加X鎖胰蝠,事務(wù)B先檢查該表上是否存在意向鎖,存在的意向鎖是否與自己準(zhǔn)備加的鎖沖突震蒋,如果有沖突茸塞,則等待直到事務(wù)A釋放,而無須逐條記錄去檢測查剖。事務(wù)B更新表時翔横,其實(shí)無須知道到底哪一行被鎖了,它只要知道反正有一行被鎖了就行了梗搅。
二禾唁、行鎖的算法
1效览、Record Lock(單行記錄)
單條索引上加鎖,record lock 永遠(yuǎn)鎖的是索引荡短,而非數(shù)據(jù)本身丐枉,如果innodb表中沒有索引,那么會自動創(chuàng)建一個隱藏的聚集索引掘托,鎖住的就是這個聚集索引瘦锹。當(dāng)一條sql沒有走任何索引時,那么將會在每一條聚集索引后面加X鎖闪盔,這個類似于表鎖弯院,但原理上和表鎖應(yīng)該是完全不同的。
記錄鎖的條件
1泪掀、命中單行記錄并且命中的條件字段是唯一索引或者主索引听绳;
update user_infosetname=’張三’whereid=1;//這里的id是唯一索引,使用了Record Lock
Record Lock總是會去鎖住索引記錄,如果InnoDB存儲引擎表在建立的時候沒有設(shè)置任何一個索引异赫,那么這時InnoDB存儲引擎會使用隱式的主鍵來進(jìn)行鎖定椅挣。
2、Gap Lock(間隙鎖)
間隙鎖是封鎖索引記錄中的間隔塔拳,或者第一條索引記錄之前的范圍鼠证,又或者最后一條索引記錄之后的范圍。
產(chǎn)生間隙的條件
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','小紅');
上面數(shù)據(jù)的間隙就是:
(-infinity, 1]
(1, 5]
(5, 7]
(7, 11]
(11, +infinity]
在test中使用間隙鎖的話弯予,鎖的就是上面的范圍
基于上面的表,看看下面的例子:
BEGIN;/* 查詢 id 在 7 - 11 范圍的數(shù)據(jù)并加記錄鎖 */SELECT*FROM`test`WHERE`id`BETWEEN5AND7FORUPDATE;/* 延遲30秒執(zhí)行个曙,防止鎖釋放 */SELECTSLEEP(30);
產(chǎn)生的間隙鎖范圍是锈嫩,會鎖住 (5, 7]、(7, 11] 這兩個區(qū)間垦搬。
BEGIN;SELECT*FROM`test`WHERE`id`=3FORUPDATE;SELECTSLEEP(30);
由于3的記錄并不存在呼寸,所以也沒有單行記錄可言,也會產(chǎn)生間隙鎖猴贰,范圍是(1,3]对雪、(3,5]
BEGIN;SELECT*FROM`test`WHERE`id`>5FORUPDATE;SELECTSLEEP(30);
會產(chǎn)生間隙鎖,范圍是(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=3forupdate;+----+------+-------+-----+|id|name|price|num|+----+------+-------+-----+|6|tom|2788|3|+----+------+-------+-----+1rowinset窗口B:mysql>insert into product_copy values(5,'kris',1888,2);這里會等待? 直到窗口Acommit才會顯示下面結(jié)果QueryOK,1row affected但是下面是不需要等待的mysql>update product_copy set price=price+100where num=1;QueryOK,2rows affectedRowsmatched:2Changed:2Warnings:0mysql>insert into product_copy values(5,'kris',1888,5);QueryOK,1row affected
通過上面的例子可以看出Gap 鎖的作用是在的范圍是(1,3]U[3,4)瑟捣。
但是要記住馋艺,使用主鍵索引/唯一索引條件的單行記錄不會使用間隙鎖,會使用記錄鎖迈套,但是如果查詢出來的是多行記錄捐祠,使用的就是間隙鎖
下面的例子,id是主鍵索引桑李,使用的是記錄鎖
窗口A:mysql>select*fromproduct_copywhereid=6forupdate;+----+------+-------+-----+|id|name|price|num|+----+------+-------+-----+|6|tom|2788|3|+----+------+-------+-----+窗口B:并不會發(fā)生等待mysql>insertintoproduct_copyvalues(5,'kris',1888,3);QueryOK,1row affected窗口A:mysql>select*fromproduct_copywhereid>6forupdate;+----+------+-------+-----+|id|name|price|num|+----+------+-------+-----+|10|優(yōu)衣庫|488|4|+----+------+-------+-----+窗口B:會發(fā)生等待mysql>insertintoproduct_copyvalues(9,'kris',1888,3);QueryOK,1row affected
從上面的例子可以看到踱蛀,第一條sql是使用了主鍵索引的單行記錄,使用了記錄鎖贵白,第二個sql即使使用了主鍵索引率拒,但是查詢的數(shù)據(jù)多于一條,使用的間隙鎖禁荒,間隙鎖鎖的范圍是(6,+infinity]
3猬膨、Next-Key Lock(Record Lock + Gap Lock,臨鍵鎖)
臨鍵鎖圈浇,是記錄鎖與間隙鎖的組合,它的封鎖范圍靴寂,既鎖住記錄本身還鎖住索引之間的間隙磷蜀。
注:臨鍵鎖的主要目的,也是為了避免幻讀(Phantom Read)百炬。如果把事務(wù)的隔離級別降級為RC褐隆,臨鍵鎖則也會失效。
MVCC剖踊,多版本的并發(fā)控制庶弃,Multi-Version Concurrency Control。
MVCC的目的就是多版本并發(fā)控制德澈,在數(shù)據(jù)庫中的實(shí)現(xiàn)歇攻,就是為了解決讀寫沖突。使用鎖和鎖協(xié)議來實(shí)現(xiàn)相應(yīng)的隔離級別來進(jìn)行并發(fā)控制會因?yàn)殒i會造成事務(wù)阻塞梆造。而多版本并發(fā)控制使得對同一行記錄做讀寫的事務(wù)之間不用相互阻塞等待缴守,提高了事務(wù)的并發(fā)能力,可以認(rèn)為MVCC是一種解決讀寫阻塞等待的行級鎖镇辉。
MVCC的數(shù)據(jù)庫表中每一行數(shù)據(jù)都可能存在多個版本屡穗,對數(shù)據(jù)庫的任何修改的提交都不會直接覆蓋之前的數(shù)據(jù),而是產(chǎn)生一個新的版本與老版本共存忽肛,通過讀寫數(shù)據(jù)時讀不同的版本來避免加鎖阻塞
1村砂、MVCC只支持(已提交讀)和(可重復(fù)讀)隔離級別。
2屹逛、MVCC能解決臟讀础废、不可重復(fù)讀問題汛骂,不能解決幻讀問題。
3色迂、MVCC是用來解決讀寫操作之間的阻塞問題香缺。
隱式字段
每行記錄除了我們自定義的字段外,還有數(shù)據(jù)庫隱式定義的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等字段
DB_TRX_ID:數(shù)據(jù)行版本號:大小為6byte歇僧,記錄最近修改(修改/插入)事務(wù)ID图张,記錄創(chuàng)建這條記錄/最后一次修改該記錄的事務(wù)ID
DB_ROLL_PTR:刪除版本號:大小為7byte,記錄回滾指針诈悍,指向當(dāng)前記錄行的undo log信息(指向該數(shù)據(jù)的前一個版本數(shù)據(jù))
DB_ROW_ID:行數(shù)據(jù)隱式id:大小為6byte祸轮,隱含的自增ID(隱藏主鍵),如果數(shù)據(jù)表沒有主鍵侥钳,InnoDB會自動以DB_ROW_ID
產(chǎn)生一個聚簇索引
ReadView
read view是讀視圖适袜,其實(shí)就相當(dāng)于一種快照,里面記錄了系統(tǒng)中當(dāng)前活躍事務(wù)的ID以及相關(guān)信息舷夺,主要用途是用來做可見性判斷苦酱,判斷當(dāng)前事務(wù)是否有資格訪問該行數(shù)據(jù)。read view有多個變量:
trx_ids: 它里面的trx_ids變量存儲了活躍事務(wù)列表给猾,也就是Read View開始創(chuàng)建時其他未提交的活躍事務(wù)的ID列表疫萤。例如事務(wù)A在創(chuàng)建read view(快照)時,數(shù)據(jù)庫中事務(wù)B和事務(wù)C還沒提交或者回滾結(jié)束事務(wù)敢伸,此時trx_ids就會將事務(wù)B和事務(wù)C的事務(wù)ID記錄下來扯饶。
假設(shè)當(dāng)前事務(wù)生成了一個ReadView,trx_ids列表里的事務(wù)id為[60,100]池颈。
1尾序、如果你要訪問的記錄版本的事務(wù)id為50,比當(dāng)前列表最小的id 60還小躯砰,那說明這個事務(wù)在ReadView生成之前就提交了每币,所以對當(dāng)前活動的事務(wù)來說是可訪問的。
2琢歇、如果你要訪問的記錄版本的事務(wù)id為70,發(fā)現(xiàn)此事務(wù)在列表id最大值和最小值之間脯爪,那就再判斷一下 70 這個id是否在列表內(nèi),如果在那就說明此事務(wù)還未提交矿微,所以版本不能被訪問痕慢。如果不在那說明事務(wù)已經(jīng)提交,所以版本可以被訪問涌矢。
3掖举、如果你要訪問的記錄版本的事務(wù)id為110,那比事務(wù)列表最大id100都大娜庇,那說明這個版本是在ReadView生成之后才發(fā)生的塔次,所以不能被訪問方篮。
Undo log
Undo log中存儲的是老版本數(shù)據(jù),當(dāng)一個事務(wù)需要讀取記錄行時励负,如果當(dāng)前記錄行不可見藕溅,可以通過回滾指針順著undo log鏈找到滿足其可見性條件的記錄行版本。
在InnoDB里继榆,undo log分為如下兩類:
①insert undo log : 事務(wù)對insert新記錄時產(chǎn)生的undo log, 只在事務(wù)回滾時需要, 并且在事務(wù)提交后就可以立即丟棄巾表。
②update undo log : 事務(wù)對記錄進(jìn)行delete和update操作時產(chǎn)生的undo log,在事務(wù)回滾時需要
實(shí)際還有一個刪除flag隱藏字段, 既記錄被更新或刪除并不代表真的刪除略吨,而是刪除flag變了
插入:獲取最新的事務(wù)版本號n,保存n到對應(yīng)行的行版本號
刪除:獲取最新的事務(wù)版本號n集币,保存到對應(yīng)行的刪除版本號
修改:變?yōu)閕nsert和delete操作的組合,先獲取最新的事務(wù)版本號n翠忠,然后進(jìn)行數(shù)據(jù)行拷貝鞠苟,插入拷貝的數(shù)據(jù),保存n到新插入數(shù)據(jù)行行版本號的字段中秽之,然后保存n到舊的數(shù)據(jù)行的刪除版本號字段中
查詢:獲取最新的事務(wù)版本號n,查詢行版本號小于或者等于n的行數(shù)據(jù)当娱,防止讀到其他事務(wù)提交的數(shù)據(jù)
MVCC實(shí)現(xiàn)過程原理主要的原理:
參考:https://baijiahao.baidu.com/s?id=1629409989970483292&wfr=spider&for=pc
版本記錄都是去版本鏈里面找的,然后根據(jù)不同隔離級別生成的ReadView就會有所不同
例如:在一個讀已提交或者是重復(fù)讀的級別事務(wù)中
有一個事務(wù)id為100的事務(wù)考榨,修改了name,使得的name等于小明2跨细,但是事務(wù)還沒提交。則此時的版本鏈如下:
此時另一個事務(wù)發(fā)起了select 語句要查詢id為1的記錄董虱,那此時生成的ReadView 列表就是[100]扼鞋。最新版本100是活躍事務(wù)不能訪問申鱼,那就得使用版本鏈去找了愤诱,首先找100的下個版本name為小明1的記錄,發(fā)現(xiàn)trx_id是60捐友,小于列表中的最小id,所以可以訪問淫半,直接訪問結(jié)果為小明1。那這時候我們把事務(wù)id為100的事務(wù)提交了匣砖,并且新建了一個事務(wù)id為110也修改id為1的記錄,name修改為小明3,并且不提交事務(wù)
這時候之前那個select事務(wù)又執(zhí)行了一次查詢,要查詢id為1的記錄蚯妇。這時候會發(fā)生兩種情況
1滞详、已提交讀隔離級別
會重新一個生成一個ReadView,那你的活動事務(wù)列表中的值就變了拂共,變成了[110]牺弄,通過版本鏈查trx_id對比,查到的只能是小明2宜狐。
2势告、可重復(fù)讀隔離級別
ReadView還是第一次select時候生成的ReadView,也就是列表的值還是[100]蛇捌。所以select的結(jié)果是小明1。所以第二次select結(jié)果和第一次一樣咱台,所以叫可重復(fù)讀络拌!
也就是說已提交讀隔離級別下的事務(wù)在每次查詢的開始都會生成一個獨(dú)立的ReadView,而可重復(fù)讀隔離級別則在第一次讀的時候生成一個ReadView,之后的讀都復(fù)用之前的ReadView回溺。
當(dāng)前讀與快照讀
1春贸、當(dāng)前讀:
即加鎖讀,讀取記錄的最新版本馅而,會加鎖保證其他并發(fā)事務(wù)不能修改當(dāng)前記錄祥诽,直至獲取鎖的事務(wù)釋放鎖;
使用當(dāng)前讀的操作主要包括:顯式加鎖的讀操作與插入/更新/刪除等寫操作瓮恭,如下所示:
select*fromtablewhere?lockinshare mode;select*fromtablewhere?forupdate;insertintotable values(…);update tableset?where?;deletefromtablewhere?;
2雄坪、快照讀:
即不加鎖讀,讀取記錄的快照版本而非最新版本屯蹦,通過MVCC實(shí)現(xiàn)维哈;
當(dāng)前讀
需要特別注意的是在MVCC下的可重復(fù)讀在讀操作是防止了幻讀,讀操作下完全就是按照ReadView進(jìn)行的快照讀登澜。但是對于會對數(shù)據(jù)修改的操作(update阔挠、insert、delete)都是采用當(dāng)前讀的模式脑蠕。在執(zhí)行這幾個操作時會讀取最新的記錄购撼,即使是別的事務(wù)提交的數(shù)據(jù)也可以查詢到。假設(shè)要update一條記錄谴仙,但是在另一個事務(wù)中已經(jīng)delete掉這條數(shù)據(jù)并且commit了迂求,如果update就會產(chǎn)生沖突,所以在update的時候需要知道最新的數(shù)據(jù)晃跺。
作者:技術(shù)只適用于干活
鏈接:http://www.reibang.com/p/615f3c7fbe6f
來源:簡書
著作權(quán)歸作者所有揩局。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處掀虎。