MySQL事務(wù)原理分析——所以鎖
共享鎖也成為讀鎖,針對同一份數(shù)據(jù),多個事務(wù)的讀操作可以同時進行而不會互相影響,相互不阻塞的。
通過下面命令加共享鎖
SELECT...LOCK IN SHARE MODE
#或
SELECT...FOR SHARE;#(8.0新增語法)
排他鎖
排他鎖也叫寫鎖,當(dāng)一個事務(wù)對一份數(shù)據(jù)執(zhí)行寫入,即加上排他鎖后,其他事務(wù)對同一份數(shù)據(jù)進行讀寫操作會阻塞,直到前一個事務(wù)提交。
通過下面的命令加排他鎖
SELECT ... FOR UPDATE;
DELETE
、UPDATE
、INSERT
等操作也相當(dāng)于加排他鎖
共享鎖和排他鎖是否會發(fā)生阻塞如下圖所示:
共享鎖排他鎖共享鎖不阻塞阻塞排他鎖阻塞阻塞
行級鎖
行級鎖是鎖住行,粒度小,性能較高,但是行級鎖只在存儲引擎層實現(xiàn),行級鎖分為3種,記錄鎖、間隙鎖和臨鍵鎖。
假如下面的一個表test_lock
:
idab000444888161616323232
id是主鍵索引
a是普通索引
b是普通列
記錄鎖 (Record Locks)
記錄鎖是僅僅鎖住一條記錄,鎖的粒度最小。
什么時候會加記錄鎖?
當(dāng)用唯一索引進行等職查詢時,且查詢的記錄是存在的時候,會加記錄鎖。
會話1會話2會話3begin;select * from test_lock where id = 16 for update;update test_lock set a = 100 where id = 16;(阻塞)insert into test_lock value(9, 9, 9);(正常)
會話1對
id=16
記錄加了行鎖會話2阻塞,無法對這條記錄進行修改操作
會話3正常插入
間隙鎖(Gap Locks)
大家還記得并發(fā)事務(wù)中會出現(xiàn)"幻讀"的問題嗎?就是事務(wù)期間,其他事務(wù)添加一條數(shù)據(jù),再次讀取突然多出一條記錄。為了解決這樣的問題,我們是不是可以對一段區(qū)間的數(shù)據(jù)加鎖,加上鎖以后,其他事務(wù)添加數(shù)據(jù)時必須阻塞。像這樣的鎖就叫做間隙鎖,即鎖定一個區(qū)間,左開右開。
什么情況會加 間隙鎖 ?
在用唯一索引進行等值查詢時,當(dāng)查詢記錄不存在時,會加間隙鎖。
select * from test_lock where id = 10 for update;
id=10位于8到16區(qū)間,由于10這條記錄不存在,所以加的間隙鎖,鎖定(8, 16)
的區(qū)間。
唯一鎖引使用范圍查詢的時候,會加間歇鎖。
select * from test_lock where id <10 and id> 8 for update;
id <10 and id> 8
是一個范圍查詢,會鎖定范圍(8,16)
。
普通索引等值查詢時,如果記錄存在,會額外添加一個間隙鎖。
select * from test_lock where a = 8 for update;
由于a=8
記錄存在,會對范圍(4,8]
添加臨鍵鎖,這個后面會提到,同時額外向下遍歷到第一個不符合條件的值才能停止,因此間隙鎖的范圍是(8,16)
。
通索引等值查詢時,如果記錄不存在,會加一個間隙鎖。
select * from test_lock where a = 10 for update;
此種情況鎖定的范圍為(8,16)
臨鍵鎖(Next-Key Locks)
如果想要同時集合上面的記錄鎖和間隙鎖,也就是既想鎖住某條記錄,又想阻止其他事務(wù)在該記錄前邊的間隙插入新記錄,所以InnoDB就提出了臨鍵鎖(Next-Key Locks
),默認(rèn)鎖定的范圍是左開右閉。InnoDB
存儲引擎默認(rèn)的鎖單位就是臨鍵鎖(Next-Key Locks
),怎么理解呢?
也就是鎖加鎖都是按照臨鍵鎖加鎖,但是會根據(jù)一定的規(guī)律退化為記錄鎖和間隙鎖。具體規(guī)律如下:
唯一索引等值查詢:
當(dāng)查詢的記錄是存在的,臨鍵鎖會退化成「記錄鎖」。
當(dāng)查詢的記錄是不存在的,臨鍵鎖 會退化成「間隙鎖」。
非唯一索引等值查詢:
當(dāng)查詢的記錄存在時,除了會加 臨鍵鎖外,還額外加間隙鎖,也就是會加兩把鎖。
當(dāng)查詢的記錄不存在時,只會加 臨鍵鎖,然后會退化為間隙鎖,也就是只會加一把鎖。
InnoDB存儲引擎中,如果一個表查詢或者更新沒有走索引,這時候還會創(chuàng)建行級鎖嗎? 答案是不會,這時候會升級為表級鎖。
頁級鎖
頁級鎖是 MySQL 中鎖定粒度介于行級鎖和表級鎖中間的一種鎖。表級鎖速度快,但沖突多,行級沖突少,但速度慢。因此,采取了折衷的頁級鎖,一次鎖定相鄰的一組記錄。BDB (BerkeleyDB)
存儲引擎 支持頁級鎖。
特點
開銷和加鎖時間界于表鎖和行鎖之間
會出現(xiàn)死鎖
鎖定粒度界于表鎖和行鎖之間,并發(fā)度一般
表級鎖
表鎖會鎖定整張表,它是MySQL中最基本的鎖策略,并不依賴于存儲引擎,表鎖是開銷最小的策略。因為表級鎖一次會將整個表鎖定,所以可以很好的避免死鎖問題。但是表鎖的并發(fā)度很差,那表鎖都有哪幾種呢?
表級別的S鎖、X鎖
LOCK TABLES t READ
:InnoDB存儲引擎會對表 t 加表級別的 S鎖 。LOCK TABLES t WRITE
:InnoDB存儲引擎會對表 t 加表級別的 X鎖 。
意向鎖
意向鎖也是一種表鎖,表示某個事務(wù)正在鎖定一行或者將要鎖定一行,表明一個意圖。它不與行級鎖沖突。那它究竟有啥作用?
意向鎖是在當(dāng)事務(wù)加表鎖時發(fā)揮作用。比如一個事務(wù)想要對表加排他鎖,如果沒有意向鎖的話,那么該事務(wù)在加鎖前需要判斷當(dāng)前表的每一行是否已經(jīng)加了鎖,如果表很大,遍歷每行進行判斷需要耗費大量的時間。如果使用意向鎖的話,那么加表鎖前,只需要判斷當(dāng)前表是否有意向鎖即可,這樣加快了對表鎖的處理速度。
意向鎖分為兩種:
意向共享鎖(intention shared lock, IS):事務(wù)有意向?qū)Ρ碇械哪承┬屑庸蚕礞i(S鎖)
意向排他鎖(intention exclusive lock, IX):事務(wù)有意向?qū)Ρ碇械哪承┬屑优潘i(X鎖)
自增鎖(AUTO-INC LOCK)
我們都知道在使用創(chuàng)建表的時候有自增主鍵AUTO_INCREMENT
屬性,那它是怎么實現(xiàn)自增的呢?
AUTO-INC
鎖是當(dāng)向使用含有AUTO_INCREMENT
列的表中插入數(shù)據(jù)時需要獲取的一種特殊的表級鎖,在執(zhí)行插入語句時就在表級別加一個AUTO-INC
鎖,然后為每條待插入記錄的AUTO_INCREMENT
修飾的列分配遞增的值,在該語句執(zhí)行結(jié)束后,再把AUTO-INC
鎖釋放掉。
元數(shù)據(jù)鎖(MDL鎖)
元數(shù)據(jù)鎖可以用來保證讀寫的正確性。比如,如果一個查詢正在遍歷一個表中的數(shù)據(jù)而執(zhí)行期間另一個線程對這個 表結(jié)構(gòu)做變更 ,增加了一列,那么查詢線程拿到的結(jié)果跟表結(jié)構(gòu)對不上,肯定是不行的。
當(dāng)對一個表做增刪改查操作的時候,加 MDL讀鎖;
當(dāng)要對表做結(jié)構(gòu)變更操作的時候,加 MDL 寫鎖。
MDL讀鎖和讀鎖之間可以共享兼容,讀鎖和寫鎖之間不兼容,會互相阻塞。
全局鎖
全局鎖就是對 整個數(shù)據(jù)庫實例 加鎖。當(dāng)你需要讓整個庫處于 只讀狀態(tài) 的時候,可以使用這個命令,之后 其他線程的以下語句會被阻塞:數(shù)據(jù)更新語句(數(shù)據(jù)的增刪改)、數(shù)據(jù)定義語句(包括建表、修改表結(jié) 構(gòu)等)和更新類事務(wù)的提交語句。
全局鎖的典型使用場是做全庫邏輯備份。
## 全局鎖命令
flush tables with read lock
悲觀鎖
悲觀鎖是總以最壞的情況假設(shè),比如操作一條數(shù)據(jù),總認(rèn)為也有其他線程要拿這條數(shù)據(jù),那就給這條數(shù)據(jù)上排他鎖,讓其他事務(wù)或者線程阻塞,類似于Java中 synchronized 和 ReentrantLock 等獨占鎖的思想。
適用場景:
悲觀鎖適寫操作多的場景,因為寫的操作具有 排它性 。采用悲觀鎖的方式,可以在數(shù)據(jù)庫層 面阻止其他事務(wù)對該數(shù)據(jù)的操作權(quán)限,防止讀 - 寫和寫 - 寫的沖突。
實現(xiàn)思路:
以秒殺商品為例,為了防止超賣,需要加鎖。
#第1步: for update 方式查出商品庫存
select quantity from items where id 1001 for update;
#第2步:如果庫存大于0,則根據(jù)商品信息生產(chǎn)訂單
insert into orders (item_id)values(1001);
#第3步:修改商品的庫存,um表示購買數(shù)量
update items set quantity quantity-num where id 1001;
select····for update
是MySQL中悲觀鎖。此時在items表中,id為1001的那條數(shù)據(jù)就被我們鎖定了,其他的要執(zhí)行select quantity from items where id=1001 for update;
語句的事務(wù)必須等本次事務(wù)提交之后才能執(zhí)行。這樣我們可以保證每次事務(wù)能拿到最新的庫存數(shù)量,從而不會超賣,但是這樣的性能很差。
樂觀鎖
樂觀鎖認(rèn)為一個事務(wù)發(fā)生并發(fā)的概率很小,就不加通過數(shù)據(jù)庫加鎖實現(xiàn),因為加鎖性能比較差,而是通過程序?qū)崿F(xiàn),那如何數(shù)據(jù)沒有被其他事務(wù)修改了呢?會在更新數(shù)據(jù)的時候判斷數(shù)據(jù)的版本或者時間戳是否發(fā)生變化。
實現(xiàn)思路:
版本號機制實現(xiàn)樂觀鎖
數(shù)據(jù)表中新增一個version字段
更新前讀取出version字段
進行業(yè)務(wù)邏輯操作,更新數(shù)據(jù),
UPDATE ... SET version=version+1 WHERE version=version
,版本+1
時間戳機制實現(xiàn)樂觀鎖
時間戳和版本號機制一樣,也是在更新提交的時候,將當(dāng)前數(shù)據(jù)的時間戳和更新之前取得的時間戳進行 比較,如果兩者一致則更新成功,否則就是版本沖突。
適用場景:
樂觀鎖適合讀操作多
C++后端開發(fā) 面試題、學(xué)習(xí)資料、教學(xué)視頻和學(xué)習(xí)路線圖(資料包括C/C++,Linux,golang技術(shù),Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協(xié)程,DPDK,ffmpeg等),免費分享有需要的可以自行添加學(xué)習(xí)交流群739729163 領(lǐng)取