MySQL之鎖類型解讀
MySQL鎖類型解讀
鎖的分類圖,如下:
鎖操作類型劃分
讀鎖
: 也稱為共享鎖
、英文用S表示。針對(duì)同一份數(shù)據(jù),多個(gè)事務(wù)的讀操作可以同時(shí)進(jìn)行而不會(huì)互相影響,相互不阻塞的。寫鎖
: 也稱為排他鎖
、英文用X表示。當(dāng)前寫操作沒(méi)有完成前,它會(huì)阻斷其他寫鎖和讀鎖。這樣就能確保在給定的時(shí)間里,只有一個(gè)事務(wù)能執(zhí)行寫入,并防止其他用戶讀取正在寫入的同一資源。- 對(duì)于InnoDB引擎,讀鎖和寫鎖可以加在表上,或者行上
鎖定讀
對(duì)讀取的記錄加S鎖
:
select...lock in share mode; #或 select...for share;#(8.0新增語(yǔ)法)。。
若當(dāng)前事務(wù)執(zhí)行了該語(yǔ)句,則會(huì)給該記錄加S鎖
,并允許別的事務(wù)繼續(xù)獲取該記錄的S鎖
,比如別的事務(wù)也使用 SELECT... LOCK IN SHAREMODE 語(yǔ)句來(lái)讀取這些記錄
但是不能獲取這些記錄的X鎖
,比如別的事務(wù)不能直接修改這些記錄,會(huì)阻塞直到當(dāng)前事務(wù)提交之后將這些記錄上的S鎖
釋放掉。
對(duì)讀取的記錄加X鎖
:
select...for update;
如果當(dāng)前事務(wù)執(zhí)行了該語(yǔ)句,則會(huì)給該記錄加X鎖
,即不允許別的事務(wù)獲取這些事務(wù)S鎖
,也不允許獲取X鎖
...
MySQL8新特性
在8.0版本中,SELECT ... FOR UPDATE, SELECT .. FOR SHARE 添加NOWAIT、 SKIP LOCKED
語(yǔ)法,跳過(guò)鎖等待,或者跳過(guò)鎖定。
通過(guò)添加NOWAIT、SKIP LOCKED語(yǔ)法,能夠立即返回。如果查詢的行已經(jīng)加鎖:
- 那么NOWAIT會(huì)
立即報(bào)錯(cuò)返回
- 而SKIP LOCKED也會(huì)立即返回,只是返回的結(jié)果中
不包含被鎖定的行
寫操作
寫操作無(wú)非是delete,update,insert
DELETE
底層是先在 B+ 樹(shù)中定位到這條記錄的位置,然后獲取這條記錄的X鎖
,再執(zhí)行 delete操作。
UPDATE
情況1: 未修改該記錄的鍵值,并且被更新的列占用的存儲(chǔ)空間在修改前后未發(fā)生變化
。
則在 B+ 樹(shù)中定位到這條記錄的位置,然后獲取記錄的 X鎖,最后在原記錄的位置進(jìn)行修改操作。
情況2: 修改了該記錄的鍵值,則相當(dāng)于在原記錄上做 DELETE 操作之后再來(lái)一次INSERT操作
,加鎖操作。需要按照 DELETE 和 INSERT 的規(guī)則進(jìn)行了。
2.意向鎖
InnoDB 支持多粒度鎖
,它允許行級(jí)鎖
與表級(jí)鎖
共存,而意向鎖就是其中的一種表鎖
- 意向鎖的存在是為了協(xié)調(diào)行鎖和表鎖的關(guān)系
- 意向鎖是一種
不與行級(jí)鎖沖突表級(jí)鎖
,這一點(diǎn)非常重要 - 意向鎖是自動(dòng)創(chuàng)建的,自動(dòng)聲明其上級(jí)獲取過(guò)鎖這一動(dòng)作,輪到時(shí)就會(huì)有排隊(duì)權(quán)
意向鎖分為兩種:
意向共享鎖: 事務(wù)有意向?qū)Ρ碇械哪承┬屑?code>共享鎖(S鎖)
#事務(wù)要獲取某些行的S鎖,必須先獲得表的IS 鎖。 select column from table ... lock in share mode;
意向排他鎖: 事務(wù)有意向?qū)Ρ碇械哪承┬屑?code>排他鎖(X鎖)
#事務(wù)要獲取某些行的X鎖,必須先獲得表的 IX 鎖 select column from table ... for update;
意向鎖是由存儲(chǔ)引擎自己維護(hù)的 ,用戶無(wú)法手動(dòng)操作意向鎖,在為數(shù)據(jù)行加共享/排他鎖之前,InooDB 會(huì)先獲取該數(shù)據(jù)行所在數(shù)據(jù)表的對(duì)應(yīng)意向鎖
意向鎖作用
現(xiàn)在有兩個(gè)事務(wù)T1和T2,其中T2試圖在該表級(jí)別上應(yīng)用共享或排它鎖
- 如果沒(méi)有意向鎖存在,那么T2就需要去檢查各個(gè)頁(yè)或行是否存在鎖
- 如果存在意向鎖,那么此時(shí)就會(huì)受到由T1控制的表級(jí)別意向鎖的阻塞,T2在鎖定該表前不必檢查各個(gè)頁(yè)或行鎖,而只需檢查表上的意向鎖。其實(shí)就是在更大一級(jí)別的空間示意里面是否已經(jīng)上過(guò)鎖!
- 在數(shù)據(jù)表的場(chǎng)景中,如果我們給某一行數(shù)據(jù)加上了排它鎖,數(shù)據(jù)庫(kù)會(huì)自動(dòng)給更大一級(jí)的空間,比如數(shù)據(jù)頁(yè)或數(shù)據(jù)表加上意向鎖,告訴其他人這個(gè)數(shù)據(jù)頁(yè)或數(shù)據(jù)表已經(jīng)有人上過(guò)排它鎖了,這樣當(dāng)其他人想要獲取數(shù)據(jù)表排它鎖的時(shí)候,只需要看是否有人已獲取這個(gè)數(shù)據(jù)表的意向排他鎖即可
假設(shè)事務(wù)A獲取了某一行的排他鎖,并未提交
begin; select * from teacher where id=6 for update;
事務(wù)B想要獲取teacher表的讀鎖,語(yǔ)句如下
begin; lock tables teacher read;
因?yàn)楣蚕礞i與排他鎖互斥,所以事務(wù)B在試圖對(duì) teacher 表加共享鎖的時(shí)候,必須保證兩個(gè)條件。
- 當(dāng)前沒(méi)有其他事務(wù)持有 teacher 表的排他鎖
- 當(dāng)前沒(méi)有其他事務(wù)持有 teacher 表中任意一行的排他鎖
為了檢測(cè)是否滿足第二個(gè)條件,事務(wù)B必須在確保 teacber 表不存在任何排他鎖的前提下,去檢測(cè)表中的每一行是否存在排他鎖。很明顯這是一個(gè)效率很差的做法,但是有了意向鎖之后,情況就不一樣了。
總結(jié):
意向鎖是一個(gè)虛擬的鎖,只是為了讓別的事務(wù)知道 這里有行級(jí)鎖 所以不能加某些表級(jí)鎖
3.自增鎖
在使用MySQL過(guò)程中,我們可以為表的某個(gè)列添加 AUTO_INCREMENT
屬性。舉例:
CREATE TABLE `teacher` ( `id` int NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
這意味著插入語(yǔ)句時(shí)不需要為其賦值,SQL語(yǔ)句修改如下所示
INSERT INTO `teacher` (name) VALUES ('zhangsan'), ('lisi'); ? #上邊的插入語(yǔ)句并沒(méi)有為id列顯式賦值,所以系統(tǒng)會(huì)自動(dòng)為它賦上遞增的值,結(jié)果如下所示 mysql> select * from teacher; +----+----------+ | id | name | +----+----------+ | 1 | zhangsan | | 2 | lisi | +----+----------+ 2 rows in set (0.00 sec)
所有插入數(shù)據(jù)的方式總共分為三類,分別是:“ Simple inserts
”,“ Bulk inserts
”和“ Mixed-mode inserts
”。
對(duì)于上面案例,MySQL中采用了自增鎖 的方式來(lái)實(shí)現(xiàn),自增鎖
是向含有AUTO_INCREMENT列的表中插入數(shù)據(jù)時(shí)的一種特殊的表級(jí)鎖
,在執(zhí)行插入語(yǔ)句時(shí)加一個(gè)AUTO-INC鎖,分配遞增的值,執(zhí)行結(jié)束后,再把AUTO-INC鎖釋放掉。
一個(gè)事務(wù)在持有AUTO-INC鎖的過(guò)程中,其他事務(wù)的插入語(yǔ)句都要被阻塞,可以保證一個(gè)語(yǔ)句中分配的遞增值是連續(xù)的。也正因?yàn)榇?,其并發(fā)性不高,當(dāng)我們向一個(gè)有AUTO_INCREMENT關(guān)鍵字的主鍵插入值的時(shí)候,每條語(yǔ)句都要對(duì)這個(gè)表鎖進(jìn)行競(jìng)爭(zhēng),這樣的并發(fā)潛力其實(shí)是很低下的,所以innodb通過(guò) innodb_autoinc_lock_mode的不同取值來(lái)提供不同的鎖定機(jī)制,來(lái)顯著提高SQL語(yǔ)句的可伸縮性和性能。
#innodb_autoinc_lock_mode=0(傳統(tǒng)鎖定模式) 所有insert語(yǔ)句都會(huì)獲得自增鎖,這種鎖定是全局性的,即它會(huì)阻止其他事務(wù)同時(shí)進(jìn)行插入操作,直到當(dāng)前插入完成,即上面例子,并發(fā)性差 ? #innodb_autoinc_lock_mode=1(連續(xù)鎖定模式) mysql8之前的默認(rèn)模式,當(dāng)執(zhí)行 INSERT 時(shí),InnoDB 會(huì)先檢查是否有可用的自增值,如果有,則立即分配該值給新行,然后才加鎖 ? #innodb_autoinc_lock_mode=2(交錯(cuò)鎖定模式) InnoDB 會(huì)預(yù)先分配一組自增ID(數(shù)量由 innodb_autoinc_cache 控制),然后將這些ID分配給后續(xù)的插入操作,多個(gè)客戶端可以同時(shí)從預(yù)分配的ID池中獲取ID并插入新行,從而大大減少了鎖等待的時(shí)間,但由于多個(gè)語(yǔ)句會(huì)同時(shí)需要數(shù)字,所以任何給定插入的行生成的值可能不是連續(xù)的
4.元數(shù)據(jù)鎖(MDL鎖)
MDL 的作用是,保證讀寫的正確性。比如,如果一個(gè)查詢正在遍歷一個(gè)表中的數(shù)據(jù),而執(zhí)行期間另一個(gè)線程對(duì)這個(gè)表結(jié)構(gòu)做變更
,增加了一列,那么查詢線程拿到的結(jié)果跟表結(jié)構(gòu)對(duì)不上,肯定是不行的。
因此,當(dāng)對(duì)一個(gè)表做增刪改查操作的時(shí)候,加MDL讀鎖
;當(dāng)要對(duì)表做結(jié)構(gòu)變更操作的時(shí)候,加 MDL寫鎖
。解決DML和DDL操作之間的一致性問(wèn)題,不需要顯式使用
,在訪問(wèn)一個(gè)表時(shí)會(huì)自動(dòng)加上
行級(jí)鎖
顧名思義,就是鎖住頁(yè)中某一行記錄,注意點(diǎn)是 行級(jí)鎖只在存儲(chǔ)引擎層實(shí)現(xiàn)
,鎖的力度小,發(fā)生鎖沖突概率低,并發(fā)度高
缺點(diǎn)是對(duì)于鎖的開(kāi)銷比較大,加鎖會(huì)比較慢,容易出現(xiàn)死鎖
情況
- 數(shù)據(jù)準(zhǔn)備
CREATE TABLE accounts ( id INT PRIMARY KEY, balance DECIMAL(10, 2) ); ? INSERT INTO accounts (id, balance) VALUES (1, 1000), (2, 2000);
- 記錄鎖
記錄鎖僅僅把一條記錄 鎖上,官方的類型名稱為:LOCK_REC_NOT_GAP
。比如我們把id值為8的那條記錄加一個(gè)記錄鎖如圖所示,僅僅是鎖住了id值為8的記錄,對(duì)周圍的數(shù)據(jù)沒(méi)有影響。
記錄鎖是有S鎖和X鎖之分的,稱之為 S型(讀)記錄鎖
和 X型(寫)記錄鎖
- 當(dāng)一個(gè)事務(wù)獲取了一條記錄的讀鎖后,其他事務(wù)可以繼續(xù)獲取該記錄的讀鎖,但不可以繼續(xù)獲取寫鎖;
- 當(dāng)一個(gè)事務(wù)獲取了一條記錄的寫鎖后,其他事務(wù)既不可以獲取該記錄的讀鎖,也不可以繼續(xù)獲取寫鎖。
死鎖
接下來(lái),我們來(lái)看一個(gè)可能引起死鎖的情況,假設(shè)事務(wù) D 和事務(wù) E 同時(shí)運(yùn)行,并且它們都試圖更新兩個(gè)賬戶的余額:
-- 啟動(dòng)事務(wù) D START TRANSACTION; -- 獲取賬戶1的排他鎖 SELECT * FROM accounts WHERE id = 1 FOR UPDATE; ? -- 啟動(dòng)事務(wù) E START TRANSACTION; -- 獲取賬戶2的排他鎖 SELECT * FROM accounts WHERE id = 2 FOR UPDATE; ? -- 事務(wù) D 試圖獲取賬戶 2 的排他鎖 SELECT * FROM accounts WHERE id = 2 FOR UPDATE; ? -- 事務(wù) E 試圖獲取賬戶 1 的排他鎖 SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
- 事務(wù)D已經(jīng)獲得了賬戶1的排他鎖,而事務(wù)E已經(jīng)獲得了賬戶2的排他鎖。
- 當(dāng)事務(wù)D試圖獲取賬戶2的排他鎖時(shí),它會(huì)被阻塞,同樣地,當(dāng)事務(wù) E 試圖獲取賬戶 1 的排他鎖時(shí)也會(huì)被阻塞。這就會(huì)形成一個(gè)死鎖的情況。
- InnoDB 會(huì)檢測(cè)到這種情況,并自動(dòng)解決死鎖。它會(huì)選擇一個(gè)事務(wù)回滾,以便另一個(gè)事務(wù)可以繼續(xù)執(zhí)行。
- 可以通過(guò)查看
INFORMATION_SCHEMA.INNODB_TRX
表來(lái)了解當(dāng)前的事務(wù)狀態(tài),包括死鎖信息
間隙鎖
MySQL 在可重復(fù)讀隔離級(jí)別下是可以解決幻讀問(wèn)題的,解決方案有兩種
- 可以使用
MVCC
方案解決 - 也可以采用
加鎖
方案解決。
但是事務(wù)在第一次執(zhí)行讀取操作時(shí),那些幻影記錄尚不存在,我們無(wú)法給這些幻影記錄
加上記錄鎖 ,所以InnoDB提出了一種稱之為Gap Locks 的鎖,官方的類型名稱為: LOCK_GAP ,我們可以簡(jiǎn)稱為 gap 鎖 (間隙鎖)
- 間隙鎖是在行級(jí)別的鎖定之上的一種擴(kuò)展,它不僅鎖定具體的行的兩邊,還鎖定行之間的間隙
- gap鎖的提出僅僅是為了防止插入幻影記錄而提出的
這種鎖定是為了防止其他事務(wù)插入新的行到已經(jīng)被鎖定的數(shù)據(jù)行之間,從而保證了事務(wù)的隔離性和一致性。
圖中id值為8的記錄加了gap鎖,意味著 不允許別的事務(wù)在id為3記錄后的間隙,即不允許(3,15)之間插入新記錄
類型
插入意向間隙鎖:
- 插入意向間隙鎖告訴其他事務(wù)這里即將發(fā)生插入操作,因此其他事務(wù)不應(yīng)該在該位置進(jìn)行插入
普通間隙鎖:
- 執(zhí)行
SELECT ... FOR UPDATE
或SELECT ... FOR SHARE
查詢時(shí),InnoDB 會(huì)在查詢范圍內(nèi)的所有數(shù)據(jù)行上放置鎖,并在這些行之間的間隙上放置間隙鎖 - 普通間隙鎖用于防止其他事務(wù)在已鎖定的數(shù)據(jù)行之間插入新行
舉例
-- 有一個(gè)表 orders,其中包含 order_id 和 order_amount 字段 CREATE TABLE orders ( order_id INT AUTO_INCREMENT PRIMARY KEY, order_amount DECIMAL(10, 2) ); ? INSERT INTO orders (order_amount) VALUES (100), (500), (1000);
現(xiàn)在,我們有兩個(gè)事務(wù) A 和 B。事務(wù) A 執(zhí)行一個(gè)范圍查詢:
-- 啟動(dòng)事務(wù) A START TRANSACTION; ? -- 獲取訂單金額在200 到 600之間的排他鎖 SELECT * FROM orders WHERE order_amount BETWEEN 200 AND 600 FOR UPDATE; ? -- 事務(wù)A在200和600之間的間隙上放置了間隙鎖。這意味著其他事務(wù)不能在這個(gè)范圍內(nèi)插入新的行
示例 2: 插入意向間隙鎖
-- 啟動(dòng)事務(wù) B START TRANSACTION; -- 嘗試插入一個(gè)新的訂單 INSERT INTO orders (order_amount) VALUES (300); #事務(wù)B將被阻塞,因?yàn)樗噲D在事務(wù)A已經(jīng)鎖定的間隙內(nèi)插入新的行
臨鍵鎖
官方的類型名稱為: LOCK_ORDINARY,我們也可以簡(jiǎn)稱為next-key鎖 。Next-Key Locks是在存儲(chǔ)引擎 innodb 、事務(wù)級(jí)別在可重復(fù)讀的情況下使用的數(shù)據(jù)庫(kù)鎖,innodb默認(rèn)的鎖就是臨鍵鎖
Next-Key Locks 是一種組合鎖,它同時(shí)包含了記錄鎖和間隙鎖。簡(jiǎn)單來(lái)說(shuō),會(huì)在一個(gè)記錄上放置一個(gè)記錄鎖,并且在該記錄間隙上放置一個(gè)間隙鎖。
舉例
-- 有一個(gè)表 orders,其中包含 order_id 和 order_amount 字段 CREATE TABLE orders ( order_id INT AUTO_INCREMENT PRIMARY KEY, order_amount DECIMAL(10, 2) ); INSERT INTO orders (order_amount) VALUES (100), (500), (1000);
現(xiàn)在,我們有兩個(gè)事務(wù)A和B。事務(wù)A執(zhí)行一個(gè)范圍查詢:
-- 啟動(dòng)事務(wù) A START TRANSACTION; ? -- 獲取訂單金額在200到600 之間的排他鎖 SELECT * FROM orders WHERE order_amount BETWEEN 200 AND 600 FOR UPDATE; -- 事務(wù)A會(huì)在 order_amount 為 500 的行上放置一個(gè)排他鎖,并在200和600之間的所有間隙上放置間隙鎖。這意味著其他事務(wù)不能在這個(gè)范圍內(nèi)插入新的行
現(xiàn)在,事務(wù) B 嘗試插入一個(gè)新的訂單:
-- 啟動(dòng)事務(wù) B START TRANSACTION; ? -- 嘗試插入一個(gè)新的訂單 INSERT INTO orders (order_amount) VALUES (300); -- 事務(wù) B 將被阻塞,因?yàn)樗噲D在事務(wù) A 已經(jīng)鎖定的間隙內(nèi)插入新的行
如果您希望避免臨鍵鎖,可以將事務(wù)隔離級(jí)別設(shè)置為 READ COMMITTED
:
-- 設(shè)置事務(wù)隔離級(jí)別為 READ COMMITTED SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; -- 啟動(dòng)事務(wù) A START TRANSACTION; ? -- 獲取訂單金額在 200 到 600 之間的排他鎖 SELECT * FROM orders WHERE order_amount BETWEEN 200 AND 600 FOR UPDATE; -- 在這種情況下,事務(wù) A 僅在匹配的行上放置排他鎖,而不會(huì)在行之間的間隙上放置間隙鎖
插入意向鎖
InnoDB規(guī)定:事務(wù)在等待的時(shí)候也需要在內(nèi)存中生成一個(gè)鎖結(jié)構(gòu),表明有事務(wù)想在某個(gè)間隙
中插入新記錄,但是現(xiàn)在在等待。InnoDB就把這種類型的鎖命名為 Insert Intention Locks ,官稱為插入意向鎖
工作原理:
假設(shè)事務(wù) T1 對(duì)區(qū)間 [10, 20] 之間的所有行以及這個(gè)區(qū)間的間隙持有 Next-Key Locks。這時(shí),事務(wù) T2 嘗試在區(qū)間 [10, 20] 內(nèi)插入一行數(shù)據(jù),比如插入 15。
- T2 產(chǎn)生插入意向鎖:由于 T1 持有該間隙上的鎖,T2 無(wú)法立即插入數(shù)據(jù),但它會(huì)在內(nèi)存中創(chuàng)建一個(gè)插入意向鎖,表示它想要在間隙 [10, 20] 中插入數(shù)據(jù)。
- T2 等待:T2 的插入操作被阻塞,等待 T1 的事務(wù)結(jié)束
- T1 提交或回滾:當(dāng) T1 完成并釋放其鎖后,T2 的
插入意向鎖變?yōu)橛行?/code>,T2 可以繼續(xù)插入數(shù)據(jù)。
小結(jié):
- 通過(guò)使用插入意向鎖,系統(tǒng)可以更好地管理事務(wù)之間的等待順序,減少死鎖的可能性
- 它允許事務(wù)聲明插入意圖,并在等待間隙鎖釋放的過(guò)程中保持一定的靈活性
頁(yè)鎖
頁(yè)鎖就是在頁(yè)的粒度上進(jìn)行鎖定,鎖定的數(shù)據(jù)資源比行鎖要多,因?yàn)橐粋€(gè)頁(yè)中可以有多個(gè)行記錄
當(dāng)我們使用頁(yè)鎖的時(shí)候,會(huì)出現(xiàn)數(shù)據(jù)浪費(fèi)的現(xiàn)象,但這樣的浪費(fèi)最多也就是一個(gè)頁(yè)上的數(shù)據(jù)行。頁(yè)鎖的開(kāi)銷介于表鎖和行鎖之間,會(huì)出現(xiàn)死鎖。鎖定粒度介于表鎖和行鎖之間,并發(fā)度一般
加鎖的態(tài)度劃分
樂(lè)觀鎖和悲觀鎖并不是鎖,而是鎖的 設(shè)計(jì)思想,從名字中也可以看出這兩種鎖是兩種看待數(shù)據(jù)并發(fā)的思維方式
①悲觀鎖
顧名思義,就是很悲觀,總是假設(shè)最壞的情況,每次去拿數(shù)據(jù)的時(shí)候都認(rèn)為別人會(huì)修改,所以每次在拿數(shù)據(jù)的時(shí)候都會(huì)上鎖,這樣別人想拿這個(gè)數(shù)據(jù)就會(huì) 阻塞 直到它拿到鎖(共享資源每次只給一個(gè)線程使用,其它線程阻塞,用完后再把資源轉(zhuǎn)讓給其它線程)
比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖,當(dāng)其他線程想要訪問(wèn)數(shù)據(jù)時(shí),都需要阻塞掛起。Java中 synchronized
和 ReentrantLock
等獨(dú)占鎖就是悲觀鎖思想的實(shí)現(xiàn)
- 實(shí)現(xiàn)方式: 使用行級(jí)鎖或表級(jí)鎖,例如可以使用
SELECT...FOR UPDATE
或LOCK INSHARE MODE
語(yǔ)句來(lái)加鎖。 - 悲觀鎖適合并發(fā)沖突多,寫多讀少的場(chǎng)景。通過(guò)每次加鎖的形式來(lái)確保數(shù)據(jù)的安全性,吞吐量較低。
-- 讀取數(shù)據(jù)并加鎖 SELECT id, name FROM users WHERE id = 1 FOR UPDATE; ? -- 執(zhí)行更新操作 UPDATE users SET name = 'new_name' WHERE id = 1;
秒殺案例
其實(shí)就是簡(jiǎn)單的加鎖,解鎖,用戶發(fā)起秒殺請(qǐng)求
- 檢查庫(kù)存:查詢商品庫(kù)存是否大于0。
- 獲取悲觀鎖:如果庫(kù)存大于0,則嘗試獲取商品對(duì)應(yīng)的悲觀鎖
- 扣減庫(kù)存:在鎖定狀態(tài)下,執(zhí)行扣減庫(kù)存的操作。
- 釋放鎖:成功扣減庫(kù)存后,釋放悲觀鎖。
- 返回結(jié)果:向用戶返回秒殺成功或失敗的消息。
諸如 還有微服務(wù)中的分布式鎖,其實(shí)也差不多
②樂(lè)觀鎖
認(rèn)為對(duì)同一數(shù)據(jù)的并發(fā)操作不會(huì)總發(fā)生,屬于小概率事件,不用每次都對(duì)數(shù)據(jù)上鎖
但是在更新的時(shí)候會(huì)判斷一下在此期間別人有沒(méi)有去更新這個(gè)數(shù)據(jù),不采用鎖機(jī)制,而是通過(guò)程序來(lái)實(shí)現(xiàn)。在程序上,我們可以采用版本號(hào)機(jī)制或者CAS機(jī)制實(shí)現(xiàn)。樂(lè)觀鎖適用于多讀的應(yīng)用類型,這樣可以提高吞吐量。
-- 假設(shè)有一張用戶表 users,包含 id、name 和 version 字段 -- 讀取數(shù)據(jù) SELECT id, name, version FROM users WHERE id = 1; ? -- 更新數(shù)據(jù)時(shí)檢查版本號(hào) UPDATE users SET name = 'new_name', version = version + 1 WHERE id = 1 AND version = current_version;
在Java中 java.util.concurrent.atomic 包下的原子變量類就是使用了樂(lè)觀鎖的一種實(shí)現(xiàn)方式:CAS實(shí)現(xiàn)的
適用場(chǎng)景
加鎖的方式劃分
①隱式鎖
顧名思義,看不到的鎖,簡(jiǎn)單來(lái)說(shuō)就是在一個(gè)事務(wù)中執(zhí)行新插入一條記錄操作并不加鎖,但是會(huì)給該插入操作加隱式鎖
的結(jié)構(gòu),對(duì)這條插入記錄進(jìn)行保護(hù),防止該記錄被其他事務(wù)訪問(wèn)
案例
-- session 1: mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> insert INTO student VALUES(34,"周八","二班"); Query OK, 1 row affected (0.00 sec) -- session 2 mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> select * from student lock in share mode; #執(zhí)行完,當(dāng)前事務(wù)被阻塞 mysql> SELECT * FROM performance_schema.data_lock_waits\G; *************************** 1. row *************************** ENGINE: INNODB REQUESTING_ENGINE_LOCK_ID: 140562531358232:7:4:9:140562535668584 REQUESTING_ENGINE_TRANSACTION_ID: 422037508068888 REQUESTING_THREAD_ID: 64 REQUESTING_EVENT_ID: 6 REQUESTING_OBJECT_INSTANCE_BEGIN: 140562535668584 BLOCKING_ENGINE_LOCK_ID: 140562531351768:7:4:9:140562535619104 BLOCKING_ENGINE_TRANSACTION_ID: 15902 BLOCKING_THREAD_ID: 64 BLOCKING_EVENT_ID: 6 BLOCKING_OBJECT_INSTANCE_BEGIN: 140562535619104 1 row in set (0.00 sec)
分析
- 上述insert 語(yǔ)句 只是給新插入的那一行上了
隱式鎖
- 后面select * 是給全表上讀鎖
- 因?yàn)楹竺嬉o全表記錄上鎖,所以前面那條insert 語(yǔ)句會(huì)將那一行的
隱式鎖轉(zhuǎn)行為X鎖
- 所以后面的 select語(yǔ)句的
讀鎖
會(huì)和insert 語(yǔ)句生成的X鎖
沖突,所以select語(yǔ)句等待
- 如果select語(yǔ)句 不是 select * 全表記錄 ,而是 select 其他的已存在索引上的等值記錄,那么就不會(huì)和insert 語(yǔ)句X鎖 沖突,則可以查詢成功
隱式鎖的邏輯過(guò)程如下:
A
. InnoDB目錄頁(yè)中的每條記錄中都一個(gè)隱含的trx_id
字段,這個(gè)字段存在于聚簇索引的B+Tree中。B
. 在操作一條記錄前,首先根據(jù)記錄中的trx_id檢查該事務(wù)是否是活動(dòng)的事務(wù)(未提交或回滾)。如果是活動(dòng)的事務(wù),首先將隱式鎖
轉(zhuǎn)換為顯式鎖
(就是為該事務(wù)添加一個(gè)鎖)C
. 檢查是否有鎖沖突,如果有沖突,創(chuàng)建鎖,并設(shè)置為waiting狀態(tài)。如果沒(méi)有沖突不加鎖,跳到E。D
. 等待加鎖成功,被喚醒,或者超時(shí)E
. 寫數(shù)據(jù),并將自己的trx_id寫入trx_id字段
如何判斷隱式鎖是否存在
InnoDB的每條記錄中都一個(gè)隱含的trx_id字段,這個(gè)字段存在于聚集索引的B+Tree中。假設(shè)只有主鍵索引,則在進(jìn)行插入時(shí),行數(shù)據(jù)的trx_id被設(shè)置為當(dāng)前事務(wù)id;假設(shè)存在二級(jí)索引,則在對(duì)二級(jí)索引進(jìn)行插入時(shí),需要更新所在page的max_trx_id。
因此對(duì)于主鍵,只需要通過(guò)查看記錄隱藏列trx_id是否是活躍事務(wù)就可以判斷隱式鎖是否存在。 對(duì)于對(duì)于二級(jí)索引會(huì)相對(duì)比較麻煩,先通過(guò)二級(jí)索引頁(yè)上的max_trx_id進(jìn)行過(guò)濾,如果無(wú)法判斷是否活躍則需要通過(guò)應(yīng)用undo日志回溯老版本數(shù)據(jù),才能進(jìn)行準(zhǔn)確的判斷。
②顯式鎖
通過(guò)特定的語(yǔ)句進(jìn)行加鎖,例如:
#顯示加共享鎖 select .... lock in share mode #顯示加排它鎖 select .... for update
其它鎖
全局鎖
就是對(duì)整個(gè)數(shù)據(jù)庫(kù)實(shí)例 加鎖。當(dāng)你需要讓整個(gè)庫(kù)處于 只讀狀態(tài) 的時(shí)候,可以使用這個(gè)命令,之后其他線程的以下語(yǔ)句會(huì)被阻塞:數(shù)據(jù)更新語(yǔ)句(數(shù)據(jù)的增刪改)、數(shù)據(jù)定義語(yǔ)句(包括建表、修改表結(jié)構(gòu)等)和更新類事務(wù)的提交語(yǔ)句-全局鎖的典型使用 場(chǎng)景 是:做 全庫(kù)邏輯備份
Flush tables with read lock
死鎖
死鎖是指兩個(gè)或多個(gè)事務(wù)在同一資源上相互占用,并請(qǐng)求鎖定對(duì)方占用的資源,從而導(dǎo)致惡性循環(huán)。
有 兩種解決策略:
- 直接進(jìn)入等待,直到超時(shí)。這個(gè)超時(shí)時(shí)間可以通過(guò)參數(shù)innodb_lock_wait_timeout 來(lái)設(shè)置
- 另一種策略是,發(fā)起死鎖檢測(cè),發(fā)現(xiàn)死鎖后,主動(dòng)回滾死鎖鏈條中的某一個(gè)事務(wù)(將持有最少行級(jí)排他鎖的事務(wù)進(jìn)行回滾),讓其他事務(wù)得以繼續(xù)執(zhí)行。將參數(shù)innodb_deadlock_detect設(shè)置為on,表示開(kāi)啟這個(gè)邏輯
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
MySQL 邏輯備份與恢復(fù)測(cè)試的相關(guān)總結(jié)
數(shù)據(jù)庫(kù)邏輯備份就是備份軟件按照我們最初所設(shè)計(jì)的邏輯關(guān)系,以數(shù)據(jù)庫(kù)的邏輯結(jié)構(gòu)對(duì)象為單位,將數(shù)據(jù)庫(kù)中的數(shù)據(jù)按照預(yù)定義的邏輯關(guān)聯(lián)格式一條一條生成相關(guān)的文本文件,以達(dá)到備份的目的。本文將具體介紹MySQL 邏輯備份的相關(guān)概念及如何做恢復(fù)測(cè)試。2021-05-05關(guān)于TIMESTAMP with implicit DEFAULT value&
本文介紹了“TIMESTAMP with implicit DEFAULT value is deprecated”錯(cuò)誤的原因及解決方法,解決方法包括顯式指定默認(rèn)值、修改字段類型、更新數(shù)據(jù)庫(kù)版本或?qū)で髱椭?感興趣的朋友一起看看吧2025-02-02mysql安裝圖解 mysql圖文安裝教程(詳細(xì)說(shuō)明)
很多朋友剛開(kāi)始接觸mysql數(shù)據(jù)庫(kù)服務(wù)器,下面是網(wǎng)友整理的一篇mysql的安裝教程,步驟明細(xì)也有詳細(xì)的說(shuō)明。2010-06-06關(guān)于MySQL死鎖的產(chǎn)生原因、檢測(cè)與解決方式
這篇文章主要介紹了關(guān)于MySQL死鎖的產(chǎn)生原因、檢測(cè)與解決方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-07-07解決MySQL錯(cuò)誤碼:1054 Unknown column ‘**‘ in&n
這篇文章主要介紹了解決MySQL錯(cuò)誤碼:1054 Unknown column ‘**‘ in ‘field list‘的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-05-05MySQL中的常用樹(shù)形結(jié)構(gòu)設(shè)計(jì)總結(jié)
這篇文章主要介紹了MySQL中的常用樹(shù)形結(jié)構(gòu)設(shè)計(jì)總結(jié),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03阿里云ECS centos6.8下安裝配置MySql5.7的教程
阿里云默認(rèn)yum命令下的MySQL是5.17****,安裝mysql5.7之前先卸載以前的版本。下面通過(guò)本文給大家介紹阿里云ECS centos6.8下安裝配置MySql5.7的教程,需要的的朋友參考下吧2017-07-07