由select for update鎖等待問題引發的深入思考

来源:https://www.cnblogs.com/coygfly/p/18026342
-Advertisement-
Play Games

要實現兩個資料庫之間的實時同步,需要給Oracle設置參數 ALTER DATABASE ADD SUPPLEMENTAL LOG DATA; -- 執行了12小時,等待資料庫中的其它事務都提交以後才執行完成 ALTER DATABASE ADD SUPPLEMENTAL LOG DATA (PRI ...


關於MySQL的加鎖機制,其實十分複雜,不同的隔離級別,是否是主鍵或索引,鎖的粒度等等。很多工作了很多年的MySQL DBA也不能把各種加鎖場景一一講清楚。有時候一個簡單的鎖等待場景都值得深入研究,大家更多的是知其然而不知其所以然。本文介紹的是一個很常見的鎖等待問題,但很少有人知道其中的原理。

一、實驗場景

本文實驗和研究的MySQL版本為8.0.31,資料庫的隔離級別設置為RC,創建一張表,併在表中插入數據:

create table siri(
id int not null auto_increment,
a int not null,
b int not null,
c int not null,
primary key (id),
unique key uniq_a (a),
key idx_c (c)
)

insert into siri values (1,1,1,1),(2,2,2,2),(4,4,4,4),(6,6,6,4);

好的,現在可以開始模擬實驗場景了:

實驗一:

Session1

Session2

mysql> begin;

Query OK, 0 rows affected (0.00 sec)

 

mysql> select * from siri where b=1 for update;

+----+---+---+---+

| id | a | b | c |

+----+---+---+---+

|  1 | 1 | 1 | 1 |

+----+---+---+---+

1 row in set (0.00 sec)

 
 

mysql> select * from siri where b=4 for update;

ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

實驗二:

Session1

Session2

mysql> begin;

Query OK, 0 rows affected (0.00 sec)

 

mysql> select * from siri where id=1 for update;

+----+---+---+---+

| id | a | b | c |

+----+---+---+---+

|  1 | 1 | 1 | 1 |

+----+---+---+---+

1 row in set (0.00 sec)

 
 

mysql> select * from siri where b=4 for update;

ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

實驗三:

Session1

Session2

mysql> begin;

Query OK, 0 rows affected (0.00 sec)

 

mysql> select * from siri where b=1 for update;

+----+---+---+---+

| id | a | b | c |

+----+---+---+---+

|  1 | 1 | 1 | 1 |

+----+---+---+---+

1 row in set (0.00 sec)

 
 

mysql> select * from siri where id=4 for update;

+----+---+---+---+

| id | a | b | c |

+----+---+---+---+

|  4 | 4 | 4 | 4 |

+----+---+---+---+

1 row in set (0.00 sec)

從以上三個實驗可以看出,session2是否被堵塞與session1中語句的條件欄位是否是索引無關,而與session2select for update語句的條件欄位有關,session2中條件欄位無索引則會被堵塞。
mysql> select * from performance_schema.data_locks\G
*************************** 1. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 139907486244056:1220:139907418869440
ENGINE_TRANSACTION_ID: 3816000
            THREAD_ID: 52900
             EVENT_ID: 44
        OBJECT_SCHEMA: test
          OBJECT_NAME: siri
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: NULL
OBJECT_INSTANCE_BEGIN: 139907418869440
            LOCK_TYPE: TABLE
            LOCK_MODE: IX
          LOCK_STATUS: GRANTED
            LOCK_DATA: NULL
*************************** 2. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 139907486244056:59:4:2:139907418866384
ENGINE_TRANSACTION_ID: 3816000
            THREAD_ID: 52900
             EVENT_ID: 44
        OBJECT_SCHEMA: test
          OBJECT_NAME: siri
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: PRIMARY
OBJECT_INSTANCE_BEGIN: 139907418866384
            LOCK_TYPE: RECORD
            LOCK_MODE: X,REC_NOT_GAP
          LOCK_STATUS: GRANTED
            LOCK_DATA: 1
2 rows in set (0.00 sec)
mysql> select * from sys.innodb_lock_waits\G
*************************** 1. row ***************************
                wait_started: 2023-11-16 14:23:49
                    wait_age: 00:00:02
               wait_age_secs: 2
                locked_table: `test`.`siri`
         locked_table_schema: test
           locked_table_name: siri
      locked_table_partition: NULL
   locked_table_subpartition: NULL
                locked_index: PRIMARY
                 locked_type: RECORD
              waiting_trx_id: 3816028
         waiting_trx_started: 2023-11-16 14:23:49
             waiting_trx_age: 00:00:02
     waiting_trx_rows_locked: 1
   waiting_trx_rows_modified: 0
                 waiting_pid: 54820
               waiting_query: select * from siri where b=4 for update
             waiting_lock_id: 139907486245672:59:4:2:139907418878432
           waiting_lock_mode: X,REC_NOT_GAP
             blocking_trx_id: 3816020
                blocking_pid: 54783
              blocking_query: NULL
            blocking_lock_id: 139907486244056:59:4:2:139907418866384
          blocking_lock_mode: X,REC_NOT_GAP
        blocking_trx_started: 2023-11-16 14:16:49
            blocking_trx_age: 00:07:02
    blocking_trx_rows_locked: 1
  blocking_trx_rows_modified: 0
     sql_kill_blocking_query: KILL QUERY 54783
sql_kill_blocking_connection: KILL 54783
1 row in set (0.01 sec)

查詢上面監控視圖可以發現,在實驗一和實驗二中,session1所申請的鎖資源也是一樣的,一個是表級別的IX鎖,一個是行級別的X鎖。而造成鎖等待的鎖是行鎖。所以這時候就有一個疑問了,行鎖鎖定的是b=1這一行,為啥session2中我們要申請b=4這一行的行鎖會發生鎖等待呢?其實原因也顯而易見了:欄位b無索引,申請b=4這一行的行鎖會掃描全表,也就是說對錶數據的每一行都會申請X鎖。而在實驗三中,可以走主鍵索引直接定位到b=4這一行,所以就不會造成鎖等待了。

下麵再看一個實驗四:

Session1

Session2

mysql> begin;

Query OK, 0 rows affected (0.00 sec)

 

mysql> select * from siri where b=1 for update;

+----+---+---+---+

| id | a | b | c |

+----+---+---+---+

|  1 | 1 | 1 | 1 |

+----+---+---+---+

1 row in set (0.00 sec)

 
 

mysql> update siri set c=4 where b=4;

Query OK, 0 rows affected (0.00 sec)

Rows matched: 1  Changed: 0  Warnings: 0

可以發現,session2中直接對b=4這一行進行update是可以直接成功的,不會被阻塞。這說明update的加鎖流程和select for update是不一樣的。可以推測一下這兩種加鎖流程有什麼區別:session2update進行更新時也會掃描全表,但是遇到第一個鎖等待時會做一個判斷,發現鎖住的行不是需要update的行時,則會跳過這個鎖,這樣就不會影響真正需要update的行,而select for update則不會做這個跳過,會一直等待鎖。

二、解讀源碼

為了驗證我的猜想,深究背後的原理,還是得在實際場景下調試一下源碼,閱讀源碼才能更好的瞭解為什麼是這樣的。

mysql源碼中,負責給行加鎖的函數是sel_set_rec_lock,我們可以在該函數處打下斷點看看select for updateupdate這兩種sql在申請鎖的流程上面有什麼區別。

/** Sets a lock on a record.
mostly due to we cannot reposition a record in R-Tree (with the
nature of splitting)
@param[in]      pcur            cursor
@param[in]      rec             record
@param[in]      index           index
@param[in]      offsets         rec_get_offsets(rec, index)
@param[in]      sel_mode        select mode: SELECT_ORDINARY,
                                SELECT_SKIP_LOKCED, or SELECT_NO_WAIT
@param[in]      mode            lock mode
@param[in]      type            LOCK_ORDINARY, LOCK_GAP, or LOC_REC_NOT_GAP
@param[in]      thr             query thread
@param[in]      mtr             mtr
@return DB_SUCCESS, DB_SUCCESS_LOCKED_REC, or error code */
static inline dberr_t sel_set_rec_lock(btr_pcur_t *pcur, const rec_t *rec,
                                       dict_index_t *index,
                                       const ulint *offsets,
                                       select_mode sel_mode, ulint mode,
                                       ulint type, que_thr_t *thr, mtr_t *mtr) {
  trx_t *trx;
  dberr_t err = DB_SUCCESS;
  const buf_block_t *block;

  block = pcur->get_block();

  trx = thr_get_trx(thr);
  ut_ad(trx_can_be_handled_by_current_thread(trx));

  if (UT_LIST_GET_LEN(trx->lock.trx_locks) > 10000) {
    if (buf_LRU_buf_pool_running_out()) {
      return (DB_LOCK_TABLE_FULL);
    }
  }

  if (index->is_clustered()) {
    err = lock_clust_rec_read_check_and_lock(
        lock_duration_t::REGULAR, block, rec, index, offsets, sel_mode,
        static_cast<lock_mode>(mode), type, thr);
  } else {
    if (dict_index_is_spatial(index)) {
      if (type == LOCK_GAP || type == LOCK_ORDINARY) {
        ib::error(ER_IB_MSG_1026) << "Incorrectly request GAP lock "
                                     "on RTree";
        ut_d(ut_error);
        ut_o(return (DB_SUCCESS));
      }
      err = sel_set_rtr_rec_lock(pcur, rec, index, offsets, sel_mode, mode,
                                 type, thr, mtr);
    } else {
      err = lock_sec_rec_read_check_and_lock(
          lock_duration_t::REGULAR, block, rec, index, offsets, sel_mode,
          static_cast<lock_mode>(mode), type, thr);
    }
  }

  return (err);
}

mysqldebug模式中執行select * from testdb.siri where b=4 for updategdb中命中sel_set_rec_lock函數斷點,函數堆棧信息如下:

#0  sel_set_rec_lock (pcur=0x7f52040e3ef8, rec=0x7f521e05c07d "\200", index=0x7f52040e8028, offsets=0x7f52146f3bc0, sel_mode=SELECT_ORDINARY, mode=3, type=1024, 
    thr=0x7f52040e4700, mtr=0x7f52146f3ef0) at /root/gdb_mysql/mysql-8.0.32/storage/innobase/row/row0sel.cc:1142

