一小時搞懂Mysql鎖機制

YLY969發表於2021-07-12

內容概述: 

我們知道,資料也是一種供許多使用者共享訪問的資源。如何保證資料併發訪問的一致性、有效性,是所有資料庫必須解決的一個問題,鎖的衝突也是影響資料庫併發訪問效能的一個重要因素。從這一角度來說,鎖對於資料庫而言就顯得尤為重要。接下來我們瞭解各種鎖的特徵。

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;
select * from t where id = 9 for update;

 

 

begin;
select * from t where id = 6 for update; 

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;

  

 

相關文章