MySQL裏有很是多鎖的概念,常常能夠聽到的有:樂觀鎖、悲觀鎖、行鎖、表鎖、Gap鎖(間隙鎖)、MDL鎖(元數據鎖)、意向鎖、讀鎖、寫鎖、共享鎖、排它鎖。這麼鎖一聽就讓人頭大,因而去看一些博客,有些講樂觀鎖、悲觀鎖,有些在講讀鎖、寫鎖,因而樂觀鎖和悲觀鎖好像理解了,讀鎖寫鎖好像也理解了,可是我任然不知道怎麼用,也不知道樂觀鎖與讀鎖寫鎖有沒有什麼關係?再看了不少文章後,逐漸弄懂了它們之間的關係,因而寫下這篇文章來梳理思路。能力有限,不免有誤,請酌情參考。html
雖然上面列舉了不少鎖的名詞,可是這些鎖其實並非在同一個維度上的,這就是我之因此含糊不清的緣由。接下來從不一樣的維度來分析 MySQL 的鎖。mysql
首先讀鎖還有一個名稱叫共享鎖,寫鎖也相應的還有個名稱叫排它鎖,也就是說共享鎖和讀鎖是同一個東西,排它鎖和寫鎖是同一個東西。讀鎖、寫鎖是系統實現層面上的鎖,也是最基礎的鎖。讀鎖和寫鎖仍是鎖的一種性質,好比行鎖裏,有行寫鎖和行讀鎖。MDL 鎖裏也有 MDL 寫鎖和 MDL 讀鎖。讀鎖和寫鎖的加鎖關係以下,Y 表示能夠共存,X 表示互斥。git
讀鎖 | 寫鎖 | |
---|---|---|
讀鎖 | Y | X |
寫鎖 | X | X |
從這個表格裏能夠知道讀鎖和寫鎖不能共存,請考慮這樣一個場景,一個請求佔用了讀鎖,這時又來了一個請求要求加寫鎖,可是資源已經被讀鎖佔據,寫鎖阻塞。這樣本沒有問題,可是若是後續不斷的有請求佔用讀鎖,讀鎖一直沒有釋放,形成寫鎖一直等待。這樣寫鎖就被餓死了,爲了不這種狀況發生,數據庫作了優化,當有寫鎖阻塞時,後面的讀鎖也會阻塞,這樣就避免了餓死現象的發生。後面還會再次提到這個現象。github
以前的文章已經介紹了 MySQL 的存儲模型,對於 InnoDB 引擎而言,採用的是 B+ 樹索引,假設須要將整個表鎖住那麼須要在整個 B+ 樹的每一個節點上都加上鎖,顯然這是個很是低效的作法。所以,MySQL 提出了意向鎖的概念,意向鎖就是若是要在一個節點上加鎖就必須在其全部的祖先節點加上意向鎖。關於意向鎖還有更多複雜設計,若是想了解能夠查看 《數據庫系統機率》 一書。算法
表鎖和行鎖是兩種不一樣加鎖粒度的鎖。除了表鎖和行鎖之外還有更大粒度的鎖——全局鎖。sql
全局鎖: 全局鎖會鎖住整個數據庫,MySQL 使用 flush tables with read lock 命令來加全局鎖,使用 unlock tables 解鎖。線程退出後鎖也會自動釋放。當加上全局鎖之後,除了當前線程之外,其餘線程的更新操做都會被阻塞,包括增刪改數據表中的數據、建表、修改表結構等。全局鎖的典型使用場景是全庫的邏輯備份。數據庫
表鎖: 表鎖會鎖住一張表,MySQL 使用 lock tables 編程
read/write 命令給表加上讀鎖或寫鎖,經過 unlock tables 命令釋放表鎖。經過 lock tables t read 給表 t 加上讀鎖後,當前線程只能訪問表 t,不能訪問數據庫中的其餘表,對錶 t 也只有讀權限,不能進行修改操做。經過 lock tables t write 給表 t 加上寫鎖後,當前線程只能訪問表 t,不能訪問數據庫中的其餘表,對錶 t 有讀寫權限。行鎖: 行鎖會鎖鎖住表中的某一行或者多行,MySQL 使用 lock in share mode 命令給行加讀鎖,用 for update 命令給行加寫鎖,行鎖不須要顯示釋放,當事務被提交時,該事務中加的行鎖就會被釋放。經過 select k from t where k = 1 for update 命令能夠鎖住 k 爲 1 的全部行。另外當使用 update 命令更新表數據時,會自動給命中的行加上行鎖。另外 MySQL 加行鎖時並非一次性把全部的行都加上鎖,執行一個 update 命令以後,server 層將命令發送給 InnoDB 引擎,InnoDB 引擎找到第一條知足條件的數據,並加鎖後返回給 server 層,server 層更新這條數據而後傳給 InnoDB 引擎。完成這條數據的更新後,server 層再取下一條數據。segmentfault
咱們用一個例子來驗證這個過程,首先執行以下命令建表並插入幾行數據bash
mysql-> create table t(id int not null auto_increment, c int not null, primary key(id))ENGINE=InnoDB;
mysql-> insert into t(id, c) values (1, 1), (2, 2), (3, 3);
複製代碼
事務 A | 事務 B | 事務 C |
---|---|---|
begin | ||
select * from t where id = 3 for update; | ||
update t set c = 0 where id = c; | ||
set session transaction isolation level READ UNCOMMITTED; select * from t; | ||
commit |
事務 A 執行 select * from t where id = 3 for update 將 id 等於3的行鎖住,事務 B 執行 update 命令的時候被阻塞。這時候再開啓事務 C,而且將事務 C 的隔離級別修改成未提交讀,獲得的以下表所示,發現前兩行已經被更新,最後 id 爲 3 的行沒有更新,說明事務 B 是阻塞在這裏了。
mysql> select * from t;
+----+---+
| id | c |
+----+---+
| 1 | 0 |
| 2 | 0 |
| 3 | 3 |
+----+---+
複製代碼
樂觀鎖老是假設不會發生衝突,所以讀取資源的時候不加鎖,只有在更新的時候判斷在整個事務期間是否有其餘事務更新這個數據。若是沒有其餘事務更新這個數據那麼本次更新成功,若是有其餘事務更新本條數據,那麼更新失敗。
悲觀鎖老是假設會發生衝突,所以在讀取數據時候就將數據加上鎖,這樣保證同時只有一個線程能更改數據。文章前面介紹的表鎖、行鎖等都是悲觀鎖。
樂觀鎖和悲觀鎖是兩種不一樣的加鎖策略。樂觀鎖假設的場景是衝突少,所以適合讀多寫少的場景。悲觀鎖則正好相反,合適寫多讀少的場景。樂觀鎖無需像悲觀鎖那樣維護鎖資源,作加鎖阻塞等操做,所以更加輕量化。
樂觀鎖的實現有兩種方式:版本號和 CAS 算法
版本號
經過版本號來實現樂觀鎖主要有如下幾個步驟:
1 給每條數據都加上一個 version 字段,表示版本號
2 開啓事務後,先讀取數據,並保存數據裏的版本號 version1,而後作其餘處理
3 最後更新的時候比較 version1 和數據庫裏當前的版本號是否相同。用 SQL 語句表示就是 update t set version = version + 1 where version = version1。 根據前面事務的文章咱們知道,update 操做時會進行當前讀,所以即便是在可重複讀的隔離級別下,也會取到到最新的版本號。若是沒有其餘事務更新過這條數據,那麼 version 等於 version1,因而更新成功。若是有其餘事務更新過這條數據,那麼 version 字段的值會被增長,那麼 version 不等於 version1,因而更新沒有生效。
CAS 算法
CAS 是 compare and swap 的縮寫,翻譯爲中文就是先比較而後再交換。CAS 實現的僞代碼:
<< atomic >>
bool cas(int* p, int old, int new)
{
if (*p != old)
{
return false
}
*p = new
return true
}
複製代碼
其中,p 是要修改的變量指針,old 是修改前的舊值,new 是將要寫入的新值。這段僞代碼的意思就是,先比較 p 所指向的值與舊值是否相同,若是不一樣說明數據已經被其餘線程修改過,返回 false。若是相同則將新值賦值給 p 所指向的對象,返回 true。這整個過程是經過硬件同步原語來實現,保證整個過程是原子的。
大多數語言都實現了 CAS 函數,好比 C 語言在 GCC 實現:
bool__sync_bool_compare_and_swap (type *ptr, type oldval type newval, ...)
type __sync_val_compare_and_swap (type *ptr, type oldval type newval, ...)
複製代碼
無鎖編程實際上也是經過 CAS 來實現,好比無鎖隊列的實現。CAS 的引入也帶來了 ABA 問題。關於 CAS 後面再開一篇專門的文章來總結無鎖編程。
MDL 鎖也是一種表級鎖,MDL 鎖不須要顯示使用。MDL 鎖是用來避免數據操做與表結構變動的衝突,試想當你執行一條查詢語句時,這個時候另外一個線程在刪除表中的一個字段,那麼二者就發生衝突了,所以 MySQL 在5.5版本之後加上了 MDL 鎖。當對一個表作增刪查改時會加 MDL 讀鎖,當對一個表作結構變動時會加 MDL 寫鎖。讀鎖相互兼容,讀鎖與寫鎖不能兼容。
MDL 須要注意的就是避免 MDL 寫鎖阻塞 MDL 讀鎖。
事務 A | 事務 B | 事務 C | 事務 D |
---|---|---|---|
select * from t | |||
select * from t | |||
alter table t add c int | |||
select * from t |
事務 A 執行 select 後給表 t 加 MDL 讀鎖。事務 B 執行 select 後給表再次加上 MDL 讀鎖,讀鎖和讀鎖能夠兼容。事務 C 執行 alter 命令時會阻塞,須要對錶 t 加 MDL 寫鎖。事務 C 被阻塞問題並不大,可是會致使後面全部的事務都被阻塞,好比事務 D。這是爲了不寫鎖餓死的狀況發生,MySQL 對加鎖所作的優化,當有寫鎖在等待的時候,新的讀鎖都須要等待。若是事務 C 長時間拿不到鎖,或者事務 C 執行的時間很長都會致使數據庫的操做被阻塞。
爲了不這種事情發生有如下幾點優化思路:
1 避免長事務。事務 A 和事務 B 若是是長事務就可能致使事務 C 阻塞在 MDL 寫鎖的時間比較長。
2 對於大表,修改表結構的語句能夠拆分紅多個小的事務,這樣每次修改表結構時佔用 MDL 寫鎖的時間會縮短。
3 給 alter 命令加等待超時時間
Gap 鎖是 InnoDB 引擎爲了不幻讀而引入的。在 MySQL的事務一文中已經談到,InnoDB 引擎在可重複讀隔離級別下能夠避免幻讀。間隙鎖就是鎖住數據行之間的間隙,避免新的數據插入進來。只有在進行當前讀的時候纔會加 gap 鎖。關於什麼是當前讀,能夠看個人上一篇文章《MySQL的事務》。
一條語句會如何加鎖,單純這一句話是沒法分析出來的。對加鎖的分析,必須結合事務的隔離級別和索引來看,阿里數據庫專家對此已經寫了很是詳細分析的文章,直接貼出來你們一塊兒學習,MySQL 加鎖處理分析
文章最後,放上我總結的 MySQL 的思惟導圖,算是對 MySQL 系列文章的一個總結。
[1] 數據庫系統概念(第6版)
[2] MySQL實戰45講,林曉斌
[3] 高性能MySQL(第3版)
[6] 樂觀鎖、悲觀鎖,這一篇就夠了!