1. 程式人生 > >MySQL 資料庫下DELETE、UPDATE 子查詢的鎖機制解析與優化

MySQL 資料庫下DELETE、UPDATE 子查詢的鎖機制解析與優化



在日常的工作之中,資料庫開發與維護人員避免不了與 in/exists、not in/not exists 子查詢打交道,接觸過的人可能知道 in/exists、not in/not exists 相關子查詢會使 SELECT 查詢變慢,沒有 join 連線效率,卻不知道 DELETE、UPDATE 下的子查詢卻可能導致更嚴重的鎖問題,直接導致 MySQL InnoDB 行鎖機制失效,鎖升級,嚴重影響資料庫的併發和效能。對大表或高併發的表的執行 DELETE、UPDATE 子查詢操作,甚至可能導致業務長時間不可用。

MySQL 下的 InnoDB 行鎖,是通過以點陣圖方式對 index page 加鎖機制來實現的。

而不是直接對相應的資料行和相關的 data page 加鎖,這樣的加鎖實現就導致了其行鎖實現的不穩定性。InnoDB這種行鎖實現特點意味著:只有通過有效索引條件檢索資料行,InnoDB 才使用行級鎖,否則,InnoDB 將使用表鎖!UPDATE、DELETE 子查詢條件下優化器的實現導致子查詢下的行鎖機制失效,行鎖升級,對更多無關的行資料加鎖,進而影響資料庫併發和效能 。

UPDATE、DELETE 子查詢鎖機制失效解析以及優化方案

下面以普通的 UPDATE 關聯子查詢更新來詳解子查詢對鎖機制的影響及具體優化解決方案:

子查詢下的事物、鎖機制分析:
優化器實現:

UPDATE pay_stream a
   SET
a.return_amount =(SELECT b.cash_amount FROM pay_main b WHERE a.pay_id = b.pay_id AND b.user_name = '1388888888');
  • 1
  • 2
  • 3
  • 4
  • 5
    id  select_type         table   partitions  type    possible_keys          key      key_len  ref                         rows  filtered  Extra        
------  --
----------------
------ ---------- ------ --------------------- ------- ------- ------------------------ ------ -------- ------------- 1 UPDATE a (NULL) index (NULL) PRIMARY 98 (NULL) 155041 100.00 (NULL) 2 DEPENDENT SUBQUERY b (NULL) eq_ref PRIMARY,idx_user_name PRIMARY 98 settlement_data.a.pay_id 1 5.00 Using where
  • 1
  • 2
  • 3
  • 4

從執行計劃可以看出該 update 子查詢,優化器先執行了 id 為2的 (DEPENDENT SUBQUERY )相關子查詢部分,然後通過對 PRIMARY 以索引全掃描方式對全表 155041 行資料加鎖主鎖,來執行的 update 操作,阻礙了了表的update、delete併發操作。
事物、鎖驗證:
事物一:
這裡寫圖片描述
事物二:
這裡寫圖片描述
事物二果真被事物一阻塞,事物一的子查詢操作的確鎖住了不相關的資料行,阻礙了資料庫的併發操作。

關聯更新下的事物、鎖機制分析:
優化器實現:

UPDATE pay_stream a INNER JOIN pay_main b ON a.pay_id = b.pay_id
   SET a.return_amount = b.cash_amount
 WHERE b.user_name = '1388888888';
  • 1
  • 2
  • 3
    id  select_type  table   partitions  type    possible_keys          key            key_len  ref                         rows  filtered  Extra   
------  -----------  ------  ----------  ------  ---------------------  -------------  -------  ------------------------  ------  --------  --------
     1  SIMPLE       b       (NULL)      ref     PRIMARY,idx_user_name  idx_user_name  387      const                          1    100.00  (NULL)  
     1  UPDATE       a       (NULL)      eq_ref  PRIMARY                PRIMARY        98       settlement_data.b.pay_id       1    100.00  (NULL)  
  • 1
  • 2
  • 3
  • 4
  • 5

從執行計劃可以看出,優化器先執行了通過 idx_user_name 索引執行了 b 表的檢索操作,然後再通過eq_ref 方式關聯 PRIMARY 更新了一行資料,並沒引起行鎖升級,影響表的併發操作。事物機制驗證如下:
事物一:
這裡寫圖片描述
事物二:
這裡寫圖片描述
不難看出 普通 join 關聯更新只對需要更新的資料行加索,更有利於資料庫的併發操作。

其它場景下 UPDATE、DELETE 子查詢的優化方案

其他場景下的子查詢,事物驗證不再詳述,優化器實現如下:
in/exists 子查詢
in 子查詢下優化器實現:

UPDATE pay_stream a
   SET a.return_amount = 0 WHERE a.pay_id IN (SELECT b.pay_id 
  FROM pay_main b WHERE  b.user_name = '1388888888');

    id  select_type         table   partitions  type             possible_keys          key      key_len  ref       rows  filtered  Extra        