執行update testdb.siri set c=2 where b=4,函數堆棧信息如下:

#0  sel_set_rec_lock (pcur=0x7f52040e3ef8, rec=0x7f521e05c07d "\200", index=0x7f52040e8028, offsets=0x7f52146f3630, sel_mode=SELECT_SKIP_LOCKED, mode=3, type=1024, 
    thr=0x7f52040e4700, mtr=0x7f52146f3960) at /root/gdb_mysql/mysql-8.0.32/storage/innobase/row/row0sel.cc:1142

發現了兩者的區別嗎?區別在於sel_mode這個參數是不同的:對於select for updatesel_modeSELECT_ORDINARY;對於updatesel_modeSELECT_SKIP_LOCKEDsel_mode參數的定義如下:

enum select_mode {
  SELECT_ORDINARY,    /* default behaviour */
  SELECT_SKIP_LOCKED, /* skip the row if row is locked */
  SELECT_NOWAIT       /* return immediately if row is locked */
};

row_search_mvcc函數中,通過以下代碼來判定這條sql是否為半一致性讀(semi-consistent read)。

/* in case of semi-consistent read, we use SELECT_SKIP_LOCKED, so we don't
waste time on creating a WAITING lock, as we won't wait on it anyway */
const bool use_semi_consistent =
    prebuilt->row_read_type == ROW_READ_TRY_SEMI_CONSISTENT &&
    !unique_search && index == clust_index && !trx_is_high_priority(trx);
