一、前言
鎖是計算機在執行多線程或線程時用於併發訪問同一共享資源時的同步機制,MySQL中的鎖是在服務器層或者存儲引擎層實現的,保證了數據訪問的一致性與有效性。
MySQL鎖可以按模式分類爲:樂觀鎖與悲觀鎖。按粒度分可以分爲全局鎖、表級鎖、頁級鎖、行級鎖。按屬性可以分爲:共享鎖、排它鎖。按狀態分爲:意向共享鎖、意向排它鎖。按算法分爲:間隙鎖、臨鍵鎖、記錄鎖。
下面將會按照上圖進行一一講解。
二、全局鎖、表級鎖、頁級鎖、行級鎖
1. 全局鎖
(1) 概念
全局鎖就是對整個數據庫實例加鎖。
(2) 應用場景
全庫邏輯備份(mysqldump)
(3) 實現方式
MySQL 提供了一個加全局讀鎖的方法,命令是Flush tables with read lock (FTWRL)。
當你需要讓整個庫處於只讀狀態的時候,可以使用這個命令,之後其他線程的以下語句會被阻塞:數據更新語句(數據的增刪改)、數據定義語句(包括建表、修改表結構等)和更新類事務的提交語句。
風險點:
如果在主庫上備份,那麼在備份期間都不能執行更新,業務基本上就能停止。
如果在從庫上備份,那麼備份期間從庫不能執行主庫同步過來的binlog,會導致主從延遲。
解決辦法:
mysqldump使用參數--single-transaction,啓動一個事務,確保拿到一致性視圖。而由於MVCC的支持,這個過程中數據是可以正常更新的。
2. 表級鎖
(1) 概念
當前操作的整張表加鎖,最常使用的 MyISAM 與 InnoDB 都支持表級鎖定。
MySQL 裏面表級別的鎖有兩種:一種是表鎖,一種是元數據鎖(meta data lock,MDL)。
(2) 實現方式
表鎖:lock tables … read/write;
例如lock tables t1 read, t2 write; 命令,則其他線程寫 t1、讀寫 t2 的語句都會被阻塞。同時,線程 A 在執行 unlock tables 之前,也只能執行讀 t1、讀寫 t2 的操作。連寫 t1 都不允許,自然也不能在unlock tables之前訪問其他表。
元數據鎖:MDL 不需要顯式使用,在訪問一個表的時候會被自動加上,在 MySQL 5.5 版本中引入了 MDL,當對一個表做增刪改查操作的時候,加 MDL讀鎖;當要對錶做結構變更操作的時候,加 MDL 寫鎖。
(3) 風險點
參考於:https://www.cnblogs.com/keme/p/11065025.html
給一個表加字段,或者修改字段,或者加索引,需要掃描全表的數據。在對大表操作的時候,肯定會特別小心,以免對線上服務造成影響。而實際上,即使是小表,操作不慎也會出問題。
1. sessionA:
begin;
select * from t limit 1;
2. sessionB:
select * from t limit 1;
3. sessionC:
alter table t add f int;
#會mdl鎖住
4. sessionD:
select * from t limit 1;
我們可以看到 session A 先啓動,這時候會對錶 t 加一個 MDL 讀鎖。由於 session B 需要的也是 MDL 讀鎖,因此可以正常執行。
之後 session C 會被 blocked,是因爲 session A 的 MDL 讀鎖還沒有釋放,而 sessionC 需要MDL 寫鎖,因此只能被阻塞。
如果只有 session C 自己被阻塞還沒什麼關係,但是之後所有要在表 t 上新申請 MDL 讀鎖的請求也會被 session C 阻塞。前面說了,所有對錶的增刪改查操作都需要先申請MDL 讀鎖,而這時讀鎖沒有釋放,對錶alter ,產生了mdl寫鎖,把表t鎖住了,這時候就對錶t完全不可讀寫了。
如果某個表上的查詢語句頻繁,而且客戶端有重試機制,也就是說超時後會再起一個新session 再請求的話,這個庫的線程很快就會爆滿。
事務中的 MDL 鎖,在語句執行開始時申請,但是語句結束後並不會馬上釋放,而會等到整個事務提交後再釋放。
注 : 一般行鎖都有鎖超時時間。但是MDL鎖沒有超時時間的限制,只要事務沒有提交就會一直鎖住。
(4) 解決辦法
首先我們要解決長事務,事務不提交,就會一直佔着 MDL 鎖。在 MySQL 的information_schema 庫的 innodb_trx 表中,你可以查到當前執行中的事務。如果你要做 DDL 變更的表剛好有長事務在執行,要考慮先暫停 DDL,或者 kill 掉這個長事務。這也是爲什麼需要在低峯期做ddl 變更。
3. 頁級鎖
(1) 概念
頁級鎖是 MySQL 中鎖定粒度介於行級鎖和表級鎖中間的一種鎖。表級鎖速度快,但衝突多,行級衝突少,但速度慢。因此,採取了折衷的頁級鎖,一次鎖定相鄰的一組記錄。BDB 引擎支持頁級鎖。
4.行級鎖
(1) 概念
行級鎖是粒度最低的鎖,發生鎖衝突的概率也最低、併發度最高。但是加鎖慢、開銷大,容易發生死鎖現象。
MySQL中只有InnoDB支持行級鎖,行級鎖分爲共享鎖和排他鎖。
(2) 實現方式
在MySQL中,行級鎖並不是直接鎖記錄,而是鎖索引。索引分爲主鍵索引和非主鍵索引兩種,如果一條sql語句操作了主鍵索引,MySQL就會鎖定這條主鍵索引;如果一條語句操作了非主鍵索引,MySQL會先鎖定該非主鍵索引,再鎖定相關的主鍵索引。 在UPDATE、DELETE操作時,MySQL不僅鎖定WHERE條件掃描過的所有索引記錄,而且會鎖定相鄰的鍵值,即所謂的next-key locking。
(3) 實戰
我們演示一下行鎖的表現,分別在session1和session2中執行update操作,看會不會被鎖定。
可以看到由於session1遲遲未提交事務,session2在等待session1釋放鎖時出現了超過鎖定超時的警告了。
那麼如果session2執行id=2的操作會不會成功呢?
執行id=2的操作是可以成功的。
三、樂觀鎖和悲觀鎖
1. 樂觀鎖
(1) 概念
樂觀鎖是相對悲觀鎖而言的,樂觀鎖假設數據一般情況下不會造成衝突,所以在數據進行提交更新的時候,纔會正式對數據的衝突與否進行檢測,如果發現衝突了,則返回給用戶錯誤的信息,讓用戶決定如何去做。
(2) 應用場景
適用於讀多寫少,因爲如果出現大量的寫操作,寫衝突的可能性就會增大,業務層需要不斷重試,會大大降低系統性能。
(3) 實現方式
一般使用數據版本(Version)記錄機制實現,在數據庫表中增加一個數字類型的 “version” 字段來實現。當讀取數據時,將version字段的值一同讀出,數據每更新一次,對此version值加一。當我們提交更新的時候,判斷數據庫表對應記錄的當前版本信息與第一次取出來的version值進行比對,如果數據庫表當前版本號與第一次取出來的version值相等,則予以更新,否則認爲是過期數據。
(4) 實戰
訂單order表中id,status,version分別代表訂單ID,訂單狀態,版本號。
1.查詢訂單信息
select id,status,version from order where id=#{id};
2.用戶支付成功
3.修改訂單狀態
update set status=支付成功,version=version+1 where id=#{id} and version=#{ version}
2. 悲觀鎖
(1) 概念
悲觀鎖,正如其名,具有強烈的獨佔和排他特性,每次去拿數據的時候都認爲別人會修改,對數據被外界(包括本系統當前的其他事務,以及來自外部系統的事務處理)修改持保守態度,因此,在整個數據處理過程中,將數據處於鎖定狀態。
(2) 應用場景
適用於併發量不大、寫入操作比較頻繁、數據一致性比較高的場景。
(3) 實現方式
在MySQL中使用悲觀鎖,必須關閉MySQL的自動提交,set autocommit=0。共享鎖和排它鎖是悲觀鎖的不同的實現,它倆都屬於悲觀鎖的範疇。
(4) 實戰
商品goods表中id,name,number分別代表商品ID,商品名稱,商品庫存。
1.開啓事務並關閉自動提交
set autocommit=0;
2.查詢商品信息
select id,name,number from goods where id=1 for update;
3.用戶下單,生成訂單
4.修改商品庫存
update set number= number-1 where id=1;
5.提交事務
commit;
說明:select...for update是MySQL提供的實現悲觀鎖的方式,屬於排它鎖,在goods表中,id爲1的那條數據就被當前事務鎖定了,其它的要執行select id,name,number from goods where id=1 for update;的事務必須等本次事務提交之後才能執行。這樣我們可以保證當前的數據不會被其它事務修改。
注意:此時MySQL InnoDB默認行級鎖。行級鎖都是基於索引的,如果一條SQL語句用不到索引是不會使用行級鎖的,會使用表級鎖把整張表鎖住。
四、共享鎖和排它鎖
1. 共享鎖
(1) 概念
共享鎖,又稱之爲讀鎖,簡稱S鎖,當事務A對數據加上讀鎖後,其他事務只能對該數據加讀鎖,不能做任何修改操作,也就是不能添加寫鎖。只有當事務A上的讀鎖被釋放後,其他事務才能對其添加寫鎖。
(2) 應用場景
共享鎖主要是爲了支持併發的讀取數據而出現的,讀取數據時,不允許其他事務對當前數據進行修改操作,從而避免”不可重讀”的問題的出現。
適合於兩張表存在關係時的寫操作,拿mysql官方文檔的例子來說,一個表是child表,一個是parent表,假設child表的某一列child_id映射到parent表的c_child_id列,那麼從業務角度講,此時我直接insert一條child_id=100記錄到child表是存在風險的,因爲剛insert的時候可能在parent表裏刪除了這條c_child_id=100的記錄,那麼業務數據就存在不一致的風險。正確的方法是在插入時執行select * from parent where c_child_id=100 lock in share mode,鎖定了parent表的這條記錄,然後執行insert into child(child_id) values (100)就不會存在這種問題了。
(3) 實現方式
select … lock in share mode
(4) 實戰
session1持有共享鎖,未提交。session2的查詢不受影響,但是update操作會被一直阻塞,直到超時。
2. 排它鎖
(1) 概念
排它鎖,又稱之爲寫鎖,簡稱X鎖,當事務對數據加上寫鎖後,其他事務既不能對該數據添加讀寫,也不能對該數據添加寫鎖,寫鎖與其他鎖都是互斥的。只有當前數據寫鎖被釋放後,其他事務才能對其添加寫鎖或者是讀鎖。
MySQL InnoDB引擎默認update,delete,insert都會自動給涉及到的數據加上排他鎖,select語句默認不會加任何鎖類型。
(2) 應用場景
寫鎖主要是爲了解決在修改數據時,不允許其他事務對當前數據進行修改和讀取操作,從而可以有效避免”髒讀”問題的產生。
(3) 實現方式
select … for update
(4) 實戰
session1排它鎖查詢。session2也做排它鎖查詢會被阻塞。
五、意向共享鎖和意向排它鎖
1. 概念
意向鎖是表鎖,爲了協調行鎖和表鎖的關係,支持多粒度(表鎖與行鎖)的鎖並存。
2. 作用
當有事務A有行鎖時,MySQL會自動爲該表添加意向鎖,事務B如果想申請整個表的寫鎖,那麼不需要遍歷每一行判斷是否存在行鎖,而直接判斷是否存在意向鎖,增強性能。
3. 意向鎖的兼容互斥性
注意:這裏的排他 / 共享鎖指的都是表鎖!!!意向鎖不會與行級的共享 / 排他鎖互斥!!!
4. 實戰
session1獲取了某一行的排他鎖,並未提交:
select *from goods where id=1 for update;
此時 goods 表存在兩把鎖:goods 表上的意向排它鎖與 id 爲 1 的數據行上的排它鎖。
session2 想要獲取 goods 表的共享鎖:
LOCK TABLES goods READ;
此時session2 檢測session1 持有 goods 表的意向排他鎖,就可以得知session1必然持有該表中某些數據行的排他鎖,那麼session2 對 goods 表的加鎖請求就會被排斥(阻塞),而無需去檢測表中的每一行數據是否存在排它鎖。
六、間隙鎖、臨鍵鎖、記錄鎖
概念
記錄鎖、間隙鎖、臨鍵鎖都是排它鎖,而記錄鎖的使用方法跟排它鎖介紹一致。
記錄鎖
記錄鎖是封鎖記錄,記錄鎖也叫行鎖,例如:
select *from goods where `id`=1 for update;
它會在 id=1 的記錄上加上記錄鎖,以阻止其他事務插入,更新,刪除 id=1 這一行。
間隙鎖
間隙鎖基於非唯一索引,它鎖定一段範圍內的索引記錄。使用間隙鎖鎖住的是一個區間,而不僅僅是這個區間中的每一條數據。
select * from goods where id between 1 and 10 for update;
即所有在(1,10)區間內的記錄行都會被鎖住,所有id 爲 2、3、4、5、6、7、8、9 的數據行的插入會被阻塞,但是 1 和 10 兩條記錄行並不會被鎖住。
臨鍵鎖
臨鍵鎖,是記錄鎖與間隙鎖的組合,它的封鎖範圍,既包含索引記錄,又包含索引區間,是一個左開右閉區間。臨鍵鎖的主要目的,也是爲了避免幻讀(Phantom Read)。如果把事務的隔離級別降級爲RC,臨鍵鎖則也會失效。
每個數據行上的非唯一索引列上都會存在一把臨鍵鎖,當某個事務持有該數據行的臨鍵鎖時,會鎖住一段左開右閉區間的數據。需要強調的一點是,InnoDB 中行級鎖是基於索引實現的,臨鍵鎖只與非唯一索引列有關,在唯一索引列(包括主鍵列)上不存在臨鍵鎖。
goods表中隱藏的臨鍵鎖有:(-∞, 96], (96, 99], (99, +∞]
session1 在對 number 爲 96 的列進行 update 操作的同時,也獲取了(-∞, 96], (96, 99]這兩個區間內的臨鍵鎖。
最終我們就可以得知,在根據非唯一索引對記錄行進行 UPDATE \ FOR UPDATE \ LOCK IN SHARE MODE 操作時,InnoDB 會獲取該記錄行的臨鍵鎖,公式爲:左gap lock + record lock + 右gap lock。
即session1在執行了上述的 SQL 後,最終被鎖住的記錄區間爲 (-∞, 99)。