為什麽開發人員必須要了解數據庫鎖?
阿新 • • 發佈:2018-08-17
資料 不同的 his 定量 串行 自己 start 文件 並發訪問
原創: 咖啡拿鐵
1.鎖?
1.1何為鎖
鎖在現實中的意義為:封閉的器物,以鑰匙或暗碼開啟。在計算機中的鎖一般用來管理對共享資源的並發訪問,比如我們java同學熟悉的Lock,synchronized等都是我們常見的鎖。當然在我們的數據庫中也有鎖用來控制資源的並發訪問,這也是數據庫和文件系統的區別之一。
1.2為什麽要懂數據庫鎖?
通常來說對於一般的開發人員,在使用數據庫的時候一般懂點DQL(select),DML(insert,update,delete)就夠了。
小明是一個剛剛畢業在互聯網公司工作的Java開發工程師,平常的工作就是完成PM的需求,當然在完成需求的同時肯定逃脫不了spring,springmvc,mybatis的那一套框架,所以一般來說sql還是自己手寫,遇到比較復雜的sql會從網上去百度一下。對於一些比較重要操作,比如交易啊這些,小明會用spring的事務來對數據庫的事務進行管理,由於數據量比較小目前還涉及不了分布式事務。
前幾個月小明過得都還風調雨順,知道有一天,小明接了一個需求,商家有個配置項,叫優惠配置項,可以配置買一送一,買一送二等等規則,當然這些配置是批量傳輸給後端的,這樣就有個問題每個規則都得去匹配他到底是刪除還是添加還是修改,這樣後端邏輯就比較麻煩,聰明的小明想到了一個辦法,直接刪除這個商家的配置,然後全部添加進去。小明馬上開發完畢,成功上線。
開始上線沒什麽毛病,但是日誌經常會出現一些mysql-insert-deadlock異常。由於小明經驗比較淺,對於這類型的問題第一次遇見,於是去問了他們組的老司機-大紅,大紅一看見這個問題,然後看了他的代碼之後,輸出了幾個命令看了幾個日誌,馬上定位了問題,告訴了小明:這是因為delete的時候會加間隙鎖,但是間隙鎖之間卻可以兼容,但是插入新的數據的時候就會因為插入意向鎖會被間隙鎖阻塞,導致雙方被資源被互占,導致死鎖。小明聽了之後似懂非懂,由於大紅的事情比較多,不方便一直麻煩大紅,所以決定自己下來自己想。下班過後,小明回想大紅說的話,什麽是間隙鎖,什麽是插入意向鎖,看來作為開發者對數據庫不應該只會寫SQL啊,不然遇到一些疑難雜癥完全沒法解決啊。想完,於是小明就踏上了學習Mysql鎖這條不歸之路。
2.InnoDB
2.1mysql體系架構
小明沒有著急去了解鎖這方面的知識,他首先先了解了下Mysql體系架構:
可以發現Mysql由連接池組件、管理服務和工具組件、sql接口組件、查詢分析器組件、優化器組件、 緩沖組件、插件式存儲引擎、物理文件組成。
小明發現在mysql中存儲引擎是以插件的方式提供的,在Mysql中有多種存儲引擎,每個存儲引擎都有自己的特點。隨後小明在命令行中打出了:
小明大概了解了一下InnoDB和MyIsAM的區別,由於使用的是InnoDB,小明就沒有過多的糾結這一塊。
2.2事務的隔離性
小明在研究鎖之前,又回想到之前上學的時候教過的數據庫事務隔離性,其實鎖在數據庫中其功能之一也是用來實現事務隔離性。而事務的隔離性其實是用來解決,臟讀,不可重復讀,幻讀幾類問題。
2.2.1 臟讀
一個事務讀取到另一個事務未提交的更新數據。 什麽意思呢?
在事務A,B中,事務A在時間點2,4分別對user表中id=1的數據進行了查詢了,但是事務B在時間點3進行了修改,導致了事務A在4中的查詢出的結果其實是事務B修改後的。破壞了數據庫中的隔離性。
2.2.2 不可重復讀
在同一個事務中,多次讀取同一數據返回的結果不同,和臟讀不同的是這裏讀取的是已經提交過後的。
2.2.3 幻讀
一個事務讀到另一個事務已提交的insert數據。
在事務A中查詢了兩次id大於1的,在第一次id大於1查詢結果中沒有數據,但是由於事務B插入了一條Id=2的數據,導致事務A第二次查詢時能查到事務B中插入的數據。
事務中的隔離性:
小明註意到在收集資料的過程中,有資料寫到InnoDB和其他數據庫有點不同,InnoDB的可重復讀其實就能解決幻讀了,小明心想:這InnoDB還挺牛逼的,我得好好看看到底是怎麽個原理。
2.3 InnoDB鎖類型
小明首先了解一下Mysql中常見的鎖類型有哪些:
2.3.1 S or X
在InnoDb中實現了兩個標準的行級鎖,可以簡單的看為兩個讀寫鎖:
2.3.2 意向鎖
意向鎖在InnoDB中是表級鎖,和他的名字一樣他是用來表達一個事務想要獲取什麽。意向鎖分為:
2.3.3 自增長鎖
自增長鎖是一種特殊的表鎖機制,提升並發插入性能。對於這個鎖有幾個特點:
小明開啟了兩個事務並輸入了上面的語句,發現事務B居然出現了超時,小明看了一下自己明明是對name = 555這一行進行的加鎖,為什麽我想插入name=556給我阻塞了。於是小明打開命令行輸入:
居然發現事務B並沒有發生阻塞,哎這個是咋回事呢,小明有點疑惑,按照實驗1的套路應該會被阻塞啊,因為25-30之間會有間隙鎖。於是小明又祭出了命令行,發現只加了X記錄鎖。原來是因為唯一索引會降級記錄鎖,這麽做的理由是:非唯一索引加next-key鎖由於不能確定明確的行數有可能其他事務在你查詢的過程中,再次添加這個索引的數據,導致隔離性遭到破壞,也就是幻讀。唯一索引由於明確了唯一的數據行,所以不需要添加間隙鎖解決幻讀。
3.3 實驗3
上面測試了主鍵索引,非唯一索引,這裏還有個字段是沒有索引,如果對其加鎖會出現什麽呢?
的確,如果用沒有索引的數據,其會對所有聚簇索引上都加上next-key鎖。
所以大家平常開發的時候如果對查詢條件沒有索引的,一定進行一致性讀,也就是加鎖讀,會導致全表加上索引,會導致其他事務全部阻塞,數據庫基本會處於不可用狀態。
4.回到事故
4.1 死鎖
小明做完實驗之後總算是了解清楚了加鎖的一些基本套路,但是之前線上出現的死鎖又是什麽東西呢?
死鎖:是指兩個或兩個以上的事務在執行過程中,因爭奪資源而造成的一種互相等待的現象。說明有等待才會有死鎖,解決死鎖可以通過去掉等待,比如回滾事務。
解決死鎖的兩個辦法:
可以看見事務A出現被回滾了,而事務B成功執行。 具體每個時間點發生了什麽呢?
時間點2:事務A刪除name = ‘777‘的數據,需要對777這個索引加上next-Key鎖,但是其不存在,所以只對555-999之間加間隙鎖,同理事務B也對555-999之間加間隙鎖。間隙鎖之間是兼容的。
時間點3:事務A,執行Insert操作,首先插入意向鎖,但是555-999之間有間隙鎖,由於插入意向鎖和間隙鎖沖突,事務A阻塞,等待事務B釋放間隙鎖。事務B同理,等待事務A釋放間隙鎖。於是出現了A->B,B->A回路等待。
時間點4:事務管理器選擇回滾事務A,事務B插入操作執行成功。
4.3 修復BUG
這個問題總算是被小明找到了,就是因為間隙鎖,現在需要解決這個問題,這個問題的原因是出現了間隙鎖,那就來去掉他吧:
- show engines \G;
- show variables like ‘%storage_engine%‘;
對比項 | InnoDB | MyIsAM |
事務 | 支持 | 不支持 |
鎖 | 支持MVCC行鎖 | 表鎖 |
外鍵 | 支持 | 不支持 |
存儲空間 | 存儲空間由於需要高速緩存,較大 | 可壓縮 |
適用場景 | 有一定量的update和Insert | 大量的select |
時間點 | 事務A | 事務B |
1 | begin; | |
2 | select * from user where id = 1; | begin; |
3 | update user set namm = ‘test‘ where id = 1; | |
4 | select * from user where id = 1; | |
5 | commit; | commit; |
時間點 | 事務A | 事務B |
1 | begin; | |
2 | select * from user where id = 1; | begin; |
3 | update user set namm = ‘test‘ where id = 1; | |
4 | commit; | |
5 | select * from user where id = 1; | |
6 | commit; | |
在事務B中提交的操作在事務A第二次查詢之前,但是依然讀到了事務B的更新結果,也破壞了事務的隔離性。 |
時間點 | 事務A | 事務B |
1 | begin; | |
2 | select * from user where id > 1; | begin; |
3 | insert user select 2; | |
4 | commit; | |
5 | select * from user where id > 1; | |
6 | commit; |
隔離級別 | 臟讀 | 不可重復讀 | 幻讀 |
未提交讀(RUC) | NO | NO | NO |
已提交讀(RC) | YES | NO | NO |
可重復讀(RR) | YES | YES | NO |
可串行化 | YES | YES | YES |
- S-共享鎖:又叫讀鎖,其他事務可以繼續加共享鎖,但是不能繼續加排他鎖。
- X-排他鎖: 又叫寫鎖,一旦加了寫鎖之後,其他事務就不能加鎖了。
. | X | S |
X | 沖突 | 沖突 |
S | 沖突 | 兼容 |
- 意向共享鎖:表達一個事務想要獲取一張表中某幾行的共享鎖。
- 意向排他鎖:表達一個事務想要獲取一張表中某幾行的排他鎖。
. | IX | IS | X | S |
IX | 兼容 | 兼容 | 沖突 | 沖突 |
IS | 兼容 | 兼容 | 沖突 | 兼容 |
X | 沖突 | 沖突 | 沖突 | 沖突 |
S | 沖突 | 兼容 | 沖突 | 兼容 |
- 在sql執行完就釋放鎖,並不是事務執行完。
- 對於Insert...select大數據量插入會影響插入性能,因為會阻塞另外一個事務執行。
- 自增算法可以配置。
- mysql> show variables like ‘innodb_autoinc_lock_mode‘;
- +--------------------------+-------+
- | Variable_name | Value |
- +--------------------------+-------+
- | innodb_autoinc_lock_mode | 2 |
- +--------------------------+-------+
- 1 row in set (0.01 sec)
- 傳統模式:也就是我們最上面的使用表鎖。
- 連續模式:對於插入的時候可以確定行數的使用互斥量,對於不能確定行數的使用表鎖的模式。
- 交錯模式:所有的都使用互斥量,為什麽叫交錯模式呢,有可能在批量插入時自增值不是連續的,當然一般來說如果不看重自增值連續一般選擇這個模式,性能是最好的。
- 如果鎖的是非主鍵索引,會在自己的索引上面加鎖之後然後再去主鍵上面加鎖鎖住.
- 如果沒有表上沒有索引(包括沒有主鍵),則會使用隱藏的主鍵索引進行加鎖。
- 如果要鎖的列沒有索引,則會進行全表記錄加鎖。
- 快照讀:讀取的歷史數據,簡單的select語句,不加鎖,MVCC實現可重復讀,使用的是MVCC機制讀取undo中的已經提交的數據。所以它的讀取是非阻塞的。
- 當前讀:需要加鎖的語句,update,insert,delete,select...for update等等都是當前讀。
- CREATE TABLE `user` (
- `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
- `name` varchar(11) CHARACTER SET utf8mb4 DEFAULT NULL,
- `comment` varchar(11) CHARACTER SET utf8 DEFAULT NULL,
- PRIMARY KEY (`id`),
- KEY `index_name` (`name`)
- ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
- insert user select 20,333,333;
- insert user select 25,555,555;
- insert user select 20,999,999;
時間點 | 事務A | 事務B |
1 | begin; | |
2 | select * from user where name = ‘555‘ for update; | begin; |
3 | insert user select 31,‘556‘,‘556‘; | |
4 | ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction |
- select * from information_schema.INNODB_LOCKS
時間點 | 事務A | 事務B |
1 | begin; | |
2 | select * from user where id = 25 for update; | begin; |
3 | insert user select 26,‘666‘,‘666‘; | |
4 | Query OK, 1 row affected (0.00 sec) | |
Records: 1 Duplicates: 0 Warnings: 0 |
時間點 | 事務A | 事務B |
1 | begin; | |
2 | select * from user where comment = ‘555‘ for update; | begin; |
3 | insert user select 26,‘666‘,‘666‘; | |
4 | ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction | |
5 | insert user select 31,‘3131‘,‘3131‘; | |
6 | ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction | |
7 | insert user select 10,‘100‘,‘100‘; | |
8 | ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction | |
小明一看哎喲我去,這個咋回事呢,咋不管是用實驗1非間隙鎖範圍的數據,還是用間隙鎖裏面的數據都不行,難道是加了表鎖嗎? |
- 等待超時:當某一個事務等待超時之後回滾該事務,另外一個事務就可以執行了,但是這樣做效率較低,會出現等待時間,還有個問題是如果這個事務所占的權重較大,已經更新了很多數據了,但是被回滾了,就會導致資源浪費。
- 等待圖(wait-for-graph): 等待圖用來描述事務之間的等待關系,當這個圖如果出現回路如下:
時間點 | 事務A | 事務B |
1 | begin; | begin; |
2 | delete from user where name = ‘777‘; | delete from user where name = ‘666‘; |
3 | insert user select 27,‘777‘,‘777‘; | insert user select 26,‘666‘,‘666‘; |
4 | ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction | Query OK, 1 row affected (14.32 sec) Records: 1 Duplicates: 0 Warnings: 0 |
- 方案一:隔離級別降級為RC,在RC級別下不會加入間隙鎖,所以就不會出現毛病了,但是在RC級別下會出現幻讀,可提交讀都破壞隔離性的毛病,所以這個方案不行。
- 方案二:隔離級別升級為可序列化,小明經過測試後發現不會出現這個問題,但是在可序列化級別下,性能會較低,會出現較多的鎖等待,同樣的也不考慮。
- 方案三:修改代碼邏輯,不要直接刪,改成每個數據由業務邏輯去判斷哪些是更新,哪些是刪除,那些是添加,這個工作量稍大,小明寫這個直接刪除的邏輯就是為了不做這些復雜的事的,所以這個方案先不考慮。
為什麽開發人員必須要了解數據庫鎖?