不一樣存儲引擎支持的鎖是不一樣的,好比MyISAM只有表鎖,而InnoDB既支持表鎖又支持行鎖。html
下圖展現了InnoDB不一樣鎖類型之間的關係:mysql
圖中的概念比較多很差理解,下面依次進行說明。算法
樂觀鎖是相對悲觀鎖而言的,樂觀鎖假設數據通常狀況下不會形成衝突,所在在數據進行提交更新時,纔會對數據的衝突與否進行檢測,若是發現衝突了,則返回給用戶錯誤信息,讓用戶決定如何處理,其核心是基於CAS算法。樂觀鎖適用於讀多寫少的場景,能夠提升程序吞吐量。sql
Mysql自帶的是沒有樂觀鎖的,可是能夠經過表上加個version字段來實現本身樂觀鎖。數據庫
假如要更新一個用戶的年齡,能夠這樣作:編程
id | Name | Age | Version |
---|---|---|---|
3 | 張三 | 26 | 1 |
更新張三的年齡爲27,注意where條件帶上版本號。update user set age = 27,version = 2 where id = 3 and version = 1;併發
若是更新的結果是1則表示更新成功了,若是是0則表示更新失敗須要從新嘗試。高併發
悲傷鎖就是在每次操做數據時,都悲觀地認爲會出現數據衝突,因此必須先獲取到數據的鎖再對其修改。傳統的關係型數據庫用的就是悲觀鎖,還有JDK中的synchronized關鍵字等。悲觀鎖主要分爲共享鎖和排他鎖。性能
共享鎖【shared locks】,又叫讀鎖,顧名思義,共享鎖就是多個事務對同一個數據能夠共享一把鎖,都能訪問到數據,可是隻能讀不能修改。rest
如何獲取共享鎖?
select * from user where id = 3 lock in share mode;
注意:在有事務獲取到了共享鎖以後,其餘事務是不能作insert/update/delete操做的,由於insert/update/delete語句會自動加上排他鎖。
排他鎖【exclusive locks】,又叫寫鎖,顧名思義,排他鎖就是不能與其餘鎖並存,若是一個事務獲取了一個數據行的排他鎖,其餘事務就不能再獲取該行的其餘鎖,包括共享鎖和排他鎖,可是獲取排他鎖的事務是能夠對數據進行讀取和修改。
如何獲取排他鎖?
在sql語句後加上for update便可。
select * from user where id = 3 for update
表鎖,顧名思義就是對整張表加鎖,是Mysql各存儲引擎中最大粒度的鎖定機制。
優勢:實現邏輯簡單,獲取鎖和釋放鎖的速度很快,因爲每次都是將整張表鎖定因此能夠很好的避免死鎖問題。
缺點:鎖定顆粒度大致使出現鎖定資源爭用的機率高,併發度低。
行鎖,顧名思義就是對錶中的某行數據加鎖,鎖定顆粒度最小。
優勢:發生鎖衝突的機率低,併發處理能力強。
缺點:因爲鎖定資源的顆粒度很小,因此每次獲取鎖和釋放鎖須要作的事情也更多,帶來的消耗天然也就更大了。此外,行級鎖定也最容易發生死鎖。
如何判斷使用的是行鎖仍是表鎖?
InnoDB的行鎖是針對索引加的鎖,不是針對記錄加的鎖,因此只有在經過索引條件檢索數據時纔會用行鎖,不然使用表鎖。而且該索引不能失效,不然都會從行鎖升級爲表鎖。因此在使用select for update時,where 子句必定要帶上索引,不然極容易形成性能問題。
行鎖又細分三種實現算法:
record lock:專門對索引項加鎖;
gap lock:間隙鎖,是對索引之間的間隙加鎖;
Next-key lock:是前面兩種的組合,對索引及其之間的間隙加鎖;
頁面鎖出現比較少,它的特色是開銷和加鎖時間界於表鎖和行鎖之間,會出現死鎖,鎖定粒度界於表鎖和行鎖之間,併發度通常。
死鎖(Deadlock) 所謂死鎖:是指兩個或兩個以上的進程在執行過程當中,因爭奪資源而形成的一種互相等待的現象,若無外力做用,它們都將沒法推動下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的進程稱爲死鎖進程。因爲資源佔用是互斥的,當某個進程提出申請資源後,使得有關進程在無外力協助下,永遠分配不到必需的資源而沒法繼續運行,這就產生了一種特殊現象死鎖。
死鎖的四個必要條件:
互斥條件:一個資源每次只能被一個進程使用。
佔有且等待:一個進程因請求資源而阻塞時,對已得到的資源保持不放。
不可強行佔有:進程已得到的資源,在末使用完以前,不能強行剝奪。
循環等待條件:若干進程之間造成一種頭尾相接的循環等待資源關係。
首先建立一張訂單記錄表,用於作訂單的冪等性校驗防止重複生成訂單。
CREATE TABLE `order_record` ( `id` int(11) NOT NULL AUTO_INCREMENT, `order_no` int(11) DEFAULT NULL, `status` int(4) DEFAULT NULL, `create_date` datetime(0) DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE, INDEX `idx_order_status`(`order_no`,`status`) USING BTREE ) ENGINE = InnoDB
事務A | 事務B |
---|---|
關閉自動提交事務,set autocommit = 0; | set autocommit = 0; |
select id from order_record where order_no = 4 for update;//檢查是否存在訂單號爲4的訂單 | |
select id from order_record where order_no = 5 for update;//檢查是否存在訂單號爲5的訂單 | |
//若是沒有則插入信息 insert into order_record(order_no,status,create_date) values(4,1,'2020-10-04 10:56:00'); 此時鎖等待中... |
|
//若是沒有則插入信息 insert into order_record(order_no,status,create_date) values(5,1,'2020-10-04 10:56:00'); |
|
返回結果代表發生死鎖,ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction | |
COMMIT;(未完成) | COMMIT;(未完成) |
分析:
因爲order_no列爲非惟一索引,並且此時是RR事務隔離級別,因此SELECT 的加鎖類型是gap lock,並且gap範圍是(4,+∞)。
當咱們執行插入 SQL 時,會在插入間隙上再次獲取插入意向鎖。插入意向鎖其實也是一種 gap 鎖,它與 gap lock 是衝突的,事務 A 和事務 B 都持有間隙 (4,+∞)的 gap 鎖,而接下來的插入操做爲了獲取到插入意向鎖,都在等待對方事務的 gap 鎖釋放,因而就形成了循環等待,致使死鎖。
InnoDB 存儲引擎的主鍵索引爲聚簇索引,其它索引爲輔助索引。若是兩個更新事務使用了不一樣的輔助索引,或者一個使用輔助索引,一個使用了聚簇索引,就都有可能致使鎖資源的循環等待,形成死鎖。
步驟:
首先,order_record表存在如下數據。
而後打開兩個窗口
事務A | 事務B |
---|---|
BEGIN; | BEGIN; |
update order_record set status = 1 where order_no = 4; | |
mysql> update order_record set status = 1 where id = 4; ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction//發生了死鎖 |
分析:
事務A | 事務B |
---|---|
首先獲取idx_order_status輔助索引 | |
獲取主鍵索引的行鎖 | |
根據輔助索引獲取主鍵索引,再獲取主鍵索引的行鎖 | |
更新status列時,須要idx_order_status輔助索引 |
因此再更新數據時,要儘可能根據主鍵來更新,能夠有效避免死鎖發生。
一般有如下手段能夠預防死鎖的發生:
若是真的發生了數據庫死鎖,也有如下方式處理:
查看當前的事務
SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX;
查看當前鎖定的事務
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;
查看當前等鎖的事務
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;
殺死進程 kill pid
並且MySQL默認開啓了死鎖檢測機制,當檢測到死鎖後會選擇一個最小(鎖定資源最少的)的事務進行回滾。
日常不多寫MySQL相關的文章,其實MySQL中的門道仍是挺多的,本文關於間隙鎖等概念講的比較簡單,推薦博客《mysql間隙鎖》。 之後可能會再寫一篇關於索引的,也有可能不會(主要是懶😄),若是本文哪裏有錯誤,請多指教。