紙上得來終覺淺,絕知此事要躬行。 InnoDB行鎖 開銷大,加鎖慢;會出現死鎖;鎖定粒度最小,發生鎖衝突的概率最低,併發度也最高。 InnoDB 實現了以下兩種類型的行鎖。 共用鎖(S):又稱為讀鎖,簡稱S鎖,共用鎖就是多個事務對於同一數據可以共用一把鎖,都能訪問到數據,但是只能讀不能修改。 排他鎖 ...
紙上得來終覺淺,絕知此事要躬行。
InnoDB行鎖
開銷大,加鎖慢;會出現死鎖;鎖定粒度最小,發生鎖衝突的概率最低,併發度也最高。
InnoDB 實現了以下兩種類型的行鎖。
- 共用鎖(S):又稱為讀鎖,簡稱S鎖,共用鎖就是多個事務對於同一數據可以共用一把鎖,都能訪問到數據,但是只能讀不能修改。
- 排他鎖(X):又稱為寫鎖,簡稱X鎖,排他鎖就是不能與其他鎖並存,如一個事務獲取了一個數據行的排他鎖,其他事務就不能再獲取該行的其他鎖(包括共用鎖和排他鎖),但是獲取排他鎖的事務是可以對數據就行讀取和修改。
可以通過以下語句顯示給記錄集加共用鎖或排他鎖 。
共用鎖(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE
排他鎖(X) :SELECT * FROM table_name WHERE ... FOR UPDATE
對於UPDATE、DELETE和INSERT語句,InnoDB會自動給涉及數據集加排他鎖(X);
對於普通SELECT語句,InnoDB不會加任何鎖;
行鎖案例
在測試前仍然準備相關測試表和數據,註意表的存儲引擎為InnoDB:
create table test_innodb_lock(
id int(11),
name varchar(16),
sex varchar(1)
)engine = innodb default charset=utf8;
insert into test_innodb_lock values(11,'100','1');
insert into test_innodb_lock values(3,'3','1');
insert into test_innodb_lock values(4,'400','0');
insert into test_innodb_lock values(5,'500','1');
insert into test_innodb_lock values(6,'600','0');
insert into test_innodb_lock values(7,'700','0');
insert into test_innodb_lock values(8,'800','1');
insert into test_innodb_lock values(9,'900','1');
insert into test_innodb_lock values(1,'200','0');
create index idx_test_innodb_lock_id on test_innodb_lock(id);
create index idx_test_innodb_lock_name on test_innodb_lock(name);
創建完成,我們看一下表結構和表數據,方便後面操作查看:
mysql> desc test_innodb_lock;
+-------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| id | int(11) | YES | MUL | NULL | |
| name | varchar(16) | YES | MUL | NULL | |
| sex | varchar(1) | YES | | NULL | |
+-------+-------------+------+-----+---------+-------+
3 rows in set (0.00 sec)
mysql> select * from test_innodb_lock;
+------+------+------+
| id | name | sex |
+------+------+------+
| 11 | 100 | 1 |
| 3 | 3 | 1 |
| 4 | 400 | 0 |
| 5 | 500 | 1 |
| 6 | 600 | 0 |
| 7 | 700 | 0 |
| 8 | 800 | 1 |
| 9 | 900 | 1 |
| 1 | 200 | 0 |
+------+------+------+
9 rows in set (0.00 sec)
行鎖基本演示
Session-1 | Session-2 |
---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
以上, 操作的都是同一行的數據,接下來,演示不同行的數據 : | |
![]() |
![]() |
無索引行鎖升級為表鎖
如果不通過索引條件檢索數據,那麼InnoDB將對錶中的所有記錄加鎖,實際效果跟表鎖一樣。
查看當前表的索引 :
show index from test_innodb_lock ;
接下來我們看一個例子,當索引失效的時候,行鎖升級為表鎖的過程:
Session-1 | Session-2 |
---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
以上兩個客戶端對一行數據進行修改時,由於執行更新時,name
欄位本來為varchar
類型, 我們是作為int
類型使用,存在類型轉換,索引失效,最終行鎖變為表鎖,導致Session-2更新數據處於等待狀態。
間隙鎖的危害
當我們用範圍條件,而不是使用相等條件檢索數據,並請求共用或排他鎖時,InnoDB會給符合條件的已有數據進行加鎖; 對於鍵值在條件範圍內但並不存在的記錄,叫做 "間隙(GAP)" , InnoDB也會對這個 "間隙" 加鎖,這種鎖機制就是所謂的間隙鎖(Next-Key鎖) 。
下麵是一個間隙鎖的案例:
Session-1 | Session-2 |
---|---|
![]() |
![]() |
![]() |
|
![]() |
|
![]() |
|
![]() |
|
提交事務 |
由於Session-1的插入操作是一個範圍,我們表中存在id
不連續的情況,導致在更新時不僅會加排它鎖,還會在當前訪問增加間隙鎖,導致Session-2在插入元素時處於等待狀態。
InnoDB 行鎖爭用情況
查看行鎖競爭情況:
show status like 'innodb_row_lock%';
- Innodb_row_lock_current_waits: 當前正在等待鎖定的數量
- Innodb_row_lock_time: 從系統啟動到現在鎖定總時間長度
- Innodb_row_lock_time_avg:每次等待所花平均時長
- Innodb_row_lock_time_max:從系統啟動到現在等待最長的一次所花的時間
- Innodb_row_lock_waits: 系統啟動後到現在總共等待的次數
總結
InnoDB存儲引擎由於實現了行級鎖定,雖然在鎖定機制的實現方面帶來了性能損耗可能比表鎖會更高一些,但是在整體併發處理能力較高。但是,InnoDB的行級鎖也是一把雙刃劍,當我們使用不當的時候,可能會讓InnoDB的整體性能下降。
優化建議:
- 儘可能通過索引來完成數據操作,避免無索引行鎖升級為表鎖。
- 合理設計索引,儘量縮小鎖的範圍
- 儘可能減少索引條件,及索引範圍,避免間隙鎖
- 儘量控制事務大小,減少鎖定資源量和時間長度
- 盡可使用低級別事務隔離(但是需要業務層面滿足需求)