------  ------------------  ------  ----------  ---------------  ---------------------  -------  -------  ------  ------  --------  -------------
     1  UPDATE              a       (NULL)      index            (NULL)                 PRIMARY  98       (NULL)  155044    100.00  Using where  
     2  DEPENDENT SUBQUERY  b       (NULL)      unique_subquery  PRIMARY,idx_user_name  PRIMARY  98       func         1      5.00  Using where  

DELETE a FROM pay_stream a WHERE a.pay_id IN (SELECT b.pay_id 
  FROM pay_main b WHERE  b.user_name = '1388888888');

    id  select_type  table   partitions  type    possible_keys          key            key_len  ref                         rows  filtered  Extra        
------  -----------  ------  ----------  ------  ---------------------  -------------  -------  ------------------------  ------  --------  -------------
     1  SIMPLE       b       (NULL)      ref     PRIMARY,idx_user_name  idx_user_name  387      const                          1    100.00  Using index  
     1  DELETE       a       (NULL)      eq_ref  PRIMARY                PRIMARY        98       settlement_data.b.pay_id       1    100.00  (NULL) 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

exists 子查詢下優化器實現:

UPDATE pay_stream a
   SET a.return_amount = 0 WHERE EXISTS (SELECT b.pay_id 
  FROM pay_main b WHERE a.pay_id = b.pay_id AND b.user_name = '1388888888');

    id  select_type         table   partitions  type    possible_keys          key      key_len  ref                         rows  filtered  Extra        
------  ------------------  ------  ----------  ------  ---------------------  -------  -------  ------------------------  ------  --------  -------------
     1  UPDATE              a       (NULL)      index   (NULL)                 PRIMARY  98       (NULL)                    155044    100.00  Using where  
     2  DEPENDENT SUBQUERY  b       (NULL)      eq_ref  PRIMARY,idx_user_name  PRIMARY  98       settlement_data.a.pay_id       1      5.00  Using where

DELETE a FROM pay_stream a WHERE EXISTS (SELECT 1 
  FROM pay_main b WHERE  a.pay_id = b.pay_id AND b.user_name = '1388888888');

    id  select_type         table   partitions  type    possible_keys          key      key_len  ref                         rows  filtered  Extra        
------  ------------------  ------  ----------  ------  ---------------------  -------  -------  ------------------------  ------  --------  -------------
     1  DELETE              a       (NULL)      ALL     (NULL)                 (NULL)   (NULL)   (NULL)                    155044    100.00  Using where  
     2  DEPENDENT SUBQUERY  b       (NULL)      eq_ref  PRIMARY,idx_user_name  PRIMARY  98       settlement_data.a.pay_id       1      5.00  Using where   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

inner join 下優化器實現:

UPDATE pay_stream a INNER JOIN pay_main b ON a.pay_id = b.pay_id
   SET a.return_amount = 0
 WHERE b.user_name = '1388888888';

    id  select_type  table   partitions  type    possible_keys          key            key_len  ref                         rows  filtered  Extra        
------  -----------  ------  ----------  ------  ---------------------  -------------  -------  ------------------------  ------  --------  -------------
     1  SIMPLE       b       (NULL)      ref     PRIMARY,idx_user_name  idx_user_name  387      const                          1    100.00  Using index  
     1  UPDATE       a       (NULL)      eq_ref  PRIMARY                PRIMARY        98       settlement_data.b.pay_id       1    100.00  (NULL)    

DELETE a FROM pay_stream a INNER JOIN pay_main b ON a.pay_id = b.pay_id
  WHERE b.user_name = '1388888888'; 

    id  select_type  table   partitions  type    possible_keys          key            key_len  ref                         rows  filtered  Extra        
------  -----------  ------  ----------  ------  ---------------------  -------------  -------  ------------------------  ------  --------  -------------
     1  SIMPLE       b       (NULL)      ref     PRIMARY,idx_user_name  idx_user_name  387      const                          1    100.00  Using index  
     1  DELETE       a       (NULL)      eq_ref  PRIMARY                PRIMARY        98       settlement_data.b.pay_id       1    100.00  (NULL)                                                                                                                                                           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

從上述的優化器行為不難看出,inner join 聯表的情況下,只對需更新的資料行加索,併發效能最高;exitsts 子查詢在 delete 與 update 操作下,均為全索引掃描,併發最差;in 子查詢在 update 操作下與 exists 一樣為全索引掃描,而在 delete 操作下為主鍵操作,只對對應的行更新的資料行加索,併發次之。

not in /not exists 子查詢
not in 子查詢下優化器實現:

UPDATE pay_stream a
   SET a.return_amount = 0 
  WHERE a.pay_id NOT IN (SELECT b.pay_id 
     FROM pay_main b WHERE  b.pay_time > '2017-08-12 00:00:00'); 

    id  select_type         table   partitions  type             possible_keys                  key      key_len  ref       rows  filtered  Extra        
------  ------------------  ------  ----------  ---------------  -----------------------------  -------  -------  ------  ------  --------  -------------
     1  UPDATE              a       (NULL)      index            (NULL)                         PRIMARY  98       (NULL)  155182    100.00  Using where  
     2  DEPENDENT SUBQUERY  b       (NULL)      unique_subquery  PRIMARY,IDX_PAY_MAIN_PAY_TIME  PRIMARY  98       func         1     46.46  Using where  

