MySQL中有哪些鎖?
一、常用引擎間的區別
MyISAM 操作資料都是使用的表鎖,你更新一條記錄就要鎖整個表,導致效能較低,併發不高。當然同時它也不會存在死鎖問題。
而 InnoDB 與 MyISAM 的最大不同有兩點:一是 InnoDB 支援事務;二是 InnoDB 採用了行級鎖。
在 Mysql 中,行級鎖並不是直接鎖記錄,而是鎖索引。索引分為主鍵索引和非主鍵索引兩種,如果一條sql 語句操作了主鍵索引,Mysql 就會鎖定這條主鍵索引;如果一條語句操作了非主鍵索引,MySQL會先鎖定該非主鍵索引,再鎖定相關的主鍵索引。
InnoDB 行鎖是通過給索引項加鎖實現的,如果沒有索引,InnoDB 會通過隱藏的聚簇索引來對記錄加鎖。也就是說:如果不通過索引條件檢索資料,那麼InnoDB將對錶中所有資料加鎖,實際效果跟表鎖一樣
二、鎖的種類
資料庫裡有的鎖有很多種,為了方面理解,所以我根據其相關性"人為"的對鎖進行了一個分類,分別如下
基於鎖的屬性分類:共享鎖、排他鎖。
基於鎖的粒度分類:表鎖、行鎖、記錄鎖、間隙鎖、臨鍵鎖。
基於鎖的狀態分類:意向共享鎖、意向排它鎖。
還有死鎖.....
三、樂觀鎖與悲觀鎖
雖然說,樂觀鎖和悲觀鎖不在MySQL鎖分類中,但是不代表我們就不去管它們,畢竟這兩個鎖是我們大多數人最新聽說大鎖。
(一)樂觀鎖
樂觀鎖不是資料庫自帶的,需要我們自己去實現。樂觀鎖是指操作資料庫時(更新操作),想法很樂觀,認為這次的操作不會導致衝突,在操作資料時,並不進行任何其他的特殊處理(也就是不加鎖),而在進行更新後,再去判斷是否有衝突了。
通常實現是這樣的:在表中的資料進行操作時(更新),先給資料表加一個版本(version)欄位,每操作一次,將那條記錄的版本號加1。也就是先查詢出那條記錄,獲取出version欄位,如果要對那條記錄進行操作(更新),則先判斷此刻version的值是否與剛剛查詢出來時的version的值相等,如果相等,則說明這段期間,沒有其他程式對其進行操作,則可以執行更新,將version欄位的值加1;如果更新時發現此刻的version值與剛剛獲取出來的version的值不相等,則說明這段期間已經有其他程式對其進行操作了,則不進行更新操作。
比如:我們要對商品表t_goods中的某個商品已經下架,此時我們不是去刪除這條記錄,而是去改變該商品的狀態即可。另外,我們在商品表中加了一個欄位version,我們先是通過比較當前的version和商品id是否一致,一致就修改狀態為2,否則不不修改。
update t_goods set status=2,version=version+1 where id=#{id} and version=#{version};
看這個是不是想起了併發程式設計中的CAS,比較然後替換。
樂觀鎖相對簡單些,下面,我們來聊聊悲觀鎖。
(二)悲觀鎖
與樂觀鎖相對應的就是悲觀鎖了。悲觀鎖就是在操作資料時,認為此操作會出現資料衝突,所以在進行每次操作時都要通過獲取鎖才能進行對相同資料的操作,這點跟java中的synchronized很相似,所以悲觀鎖需要耗費較多的時間。另外與樂觀鎖相對應的,悲觀鎖是由資料庫自己實現了的,要用的時候,我們直接呼叫資料庫的相關語句就可以了。
說到這裡,由悲觀鎖涉及到的另外兩個鎖概念就出來了,它們就是共享鎖與排它鎖。共享鎖和排它鎖是悲觀鎖的不同的實現,它倆都屬於悲觀鎖的範疇。
接著,我們來聊聊共享鎖與排它鎖。
四、共享鎖與排它鎖
資料庫的增刪改操作預設都會加排他鎖,而查詢不會加任何鎖!!!!!!!!!!!!!!!!!!!!!
共享鎖:對某一資源加共享鎖,自身可以讀該資源,其他人也可以讀該資源(也可以再繼續加共享鎖,即 共享鎖可多個共存),但無法修改。要想修改就必須等所有共享鎖都釋放完之後。
排他鎖:對某一資源加排他鎖,自身可以進行增刪改查,其他人無法進行任何操作。
//共享鎖 select * from 表名 lock in share mode //排他鎖 select * from 表名 for update
(一)共享鎖
共享鎖指的就是對於多個不同的事務,對同一個資源共享同一個鎖。共享單詞是share,因此,也稱之為S鎖。相當於對於同一把門,它擁有多個鑰匙一樣。
剛剛說了,對於悲觀鎖,一般資料庫已經實現了,共享鎖也屬於悲觀鎖的一種,那麼共享鎖在MySQL中是通過什麼命令來呼叫呢。通過查詢資料,瞭解到通過在執行語句後面加上 lock in share mode就代表對某些資源加上共享鎖了。
比如:
SELECT id,t_name,t_status from t_goods where id = "1" lock in share mode;
(二)排它鎖
排他鎖又稱寫鎖(eXclusive Lock),簡稱X鎖,當一個事務為資料加上寫鎖時,其他請求將不能再為資料加任何鎖,直到該鎖釋放之後,其他事務才能對資料進行加鎖。
排他鎖的目的是在資料修改時候,不允許其他人同時修改,也不允許其他人讀取。避免了出現髒資料和髒讀的問題。
比如:
SELECT id,t_name,t_status from t_goods where id = "1" for update
排他鎖就是不能與其他鎖並存,如一個事務獲取了一個數據行的排他鎖,其他事務就不能再獲取該行的其他鎖。
五、表鎖與行鎖
我們首先來了解一下表鎖和行鎖:表鎖是指對一整張表加鎖,一般是 DDL 處理時使用;而行鎖則是鎖定某一行或者某幾行,或者行與行之間的間隙。
(一)行鎖
表鎖由 MySQL Server 實現,行鎖則是儲存引擎實現,不同的引擎實現的不同。在 MySQL的常用引擎中InnoDB 支援行鎖,而 MyISAM則只能使用 MySQL Server提供的表鎖。
簡言之,表鎖是指上鎖的時候鎖住的是整個表,當下一個事務訪問該表的時候,必須等前一個事務釋放了鎖才能進行對錶進行訪問。特點就是粒度大、加鎖簡單、容易衝突。
表鎖由 MySQL Server 實現,一般在執行 DDL 語句時會對整個表進行加鎖,比如說 ALTER TABLE 等操作。在執行 SQL 語句時,也可以明確指定對某個表進行加鎖。
比如:
mysql> lock table user read(write); # 分為讀鎖和寫鎖 mysql> select * from user where id = 100; # 成功 mysql> select * from role where id = 100; # 失敗,未提前獲取該 role的讀表鎖 mysql> update user set name = 'Tom' where id = 100; # 失敗,未提前獲得user的寫表鎖 mysql> unlock tables; # 顯示釋放表鎖表鎖使用的是一次性鎖技術,也就是說,在會話開始的地方使用 lock 命令將後續需要用到的表都加上鎖,在表釋放前,只能訪問這些加鎖的表,不能訪問其他表,直到最後通過 unlock tables 釋放所有表鎖。
除了使用 unlock tables 顯示釋放鎖之外,會話持有其他表鎖時執行lock table 語句會釋放會話之前持有的鎖;會話持有其他表鎖時執行 start transaction 或者 begin 開啟事務時,也會釋放之前持有的鎖。
(二)表鎖
mysql> lock table user read(write); # 分為讀鎖和寫鎖Query OK, 0 rows affected (0.00 sec) mysql> select * from user where id = 100; # 成功m ysql> select * from role where id = 100; # 失敗,未提前獲取該 role的讀表鎖 mysql> update user set name = where id = 100; # mysql> unlock tables; # OK, 0 rows affected (0.00 sec)
(三)行鎖的模式
鎖的模式有:
- 讀意向鎖
- 寫意向鎖
- 讀鎖
- 寫鎖
- 自增鎖(auto_inc)
很多人喜歡說InnoDb是行鎖,這裡糾正一下:
行鎖在 InnoDB 中是基於索引實現的,所以一旦某個加鎖操作沒有使用索引,那麼該鎖就會退化為表鎖。
別急,下面我們一個一個來搞清楚。
1、讀寫鎖
讀鎖,又稱共享鎖(Share locks,簡稱 S 鎖),加了讀鎖的記錄,所有的事務都可以讀取,但是不能修改,並且可同時有多個事務對記錄加讀鎖。
寫鎖,又稱排他鎖(Exclusive locks,簡稱 X 鎖),或獨佔鎖,對記錄加了排他鎖之後,只有擁有該鎖的事務可以讀取和修改,其他事務都不可以讀取和修改,並且同一時間只能有一個事務加寫鎖。
2、意向鎖
鎖定允許事務在行級上的鎖和表級上的鎖同時存在。為了支援在不同粒度上進行加鎖操作,InnoDB儲存引擎支援一種額外的鎖方式。
釋義:
意向共享鎖(IS):事務想要在獲得表中某些記錄的共享鎖,需要在表上先加意向共享鎖。
意向排它鎖(IX):事務想要在獲得表中某些記錄的排它鎖,需要在表上先加意向排它鎖。
意向共享鎖和意向排它鎖總稱為意向鎖。意向鎖的出現是為了支援Innodb支援多粒度鎖。
首先,意向鎖是表級別鎖。
理由:當我們需要給一個加表鎖的時候,我們需要根據意向鎖去判斷表中有沒有資料行被鎖定,以確定是否能加成功。如果意向鎖是行鎖,那麼我們就得遍歷表中所有資料行來判斷。如果意向鎖是表鎖,則我們直接判斷一次就知道表中是否有資料行被鎖定了。所以說將意向鎖設定成表級別的鎖的效能比行鎖高的多。
所以,意向鎖的作用就是:
當一個事務在需要獲取資源的鎖定時,如果該資源已經被排他鎖佔用,則資料庫會自動給該事務申請一個該表的意向鎖。如果自己需要一個共享鎖定,就申請一個意向共享鎖。如果需要的是某行(或者某些行)的排他鎖定,則申請一個意向排他鎖。
3、自增鎖
AUTOINC鎖又叫自增鎖(一般簡寫成 AI 鎖),是一種表鎖,當表中有自增列(AUTOINCREMENT)時出現。當插入表中有自增列時,資料庫需要自動生成自增值,它會先為該表加AUTOINC 表鎖,阻塞其他事務的插入操作,這樣保證生成的自增值肯定是唯一的。
AUTOINC鎖具有如下特點:
- AUTO_INC 鎖互不相容,也就是說同一張表同時只允許有一個自增鎖;
- 自增值一旦分配了就會 +1,如果事務回滾,自增值也不會減回去,所以自增值可能會出現中斷的情況。
顯然,AUTOINC 表鎖會導致併發插入的效率降低,為了提高插入的併發性,MySQL從 5.1.22 版本開始,引入了一種可選的輕量級鎖(mutex)機制來代替AUTOINC 鎖,可以通過引數 innodbautoinclockmode 來靈活控制分配自增值時的併發策略。
比如:
CREATE TABLE `t_user` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_name` varchar(255) , `password` varchar(255) , `email` varchar(255), `age` int, PRIMARY KEY (`id`) USING BTREE ) ENGINE=InnoDB =1 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;從上面的建表語句中可以看出,t_user表中的id就用到了自增。
(四)行鎖型別
根據鎖的粒度可以把鎖細分為表鎖和行鎖,行鎖根據場景的不同又可以進一步細分。下面要說的三種鎖,也是我們面試中的加分項,所以很有必要來聊聊。
- 記錄鎖(Record Locks)
- 間隙鎖(Gap Locks)
- 臨鍵鎖(Next-Key Locks)
1、記錄鎖
記錄鎖就是為某行記錄加鎖,它封鎖該行的索引記錄:
-- id 列為主鍵列或唯一索引列
SELECT * FROM t_user WHERE id = 1 FOR UPDATE;
id 為 1 的記錄行會被鎖住。
需要注意的是:id 列必須為唯一索引列或主鍵列,否則上述語句加的鎖就會變成臨鍵鎖。
同時查詢語句必須為精準匹配(=),不能為 >、<、like等,否則也會退化成臨鍵鎖。
我們也可以在通過 主鍵索引 與 唯一索引 對資料行進行 UPDATE 操作時,也會對該行資料加記錄鎖:
-- id UPDATE t_user SET age = 50 WHERE id = 1;
其實。還是蠻簡單的哈,但是很多人估計也還都不知道。
2、間隙鎖
間隙鎖基於非唯一索引,它鎖定一段範圍內的索引記錄。間隙鎖基於下面將會提到的Next-Key Locking 演算法,請務必牢記:使用間隙鎖鎖住的是一個區間,而不僅僅是這個區間中的每一條資料。
SELECT * FROM t_user WHERE id BETWEN 1 AND 10 FOR UPDATE;
即所有在(1,10)區間內的記錄行都會被鎖住,所有id 為 2、3、4、5、6、7、8、9 的資料行的插入會被阻塞,但是 1 和 10 兩條記錄行並不會被鎖住。
除了手動加鎖外,在執行完某些 SQL後,InnoDB也會自動加間隙鎖。
3、臨鍵鎖
臨鍵鎖是一種特殊的間隙鎖,也可以理解為一種特殊的演算法。通過臨建鎖可以解決幻讀的問題。每個資料行上的非唯一索引列上都會存在一把臨鍵鎖,當某個事務持有該資料行的臨鍵鎖時,會鎖住一段左開右閉區間的資料。需要強調的一點是,InnoDB 中行級鎖是基於索引實現的,臨鍵鎖只與非唯一索引列有關,在唯一索引列(包括主鍵列)上不存在臨鍵鎖。
比如:表資訊 t_user(id PK, age KEY, name)
該表中 age 列潛在的臨鍵鎖有:
在事務 A 中執行如下命令:
-- 根據非唯一索引列 UPDATE 某條記錄 UPDATE t_user SET name = Vladimir WHERE age = 24; -- 或根據非唯一索引列 鎖住某條記錄 SELECT * FROM t_user WHERE age = 24 FOR UPDATE;不管執行了上述 SQL 中的哪一句,之後如果在事務 B 中執行以下命令,則該命令會被阻塞:
INSERT INTO t_user VALUES(100, 26, 'tian');
很明顯,事務 A 在對 age 為 24 的列進行 UPDATE 操作的同時,也獲取了 (24, 32] 這個區間內的臨鍵鎖。
不僅如此,在執行以下 SQL 時,也會陷入阻塞等待:
INSERT INTO table VALUES(100, 30, 'zhang');
那最終我們就可以得知,在根據非唯一索引 對記錄行進行 UPDATE \ FOR UPDATE \ LOCK IN SHARE MODE 操作時,InnoDB 會獲取該記錄行的 臨鍵鎖 ,並同時獲取該記錄行下一個區間的間隙鎖。
即事務 A在執行了上述的 SQL 後,最終被鎖住的記錄區間為 (10, 32)。
六、死鎖
死鎖是併發系統中常見的問題,同樣也會出現在資料庫MySQL的併發讀寫請求場景中。當兩個及以上的事務,雙方都在等待對方釋放已經持有的鎖或因為加鎖順序不一致造成迴圈等待鎖資源,就會出現“死鎖”。常見的報錯資訊為 ” Deadlock found when trying to get lock...”。
從死鎖的定義來看,MySQL 出現死鎖的幾個要素為:
- 兩個或者兩個以上事務
- 每個事務都已經持有鎖並且申請新的鎖
- 鎖資源同時只能被同一個事務持有或者不相容
- 事務之間因為持有鎖和申請鎖導致彼此迴圈等待
(一)死鎖分析思路
大致分為兩個步驟:
- 檢視死鎖日誌時,首先看一下發生死鎖的事務等待獲取鎖的語句都是啥。
- 找到發生死鎖的事務中所有的語句之後,對照著事務獲取到的鎖和正在等待的鎖的資訊來分析死鎖發生過程。
(二)如何預防死鎖?
1、innodb_lock_wait_timeout 等待鎖超時回滾事務
直觀方法是在兩個事務相互等待時,當一個等待時間超過設定的某一閥值時,對其中一個事務進行回滾,另一個事務就能繼續執行。
2、wait-for graph演算法來主動進行死鎖檢測
每當加鎖請求無法立即滿足需要並進入等待時,wait-for graph演算法都會被觸發。
3、wait-for graph要求資料庫儲存以下兩種資訊:
- 鎖的資訊連結串列
- 事務等待連結串列
(三)那麼如何解決死鎖?
1、等待事務超時,主動回滾。
2、進行死鎖檢查,主動回滾某條事務,讓別的事務能繼續走下去。
下面提供一種方法,解決死鎖的狀態:
SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX;--檢視正在被鎖的事務
kill trx_mysql_thread_id;--(上圖trx_mysql_thread_id列的值)
七、總結
文章比較長,能認真看到這裡的你已經非常棒了。文中一共講了13中鎖:
樂觀鎖
悲觀鎖
行鎖
表鎖
共享鎖
排它鎖
間隙鎖
記錄鎖
臨鍵鎖
讀寫鎖
意向排它鎖
意向共享鎖
死鎖