內容概述:
我們知道,數據也是一種供許多用戶共享訪問的資源。如何保證數據並發訪問的一致性、有效性,是所有數據庫必須解決的一個問題,鎖的沖突也是影響數據庫並發訪問性能的一個重要因素。從這一角度來說,鎖對於數據庫而言就顯得尤為重要。接下來我們了解各種鎖的特征。
1.1 表鎖
表級鎖是mysql鎖中粒度最大的一種鎖,表示當前的操作對整張表加鎖,資源開銷比行鎖少,不會出現死鎖的情況,但是發生鎖沖突的概率很大。該鎖定機制最大的特點是實現邏輯非常簡單,帶來的系統負面影響最小。所以獲取鎖和釋放鎖的速度很快。由於表級鎖一次會將整個表鎖定,所以可以很好地避免困擾我們的死鎖問題。InnoDB支持表級鎖。但是默認的行鎖,而且只有在查詢或者其他SQL語句通過索引才會使用行鎖。
1.2 行鎖
行鎖的是mysql鎖中粒度最小的一種鎖,因為鎖的粒度很小,所以發生資源爭搶的概率也最小,並發性能最大,但是也會造成死鎖,每次加鎖和釋放鎖的開銷也會變大。目前主要是Innodb使用行鎖。根據使用方式也分為共享鎖(S鎖或者讀鎖)和排它鎖(X鎖或者寫鎖)。
1.3共享鎖(讀鎖,S鎖)
共享鎖的具體邏輯如下案例:
若事務A對數據對象o加上S鎖,則事務A可以讀數據對象o但不能修改,其他事務只能再對數據對象o加S鎖,而不能加X鎖,直到事務A釋放數據對象o上的S鎖。這保證了其他事務可以讀數據對象o,但在事務A釋放數據對象o上的S鎖之前不能對數據對象o做任何修改。
語法和案例如下:
# 加讀鎖 Lock table tablename read; #釋放鎖 unlock table; 案例如下 : #session1 給表創建讀鎖(創建一個連接就是一個回話session) mysql> lock table userinfo read; Query OK, 0 rows affected (0.01 sec) #可以讀取當前表的數據 mysql> select * from userinfo ; +----+----------+------+ | id | name | age | +----+----------+------+ | 1 | zhangsan | 27 | | 2 | lisi | 27 | | 3 | dd | NULL | | 4 | dd | 2 | +----+----------+------+ 4 rows in set (0.00 sec) #session1 因為給userinfo表加了讀鎖,所以不能讀取其他表數據 mysql> select * from student_score; ERROR 1100 (HY000): Table 'student_score' was not locked with LOCK TABLES #session1 因為給表加的讀鎖,所以當前回話不能對表進行其他操作, mysql> update userinfo set age=age+1; ERROR 1099 (HY000): Table 'userinfo' was locked with a READ lock and can't be updated #session2,在session1 沒有釋放鎖之前,session2可以進行讀取userinfo mysql> select * from userinfo; +----+----------+------+ | id | name | age | +----+----------+------+ | 1 | zhangsan | 27 | | 2 | lisi | 27 | | 3 | dd | NULL | | 4 | dd | 2 | +----+----------+------+ 4 rows in set (0.00 sec) #session2,在session1 沒有釋放鎖之前,session2修改或刪除表userinfo的任何數據都被阻塞 mysql> update userinfo set age=1 where name='dd'; 。。。。一直等待,直到seesion1 釋放鎖 #session1 釋放鎖,session1在執行下面釋放鎖語句后,session2才會提交上面修改語句。 mysql> unlock tables;
1.4 排它鎖(寫鎖,X鎖)
共享鎖的具體邏輯如下案例:
若事務A對數據對象o加上X鎖,事務A可以讀數據對象o也可以修改數據對象o,其他事務不能再對數據對象o加任何鎖,直到事務A釋放數據對象o上的鎖。這保證了其他事務在事務A釋放數據對象o上的鎖之前不能再讀取和修改數據對象o。
# 加寫鎖給表 Lock table tablename write; # 加寫鎖給行 select ... for update; #釋放表鎖 unlock table; 案例如下 : # session1 當前會話session1給表加寫鎖。 mysql> lock table userinfo write; Query OK, 0 rows affected (0.00 sec) #session1 當前會話可以修改此表數據 mysql> update userinfo set age=9; Query OK, 4 rows affected (0.01 sec) Rows matched: 4 Changed: 4 Warnings: 0 #session1 當前會話可以查詢此表 mysql> select * from userinfo; +----+----------+------+ | id | name | age | +----+----------+------+ | 1 | zhangsan | 9 | | 2 | lisi | 9 | | 3 | dd | 9 | | 4 | dd | 9 | +----+----------+------+ 4 rows in set (0.00 sec) #session2,在session1 沒有釋放鎖之前,session2對表的任何數據任何操作(增刪改查)userinfo的任何數據都被阻塞 mysql> update userinfo set age=1 where name='dd'; 。。。。一直等待,直到seesion1 釋放鎖 如上是鎖表,接下來我們看看給行加鎖的效果: #修改提交模式,改為手動提交 mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) #session1 當前會話給id=1的行加寫鎖。 mysql> select * from userinfo where id=1 for update; +----+----------+------+ | id | name | age | +----+----------+------+ | 1 | zhangsan | 10 | +----+----------+------+ 1 row in set (0.00 sec) #session2 當前會話2 可以查詢當前表數據。 mysql> select * from userinfo; +----+----------+------+ | id | name | age | +----+----------+------+ | 1 | zhangsan | 11 | | 2 | lisi | 12 | | 3 | dd | 12 | | 4 | dd | 11 | +----+----------+------+ 4 rows in set (0.00 sec) #session2 當前會話2 可以操作其他行數據。 mysql> update userinfo set age=12 where id=2; Query OK, 0 rows affected (0.01 sec) Rows matched: 1 Changed: 0 Warnings: 0 #session2 當前會話2修改數據涉及到id=1的行,所以被阻塞知道行鎖被釋放 mysql> update userinfo set age=12 ; 。。。。一直被阻塞,直到sesssion1 執行commit語句。 #session1 回話1 提交數據,也就是釋放鎖 mysql> commit; Query OK, 0 rows affected (0.00 sec)
1.5意向鎖
意向共享鎖和意向排它鎖總稱為意向鎖。意向鎖的出現是為了支持Innodb支持多粒度鎖。它是表級別的鎖。兩種鎖含義如下:
1.意向共享鎖:事務想要在獲得表中某些記錄的共享鎖(讀鎖),需要在表上先加意向共享鎖。
2.意向互斥鎖:事務想要在獲得表中某些記錄的互斥鎖(寫鎖),需要在表上先加意向互斥鎖。
當我們需要給一個加表鎖的時候,我們需要根據意向鎖去判斷表中有沒有數據行被鎖定,以確定是否能加成功。如果意向鎖是行鎖,那么我們就得遍歷表中所有數據行來判斷。如果意向鎖是表鎖,則我們直接判斷一次就知道表中是否有數據行被鎖定了。所以說將意向鎖設置成表級別的鎖的性能比行鎖高的多。
所以一項所的作用就是當一個事務在需要獲取資源的鎖定時,如果該資源已經被排他鎖占用,則數據庫會自動給該事務申請一個該表的意向鎖。如果自己需要一個共享鎖定,就申請一個意向共享鎖。如果需要的是某行(或者某些行)的排他鎖定,則申請一個意向排他鎖。
業務執行流程如下:
有了意向鎖之后,事務A在申請行鎖(寫鎖)之前,數據庫會自動先給事務A申請表的意向排他鎖。當事務B去申請表的寫鎖時就會失敗,因為表上有意向排他鎖之后事務B申請表的寫鎖時會被阻塞。明顯提高性能。快速判斷好此表有被鎖的數據,所以,不能獲取到此表的表級別的鎖。
1.6 樂觀鎖
樂觀鎖不是數據庫自帶的,是需要我們自己寫業務去實現。樂觀鎖是指操作數據庫時(更新操作),想法很樂觀,認為這次的操作不會導致沖突,在操作數據時,並不進行任何其他的特殊處理(也就是不加鎖),而在進行更新后,再去判斷是否有沖突了。
操作流程如下:
在表中的數據進行更新操作時,先給數據表加一個版本(version)字段,每操作一次,將那條記錄的版本號加1。也就是先查詢出那條記錄,獲取出version字段,如果要對那條記錄進行操作(更新),則先判斷此刻version的值是否與剛剛查詢出來時的version的值相等,如果相等,則說明這段期間,沒有其他程序對其進行操作,則可以執行更新,將version字段的值加1;如果更新時發現此刻的version值與剛剛獲取出來的version的值不相等,則說明這段期間已經有其他程序對其進行操作了,則不進行更新操作。這樣做的好處是避免了長事務中的數據庫加鎖開銷,大大提升了大並發量下的系統整體性能表現。
偽業務代碼如下: #首先獲取修改之前的數據和版本號 SELECT data AS old_data, version AS old_version FROM …; #進行修改,條件是當前數據的版本號和之前的老版本號一致,表示沒有人進行過修改 Int updaterow= UPDATE SET data = new_data, version = new_version WHERE version = old_version // 表示修改成功,拿到了樂觀鎖 if (updated row > 0) { // 樂觀鎖獲取成功,操作完成 } else { // 樂觀鎖獲取失敗,回滾並重試 }
1.7 悲觀鎖
悲觀鎖指的是對數據被外界(包括本系統當前的其他事務,以及來自外部系統的事務處理)修改持保守態度,因此,在整個數據處理過程中,將數據處於鎖定狀態。悲觀鎖的實現,往往依靠數據庫提供的鎖機制(也只有數據庫層提供的鎖機制才能真正保證數據訪問的排他性,否則,即使在本系統中實現了加鎖機制,也無法保證外部系統不會修改數據)
案例如下:
#關閉自動提交后 set autocommit=0; #開始事務 start transaction; #查詢當前商品信息,然后通過for update鎖定數據防止其他事務修改 select status from goods where goodsid=1 for update; #根據商品信息生成訂單 insert into orders (id,goodsid) values (null,1); #修改商品status為2 update t_goods set status=2; #執行完畢,提交事務 commit;
上述案例就實現了悲觀鎖,悲觀鎖就是悲觀主義者,它會認為我們在事務A中操作數據1的時候,一定會有事務B來修改數據1,所以,在第2步我們將數據查詢出來后直接加上排它鎖(X)鎖,防止別的事務來修改事務1,直到我們commit后,才釋放了排它鎖。
需要注意的是,樂觀鎖保證了數據處理時的安全性但是更新有可能會失敗,甚至是更新幾次都失敗,這是有風險的。所以如果寫入居多,對吞吐要求不高,可使用悲觀鎖。而且要注意的是悲觀鎖加鎖造成了開銷增加,降低了性能,而且還會出現死鎖。
1.8 間歇鎖
間隙鎖(Gap Lock)是Innodb在提交下為了解決幻讀問題時引入的鎖機制,(下面的所有案例沒有特意強調都使用可重復讀隔離級別)幻讀的問題存在是因為新增或者更新操作,這時如果進行范圍查詢的時候(加鎖查詢),會出現不一致的問題,這時使用不同的行鎖已經沒有辦法滿足要求,需要對一定范圍內的數據進行加鎖,間隙鎖就是解決這類問題的。在可重復讀隔離級別下,數據庫是通過行鎖和間隙鎖共同組成的(next-key lock),來實現的。加鎖規則有以下特性:
1.加鎖的基本單位是(next-key lock),他是前開后閉原則
2.插敘過程中訪問的對象會增加鎖
3.索引上的等值查詢--給唯一索引加鎖的時候,next-key lock升級為行鎖
4.索引上的等值查詢--向右遍歷時最后一個值不滿足查詢需求時,next-key lock 退化為間隙鎖
5.唯一索引上的范圍查詢會訪問到不滿足條件的第一個值為止。
id(主鍵) |
name |
age |
5 |
name1 |
18 |
10 |
name2 |
19 |
15 |
name3 |
20 |
20 |
name4 |
21 |
25 |
name5 |
22 |
以上數據為了解決幻讀問題,更新的時候不只是對上述的五條數據增加行鎖,還對於中間的取值范圍增加了6間隙鎖,(-∞,5](5,10](10,15](15,20](20,25](25,+∞]
如下案例:
#session1 操作會話session1 開啟事務 mysql> begin; Query OK, 0 rows affected (0.00 sec) #session1 更新id=11的數據,獲取行鎖。 mysql> select * from userinfo where id=11 for update; Empty set (0.00 sec) #session2 操作會話session2 開啟事務 mysql> begin; Query OK, 0 rows affected (0.00 sec) #session2 在會話session2 新增數據主鍵為12 mysql> insert into userinfo values(12,'hh',18); .... 一直處於阻塞狀態 #如果等待時間過長,session1沒有釋放鎖,會拋出如下異常。 ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
如上事務session1和事務session2時,事務session1會對數據庫表增加(10,15]這個區間鎖,這時insert id = 12 的數據的時候就會因為區間鎖(10,15]而被鎖住無法執行。
特別注意,間歇鎖,如果操作不當會出現死鎖。如下:
事務A |
事務B |
begin; |
|
|
begin; insert into user value(7,7,7) |
insert into user value(7,7,7) |
|
如上,事務A獲取到(5,10]之間的間隙鎖不允許其他的DDL操作,在事務提交,間隙鎖釋放之前,事務B也獲取到了間隙鎖(5,10],這時兩個事務就處於死鎖狀態
1.9記錄鎖
記錄鎖,它封鎖索引記錄,作用於唯一索引上。如果執行這條語句(select * from userinfo where id=4 for update;)它會在id=4的索引記錄上加鎖,以阻止其他事務插入,更新,刪除id=4的這一行。就是說,當一個會話執行這條語句之后,其它會話,執行下面這幾種語句,都會處於阻塞狀態。
select * from userinfo where id=1 for update; delete from userinfo where id=4; insert into userinfo values(4,'hh',18); 阻塞時間過長可能出拋出如下錯誤: ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
需要注意的是,如果把事務的隔離級別降級為讀提交(Read Committed, RC),間隙鎖則會自動失效。
1.10臨鍵鎖
臨鍵鎖,作用於非唯一索引上,是記錄鎖與間隙鎖的組合,如下圖表格。
id(主鍵) |
name |
age |
5 |
name1 |
18 |
10 |
name2 |
19 |
15 |
name3 |
20 |
20 |
name4 |
21 |
25 |
name5 |
22 |
它的封鎖范圍,既包含索引記錄,又包含索引之前的區間,即(-∞,5](5,10](10,15](15,20](20,25](25,+∞]。
如下在事務中執行。 Update userinfo SET age=19 where id= 10; Select * from userinfo where id=10 FOR UPDATE;
這兩個語句都會鎖定(5,10],(10,15)這兩個區間。即, InnoDB 會獲取該記錄行的 臨鍵鎖 ,並同時獲取該記錄行下一個區間的間隙鎖。
1.11 死鎖
死鎖是指兩個或兩個以上事務在執行過程中因爭搶鎖資源而造成的互相等待的現象。如下圖7-3所示的操作流程。
上圖所示,這兩種情況即為死鎖產生的常規情景。事務A等着事務B釋放鎖,事務B等着事務A釋放鎖。就會出現相互等待,一直等待下去。避免這種問題的辦法有兩種,一種是,等待事務超時,主動回滾。第二種是進行死鎖檢查,主動回滾某條事務,讓別的事務能繼續走下去。可以使用相關指令查詢當前查看正在被鎖的事務。
#查看正在被鎖的事務 SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX; #可以通過如下指令,殺死當前事務進程,釋放鎖。 trx_id字段對應上面語句結果對應的第一個字段值 kill trx_id;