概述:
Update和Insert是鎖表還是鎖行,會影響到程序中並發程序的設計。
總結:
(1)Update時,where中的過濾條件列,如果用索引,鎖行,無法用索引,鎖表。按照索引規則,如果能使用索引,鎖行,不能使用索引,鎖表。
(2)Insert時,可以並發執行,之間並不會相互影響。
一、Update操作
1. 實驗一
1)創建表和基礎數據,id是主鍵,如下圖:
2)在navicat中,新建一個查詢頁面,如下圖:
關閉自動提交,並更新第1條數據,執行上圖中的sql語句。
由於沒有使用commit;進行提交,所以id=1數據的age並沒有被更新為111。
3)在navicat中,再次新建一個查詢頁面,如下圖:
關閉自動提交,並修改id=1的數據的age值為1111,執行上圖中的sql語句,結果如下:
可以看到,無法對id=1的數據進行修改。但是,此時只知道id=1的數據無法修改,無法確定是鎖表還是鎖行。
4)在navicat中再次新建一個查詢頁面,如下圖:
關閉自動提交,並更新id=2的數據的age列為222,執行上圖中的sql語句,結果如下:
可以看到,id=2的數據的age被修改。也就是說,update時,where中使用id作為過濾條件時,只是鎖行,而不是鎖表。
2.實驗二
(1)將表中數據重置為初始值,如下圖:
(2)以上步驟,將過濾條件,由id改為name,例如:
由於沒有commit,所以,在執行之后,表中數據並沒有改變。
(3)接着,執行以下語句,如下圖:
(4)查看執行結果
執行失敗。如果只是鎖行,那么where name='陳二'的數據應該是可以修改的。所以這里是鎖表。
3.實驗三
(1)將表中數據重置為初始值,如下圖:
(2)給name列添加索引
ALTER TABLE `tb_user` ADD INDEX index_name ( `name` ) ;
(3)重新執行查詢4的語句,如下圖:
(4)接着,執行查詢5的語句,如下圖:
(5)查看結果
可以看到,where name='陳二'的數據被成功修改,意味着,當name有索引的時候,是鎖行。
4、結論
(1)實驗一和實驗二,實驗變量是過濾條件where中的列,實驗一是id列(主鍵,有索引),實驗二是name列(沒有索引)。結果是以id列為過濾條件,也就是使用有索引的列時,只是鎖行,而以name列為過濾條件,也就是沒有索引的列時,會鎖表。
(2)為驗證以上結果,實驗三給name列添加普通索引,實驗二和實驗三種,where的過濾條件都是name列,實驗變量是name是否有索引。結果是:有索引,鎖行;無索引,鎖表。同結論(1),驗證完成。
二、Insert操作
0.准備
注意,將索引刪除(並沒有影響,但是要減少變量),如下:
ALTER TABLE `tb_user` DROP INDEX index_name;
1. 實驗一
(1)關閉自動提交,insert一條新數據,如下圖:
查看tb_user表,發現並沒有插入。(原因是沒有執行commit。)
(2)再次insert一條新數據(這里並沒有關閉autocommit),如下圖:
結果如下:
可以看到,插入成功。也就是說id=5被鎖定。之后的insert語句並沒有受到影響。
2. 結論
insert的時候,可以並發執行,之間並不會相互影響。
三、深入探究:UPDATE能走索引還會鎖全表嗎
導讀
執行UPDATE時,WEHRE條件列雖已有索引,但還會鎖全表,腫么回事?
問題描述
葉師傅有次上課過程中執行UPDATE測試案例時,發現雖然WHERE條件列已有索引,有時候能利用二級索引進行更新(且只鎖定相應必要的幾行記錄),但有時候卻變成了根據主鍵進行更新,且會鎖全表。我們先來看看下面的例子。
測試表 t1
CREATE TABLE `t1` ( `c1` int(10) unsigned NOT NULL DEFAULT '0', `c2` int(10) unsigned NOT NULL DEFAULT '0', `c3` int(10) unsigned NOT NULL DEFAULT '0', `c4` int(10) unsigned NOT NULL DEFAULT '0', PRIMARY KEY (`c1`), KEY `c2` (`c2`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8
表中數據
+----+----+----+----+ | c1 | c2 | c3 | c4 | +----+----+----+----+ | 0 | 0 | 0 | 0 | | 1 | 1 | 1 | 0 | | 3 | 3 | 3 | 0 | | 4 | 2 | 2 | 0 | | 6 | 8 | 5 | 0 | | 7 | 6 | 6 | 10 | | 10 | 10 | 4 | 0 | +----+----+----+----+
case1:根據二級索引UPDATE,不鎖全表
先看執行計划
yejr@imysql.com [yejr]>desc update t1 set c4=123 where c2>=8\G *************************** 1. row *************************** id: 1 select_type: UPDATE table: t1 partitions: NULL type: range possible_keys: c2 key: c2 key_len: 4 ref: const rows: 2 filtered: 100.00 Extra: Using where
啟動兩個session執行UPDATE測試
session1 | session 2(后執行) |
mysql> begin; mysql> update t1 set c4=123 where c2>=8;
Query OK, 2 rows affected (0.00 sec) |
mysql> begin; mysql> select * from t1 where c2 = 7 for update;
… |
case2:根據PRIMARY KEY更新,鎖全表
yejr@imysql.com [yejr]>desc update t1 set c4=123 where c2>=6\G *************************** 1. row *************************** id: 1 select_type: UPDATE table: t1 partitions: NULL type: index possible_keys: c2 key: PRIMARY key_len: 4 ref: NULL rows: 7 filtered: 100.00 Extra: Using where
我們能看到本次執行計划是 根據主鍵索引進行更新,且會鎖全表。
同樣地,啟動兩個session執行UPDATE測試
session1 | session2(后執行) |
mysql> begin; mysql> update t1 set c4=123 where c2>=6;
Query OK, 3 rows affected (0.00 sec) |
mysql> begin; mysql> select * from t1 where c2 = 3 for update; #無法得到結果,被阻塞了 |
mysql> rollback;
#執行rollback,釋放鎖
=============================================
…
1 row in set (4.23 sec)
#session1釋放鎖后才能得到結果
查看行鎖等待情況
yejr@imysql.com [yejr]>select * from sys.innodb_lock_waits\G *************************** 1. row *************************** wait_started: 2017-08-15 15:20:20 wait_age: 00:00:17 wait_age_secs: 17 locked_table: `yejr`.`t1` locked_index: PRIMARY <--主鍵上加鎖 locked_type: RECORD waiting_trx_id: 268350 waiting_trx_started: 2017-08-15 15:20:20 waiting_trx_age: 00:00:17 waiting_trx_rows_locked: 2 waiting_trx_rows_modified: 0 waiting_pid: 13 waiting_query: select * from t1 where c2 = 3 for update waiting_lock_id: 268350:387:3:4 waiting_lock_mode: X blocking_trx_id: 268349 blocking_pid: 12 blocking_query: NULL blocking_lock_id: 268349:387:3:4 blocking_lock_mode: X blocking_trx_started: 2017-08-15 15:20:18 blocking_trx_age: 00:00:19 blocking_trx_rows_locked: 8 <-- 所有記錄都被加鎖了 blocking_trx_rows_modified: 3 <---持有鎖的事務更新了3行記錄 sql_kill_blocking_query: KILL QUERY 12 sql_kill_blocking_connection: KILL 12
問題分析
好了,案例說完了,也該說原因了。
腎好的同學可能記得我說過一個結論:當MySQL預估掃描行數超過全表總數約 20% ~ 30% 時,即便有二級索引,也會直接升級為全表掃描。
這個結論的原因並不難理解,二級索引的順序和主鍵順序一般來說是不一樣的,根據二級索引的順序回表讀數據時,實際上對於主鍵則很可能是隨機掃描,因此當需要隨機掃描的數量超過一定比例時(一般是20% ~ 30%),則優化器會決定直接改成全表掃描。
上述說法出處:WHERE Clause Optimization
Each table index is queried, and the best index is used unless the optimizer believes
that it is more efficient to use a table scan. At one time, a scan was used based on whether
the best index spanned more than 30% of the table, but a fixed percentage no longer determines
the choice between using an index or a scan. The optimizer now is more complex and bases its estimate
on additional factors such as table size, number of rows, and I/O block size.
不過,上面這個結論是針對讀數據的情況,UPDATE/DELETE修改數據時是否也這樣呢?
答案是肯定的,要不然上面的測試結果怎么解釋……
按照官方開發者的說法,當優化器評估根據二級索引更新行數超過約50%(從上面測試結果來看,其實20% ~ 30%就已經是這樣了,不過這個比例並不是固定值,會根據各種代價計算自動調整)就會改成走主鍵索引,並且鎖全表,這么做是既定的策略,原因和上面一樣,也是為了提高檢索效率。
總結
老調重彈,幾點建議:
- 不管檢索還是更新,都盡可能利用索引;
- 不要一次性檢索或更新大批量數據,建議分城多批次;
- 事務盡快提交,降低行鎖持有時間及其影響。
參考文章:
https://blog.csdn.net/wodeshouji6/article/details/104323875
https://imysql.com/2020/07/14/why-update-rows-using-index-but-cause-row-lock.shtml