err = sel_set_rec_lock(
    pcur, rec, index, offsets,
    use_semi_consistent ? SELECT_SKIP_LOCKED : prebuilt->select_mode,
    prebuilt->select_lock_type, lock_type, thr, &mtr);

update語句是半一致性讀,因此use_semi_consistenttrueselect_modeSELECT_SKIP_LOCKED,這表示會話不會浪費時間在創建鎖等待上,可以跳過持有鎖的行。而對於select for update語句,use_semi_consistentfalseselect_modeSELECT_ORDINARY,表示會話會創建一個鎖等待,直到等待超時。

因此,對於實驗四中的現象update不會被堵塞的原因已經比較清楚了,updatemysql內部被定義成了半一致性讀(SELECT_SKIP_LOCKED),因此實驗四的session2update進行全表掃描讀取主鍵時,讀取到b=1這一列時,會跳過session1所持有的位於b=1行上的行鎖,所以也就不會發生鎖等待的現象。相反,實驗二中select for updatemysql內部定義為普通讀(SELECT_ORDINARY),讀取到b=1這一列時,會被session1所持有的位於b=1行上的行鎖堵塞,發生鎖等待的現象。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 哈嘍大家好,我是鹹魚。 之前寫過兩篇關於 SSL 過期巡檢腳本的文章: SSL 證書過期巡檢腳本 SSL 證書過期巡檢腳本(Python 版) 這兩篇文章都是講如何通過腳本去自動檢測 SSL 過期時間的,當我們發現某一功能變數名稱的 SSL 證書過期之後,就要及時更換。 如果這個功能變數名稱下有很多伺服器,我們一臺 ...
  • 當用戶需要的計算或者存儲資源冗餘超出業務需求時,可在管理控制台對已有集群進行縮容操作,以便充分利用GaussDB(DWS) 提供的計算資源和存儲資源。 ...
  • MySQL資料庫中,如何查看表和欄位的註釋信息,以及如何添加,修改表和欄位的註釋信息呢?這裡簡單總結歸納一下。僅供參考。 添加表的註釋信息 方法1:創建表的時候添加表的註釋信息 create table if not exists employee( employee_id int not null ...
  • SQL Server的配置管理器(SQL Server Configuration Manager)下,你可以隱藏資料庫實例,設置了隱藏實例後, SQL Server Browser不會對此伺服器實例的相關信息做出響應,(當 SQL Server 客戶端請求 SQL Server 資源時,客戶端將使 ...
  • 本文分享自華為雲社區《華為雲峰會2024,GaussDB揚帆出海,給世界一個更優選擇》,作者:GaussDB 資料庫。 2024年2月26~29日,由GSM協會主辦的“2024年世界移動通信大會”(Mobile World Congress 2024,簡稱MWC 2024)在西班牙巴塞羅那舉行。MW ...
  • GreatSQL TPC-H 性能測試報告 - (2024 年 2 月28日) 完整性能測試報告:https://greatsql.cn/docs/8032-25/user-manual/10-optimze/3-3-benchmark-greatsql-tpch-report.html 1、概述 ...
  • 本文分享自華為雲社區《GaussDB(DWS)等待視圖之Hashjoin-nestloop》,作者:Arrow0lf。 1. 業務場景 眾所周知,GaussDB(DWS)中有3種常見的join方式:HashJon/MergeJoin/NestLoop 但在有一些場景中,等待視圖中等待狀態會顯示為:H ...
  • 在資料庫設計中,存儲過程、觸發器、游標、視圖、自定義函數、欄位類型、欄位可空、統計欄位、邏輯刪除以及許可權系統和無限級類別設計都是重要的概念。下麵我將逐一解釋這些概念,並提供相關的設計建議。 存儲過程 (Stored Procedure) 定義:存儲過程是一組為了完成特定功能的SQL語句集,經編譯後存 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...