DELETE a FROM pay_stream a WHERE a.pay_id NOT IN (SELECT b.pay_id 
  FROM pay_main b WHERE b.pay_time >= '2017-08-12 00:00:00');

    id  select_type         table   partitions  type             possible_keys                  key      key_len  ref       rows  filtered  Extra        
------  ------------------  ------  ----------  ---------------  -----------------------------  -------  -------  ------  ------  --------  -------------
     1  DELETE              a       (NULL)      ALL              (NULL)                         (NULL)   (NULL)   (NULL)  155182    100.00  Using where  
     2  DEPENDENT SUBQUERY  b       (NULL)      unique_subquery  PRIMARY,IDX_PAY_MAIN_PAY_TIME  PRIMARY  98       func         1     46.46  Using where  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

not exists 子查詢下優化器實現:

UPDATE pay_stream a
   SET a.return_amount = 0 
 WHERE NOT EXISTS (SELECT b.pay_id 
    FROM pay_main b WHERE a.pay_id = b.pay_id AND b.pay_time > '2017-08-12 00:00:00');

    id  select_type         table   partitions  type    possible_keys                  key      key_len  ref                rows  filtered  Extra        
------  ------------------  ------  ----------  ------  -----------------------------  -------  -------  ---------------  ------  --------  -------------
     1  UPDATE              a       (NULL)      index   (NULL)                         PRIMARY  98       (NULL)           155182    100.00  Using where  
     2  DEPENDENT SUBQUERY  b       (NULL)      eq_ref  PRIMARY,IDX_PAY_MAIN_PAY_TIME  PRIMARY  98       settle.a.pay_id       1     46.46  Using where

DELETE a FROM pay_stream a
 WHERE NOT EXISTS (SELECT 1
          FROM pay_main b
         WHERE a.pay_id = b.pay_id AND b.pay_time >= '2017-08-12 00:00:00');

    id  select_type         table   partitions  type    possible_keys                  key      key_len  ref                rows  filtered  Extra        
------  ------------------  ------  ----------  ------  -----------------------------  -------  -------  ---------------  ------  --------  -------------
     1  DELETE              a       (NULL)      ALL     (NULL)                         (NULL)   (NULL)   (NULL)           155182    100.00  Using where  
     2  DEPENDENT SUBQUERY  b       (NULL)      eq_ref  PRIMARY,IDX_PAY_MAIN_PAY_TIME  PRIMARY  98       settle.a.pay_id       1     46.46  Using where  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

left join 下優化器實現:

UPDATE pay_stream a LEFT JOIN pay_main b 
  ON a.pay_id = b.pay_id AND b.pay_time >= '2017-08-12 00:00:00'
   SET a.return_amount = 0
 WHERE b.pay_id IS NULL;

    id  select_type  table   partitions  type    possible_keys                  key      key_len  ref                rows  filtered  Extra                    
------  -----------  ------  ----------  ------  -----------------------------  -------  -------  ---------------  ------  --------  -------------------------
     1  UPDATE       a       (NULL)      ALL     (NULL)                         (NULL)   (NULL)   (NULL)           155182    100.00  (NULL)                   
     1  SIMPLE       b       (NULL)      eq_ref  PRIMARY,IDX_PAY_MAIN_PAY_TIME  PRIMARY  98       settle.a.pay_id       1    100.00  Using where; Not exists  

DELETE a FROM pay_stream a LEFT JOIN pay_main b 
  ON a.pay_id = b.pay_id AND b.pay_time >= '2017-08-12 00:00:00'
 WHERE b.pay_id IS NULL;

    id  select_type  table   partitions  type    possible_keys                  key      key_len  ref                rows  filtered  Extra                    
------  -----------  ------  ----------  ------  -----------------------------  -------  -------  ---------------  ------  --------  -------------------------
     1  DELETE       a       (NULL)      ALL     (NULL)                         (NULL)   (NULL)   (NULL)           155182    100.00  (NULL)                   
     1  SIMPLE       b       (NULL)      eq_ref  PRIMARY,IDX_PAY_MAIN_PAY_TIME  PRIMARY  98       settle.a.pay_id       1    100.00  Using where; Not exists  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

從上述優化器的行為分析不難看出,left join 完全持有 a 表表鎖,其間完全失去了併發;not in 與 not exists 執行計劃類似,delete 操作下持有表鎖,完全不支援併發,update 操作下以 PRIMARY 索引全掃描的方式,鎖住了表中資料行,阻礙了對錶的 delete,update 操作,但不妨礙 insert 的併發操作,但 (DEPENDENT SUBQUERY) 相關子查詢下 unique_subquery 的檢索效率高於 eq_ref 方式,delete、update下的 not in 子查詢效能和併發支援最高。

MySQL 優化器以及 InnoDB 行鎖機制特性,增加了 UPDATE、DELETE 下子查詢複雜的度,資料庫技術也在不斷進步,在 MySQL 資料庫程式開發資料庫維護過程中,真正瞭解優化器的實現和 InnoDB 行鎖機制的行為,才有能設計出正真的高併發系統和形成良好的運維習慣。