精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

答讀者問:唯一索引沖突,為什么主鍵的 Supremum 記錄會加 Next-Key 鎖?

數據庫 MySQL
我在 MySQL 8.0.32 復現了問題,并調試了加鎖流程,寫下來和大家分享。了解完整的加鎖流程,有助于我們更深入的理解 InnoDB 的記錄鎖,希望大家有收獲。

本文緣起于一位讀者的提問:插入一條記錄,導致唯一索引沖突,為什么會對主鍵的 supremum 記錄加 next-key 排他鎖?

我在 MySQL 8.0.32 復現了問題,并調試了加鎖流程,寫下來和大家分享。

了解完整的加鎖流程,有助于我們更深入的理解 InnoDB 的記錄鎖,希望大家有收獲。

本文基于 MySQL 8.0.32 源碼,存儲引擎為 InnoDB。

1、準備工作

創建測試表:

CREATE TABLE `t6` (
  `id` int unsigned NOT NULL AUTO_INCREMENT,
  `i1` int unsigned NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uniq_i1` (`i1`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;

插入測試數據:

INSERT INTO `t6`(i1) VALUES
  (1001), (1002), (1003),
  (1004), (1005), (1006);

設置事務隔離級別:
在 my.cnf 中,把系統變量 transaction_isolation 設置為 REPEATABLE-READ。

2、問題復現

插入一條會導致唯一索引沖突的記錄:

BEGIN;
INSERT INTO `t6`(i1) VALUES(1001);

通過 BEGIN 顯式開啟事務,INSERT 執行完成之后,我們可以通過以下 SQL 查看加鎖情況:

SELECT 
  OBJECT_NAME, INDEX_NAME, LOCK_TYPE,
  LOCK_MODE, LOCK_STATUS, LOCK_DATA
FROM `performance_schema`.`data_locks`;

結果如下:

唯一索引(uniq_i1):id = 1,i1 = 1001 的記錄,加 next-key 共享鎖。

主鍵索引(PRIMARY):supremum 記錄,加 next-key 排他鎖。

3、前置知識點:隱式鎖

插入記錄時,隱式鎖是個比較重要的概念,它存在的目的是:減少插入記錄時不必要的加鎖,提升 MySQL 的并發能力。

我們先來看一下隱式鎖的定義:

事務 T 要插入一條記錄 R,只要即將插入記錄的目標位置沒有被其它事務上鎖,事務 T 就不需要申請對目標位置加鎖,可以直接插入記錄。

事務 T 提交之前,如果其它事務出現以下 2 種情況,都必須幫助事務 T 給記錄 R 加上排他鎖:

  • 其它事務執行 UPDATE、DELETE 語句時掃描到了記錄 R。
  • 其它事務插入的記錄和 R 存在主鍵或唯一索引沖突。

未提交事務 T 插入的記錄上,這種隱性的、由其它事務在需要時幫忙創建的鎖,就是隱式鎖

隱式鎖,就像神話電視劇里的結界。沒有觸碰到它時,看不見,就像不存在一樣,一旦觸碰到,它就顯現出來了。

隱式鎖可能出現于多種場景,我們來看看主鍵索引的 2 種隱式鎖場景:

前提條件:

事務 T1 插入一條記錄 R1,即將插入 R1 的目標位置沒有被其它事務上鎖,事務 T1 可以直接插入 R1。

場景 1:

事務 T1 插入 R1 之后,提交事務之前,事務 T2 試圖插入一條記錄 R2(主鍵字段值和 R1 相同)。

事務 T2 給 R2 尋找插入位置的過程中,就會發現 R2 和 R1 沖突,并且插入 R1 的事務 T1 還沒有提交,這就觸發了 R1 的隱式鎖邏輯。

事務 T2 會幫助 T1 給 R1 加上排他鎖,然后,它自己會申請對 R1 加共享鎖,并等待事務 T1 釋放 R1 上的排他鎖。

事務 T1 釋放 R1 的鎖之后,如果事務 T2 沒有鎖等待超時,它獲取到 R1 上的鎖之后,就可以繼續進行主鍵沖突的后續處理邏輯了。

場景 2:

事務 T1 插入 R1 之后,提交事務之前,事務 T3 執行 UPDATE 或 DELETE 語句時掃描到了 R1,發現插入 R1 的事務 T1 還沒有提交,同樣觸發了 R1 的隱式鎖邏輯。

事務 T3 會幫助 T1 給 R1 加上排他鎖,然后,它自己會申請對 R1 加排他鎖,并等待事務 T1 釋放 R1 上的排他鎖。

事務 T1 提交并釋放 R1 的鎖之后,如果事務 T3 沒有鎖等待超時,它獲取到 R1 上的鎖之后,就可以繼續對 R1 進行修改或刪除操作了。

對隱式鎖有了大概了解之后,接下來,我們回到本文主題,來看看 INSERT 執行過程中的加鎖流程。

4、流程分析

我們先來看一下主要堆棧,接下來的流程分析圍繞這個堆棧進行:

| > row_insert_for_mysql_using_ins_graph() storage/innobase/row/row0mysql.cc:1585
| + > row_ins_step(que_thr_t*) storage/innobase/row/row0ins.cc:3677
| + - > row_ins(ins_node_t*, que_thr_t*) storage/innobase/row/row0ins.cc:3559
| + - x > row_ins_index_entry_step(ins_node_t*, que_thr_t*) storage/innobase/row/row0ins.cc:3435
| + - x = > row_ins_index_entry() storage/innobase/row/row0ins.cc:3303
| + - x = | > row_ins_sec_index_entry() storage/innobase/row/row0ins.cc:3203
| + - x = | + > row_ins_sec_index_entry_low() storage/innobase/row/row0ins.cc:2926
| + - x = | + - > row_ins_scan_sec_index_for_duplicate() storage/innobase/row/row0ins.cc:1894
| + > row_mysql_handle_errors() storage/innobase/row/row0mysql.cc:701

這個堆棧的關鍵步驟有 2 個:

  • row_ins_step(),插入記錄到主鍵、唯一索引。
  • row_mysql_handle_errors(),插入失敗之后,進行錯誤處理。

(1)插入記錄到主鍵、唯一索引

// storage/innobase/row/row0mysql.cc
static dberr_t row_insert_for_mysql_using_ins_graph(...) {
  ...
  // 主要構造用于執行插入操作的 2 個對象:
  // 1. ins_node_t 對象,保存在 prebuilt->ins_node 中
  // 2. que_fork_t 對象,保存在 prebuilt->ins_graph 中
  row_get_prebuilt_insert_row(prebuilt);
  node = prebuilt->ins_node;

  // 把 server 層的記錄格式轉換為 InnoDB 的記錄格式
  row_mysql_convert_row_to_innobase(node->row, prebuilt, mysql_rec, &temp_heap);
  ...
  // 執行插入操作
  row_ins_step(thr);
  ...
  if (err != DB_SUCCESS) {
  error_exit:
    que_thr_stop_for_mysql(thr);
    ...
    // 錯誤處理
    auto was_lock_wait = row_mysql_handle_errors(&err, trx, thr, &savept);
    ...
    return (err);
  }
  ...
}

這個方法的主要邏輯:

  • 調用 row_get_prebuilt_insert_row(),構造包含插入數據的 ins_node_t 對象、查詢執行圖 que_fork_t 對象,分別保存到 prebuilt 的 ins_node、ins_graph 屬性中。
  • 把 server 層的記錄格式轉換為 InnoDB 的記錄格式。
  • 調用 row_ins_step(),插入記錄到主鍵索引、二級索引(包含唯一索引、非唯一索引)。
// storage/innobase/row/row0ins.cc
que_thr_t *row_ins_step(que_thr_t *thr)
{
  ...
  // 重置 node->trx_id_buf 指針指向的內存區域
  memset(node->trx_id_buf, 0, DATA_TRX_ID_LEN);
  // 把當前事務 ID 拷貝到 node->trx_id_buf 指針指向的內存區域
  trx_write_trx_id(node->trx_id_buf, trx->id);

  if (node->state == INS_NODE_SET_IX_LOCK) {
    ...
    // 給表加上意向鎖
    err = lock_table(0, node->table, LOCK_IX, thr);
    ...
  }
  ...
  err = row_ins(node, thr);
  ...
  return (thr);
}

row_ins_step() 調用 row_ins() 插入記錄到主鍵索引、二級索引。

// storage/innobase/row/row0ins.cc
[[nodiscard]] static dberr_t row_ins(...)
{
  ...
  // 迭代表中的索引,插入記錄到索引中
  while (node->index != nullptr) {
    // 只要不是全文索引
    if (node->index->type != DICT_FTS) {
      // 調用 row_ins_index_entry_step()
      // 插入記錄到當前迭代的索引中
      err = row_ins_index_entry_step(node, thr);

      switch (err) {
        // 執行成功,跳出 switch
        // 會接著進行下一輪迭代
        case DB_SUCCESS:
          break;
        // 存在主鍵索引或唯一索引沖突
        case DB_DUPLICATE_KEY:
          thr_get_trx(thr)->error_state = DB_DUPLICATE_KEY;
          thr_get_trx(thr)->error_index = node->index;
          // 貫穿到 default 分支
          [[fallthrough]];
        default:
          // 返回錯誤碼 DB_DUPLICATE_KEY
          return err;
      }
    }

    // 插入記錄到主鍵索引或二級索引成功
    // node->index、entry 指向表中的下一個索引
    node->index = node->index->next();
    node->entry = UT_LIST_GET_NEXT(tuple_list, node->entry);
    ...
  }
  ...
}

row_ins() 的主要邏輯是個 while 循環,逐個迭代表中的索引,每迭代一個索引,都把構造好的記錄插入到索引中。迭代完全部索引之后,插入一條記錄到表中的操作就完成了。

接下來,我們通過示例 SQL 來看看 row_ins() 的具體執行流程。

-- 為了方便,這里再展示下測試表和示例 SQL
CREATE TABLE `t6` (
  `id` int unsigned NOT NULL AUTO_INCREMENT,
  `i1` int unsigned NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uniq_i1` (`i1`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;

INSERT INTO `t6`(i1) VALUES(1001);

測試表 t6 有兩個索引:主鍵索引、uniq_i1(唯一索引),對于示例 SQL,上面代碼中的 while 會進行 2 輪迭代:

第 1 輪,調用 row_ins_index_entry_step(),插入記錄到主鍵索引。示例 SQL 沒有指定主鍵字段值,主鍵字段會使用自增值,不會和表中原有記錄沖突,插入操作能執行成功。

第 2 輪,調用 row_ins_index_entry_step(),插入記錄到 uniq_i1。新插入記錄的 i1 字段值為 1001,和表中原有記錄(id = 1)的 i1 字段值相同,會導致唯一索引沖突。

圖片

row_ins_index_entry_step() 插入記錄到 uniq_i1,導致唯一索引沖突,它會返回錯誤碼 DB_DUPLICATE_KEY 給 row_ins()。

row_ins() 拿到錯誤碼之后,它的執行流程到此結束,把錯誤碼返回給調用者。

當執行流程帶著錯誤碼(DB_DUPLICATE_KEY)一路返回到 row_insert_for_mysql_using_ins_graph(),接下來會調用 row_mysql_handle_errors() 處理唯一索引沖突的善后邏輯(這部分留到 4.3 回滾語句再聊)。

介紹唯一索引沖突的善后邏輯之前,我們以 row_ins_sec_index_entry_low() 為入口,一路跟隨執行流程進入 row_ins_sec_index_entry_low(),來看看給唯一索引中沖突記錄加 next-key 共享鎖的流程。

這里的 next-key 共享鎖,就是下圖中 LOCK_DATA = 1001,1 對應的鎖。

(2)唯一索引記錄加鎖

// storage/innobase/row/row0ins.cc
dberr_t row_ins_sec_index_entry_low(...) {
  ...
  if (dict_index_is_spatial(index)) {
    // 處理空間索引的邏輯
    ...
  } else {
    if (index->table->is_intrinsic()) {
      // MySQL 內部臨時表
      ...
    } else {
      // 找到記錄將要插入到哪個位置
      btr_cur_search_to_nth_level(index, 0, entry, PAGE_CUR_LE, search_mode,
                                  &cursor, 0, __FILE__, __LINE__, &mtr);
    }
  }
  ...
  // 索引中需要用幾個(n_unique)字段
  // 才能唯一標識一條記錄
  n_unique = dict_index_get_n_unique(index);
  // 如果是主鍵索引或唯一索引
  if (dict_index_is_unique(index) &&
      // 并且即將插入的記錄
      // 和索引中的記錄相同
      (cursor.low_match >= n_unique || cursor.up_match >= n_unique)) {
    ...
    // 判斷新插入記錄是否會導致沖突
    // 如果會導致沖突,會對沖突記錄加鎖
    err = row_ins_scan_sec_index_for_duplicate(flags, index, entry, thr, check,
                                               &mtr, offsets_heap);
    ...
  }
  ...
}

row_ins_sec_index_entry_low() 找到插入記錄的目標位置之后,如果發現這個位置已經有一條相同的記錄了,說明有可能導致唯一索引沖突,調用 row_ins_scan_sec_index_for_duplicate() 確認是否沖突,并根據情況進行加鎖處理。

// storage/innobase/row/row0ins.cc
[[nodiscard]] static dberr_t row_ins_scan_sec_index_for_duplicate(...)
{
  ...
  // SQL 語句是否包含解決主鍵、唯一索引沖突的邏輯
  allow_duplicates = row_allow_duplicates(thr);
  ...
  do {
    ...
    if (flags & BTR_NO_LOCKING_FLAG) {
      /* Set no locks when applying log in online table rebuild. */
    } else if (allow_duplicates) {
      ...
      // 如果 SQL 語句包含解決主鍵、唯一索引沖突的邏輯
      // 給沖突記錄加排他鎖(LOCK_X)
      err = row_ins_set_rec_lock(LOCK_X, lock_type, block, rec, index, offsets,
                                 thr);
    } else /* else_1 */ {
      if (skip_gap_locks) {
        // 如果是數據字典表、SDI 表
        // 決定加什么鎖,忽略
        ...
      } else if (is_supremum) {
        /* We use next key lock to possibly combine the locks in bitmap.
        Equivalent to LOCK_GAP. */
        // next-key 鎖
        lock_type = LOCK_ORDINARY;
      } else if (is_next) {
        /* Only gap lock is required on next record. */
        // gap 鎖
        lock_type = LOCK_GAP;
      } else /* else_2 */ {
        /* Next key lock for all equal keys. */
        // next-key 鎖
        lock_type = LOCK_ORDINARY;
      }
      ...
      // SQL 語句【不包含】解決主鍵、唯一索引沖突的邏輯
      // 給沖突記錄加共享鎖(LOCK_S)
      err = row_ins_set_rec_lock(LOCK_S, lock_type, block, rec, index, offsets,
                                 thr);
    }
    ...
    if (is_supremum) {
      continue;
    }
    // !index->allow_duplicates = true
    // 即 index->allow_duplicates = false 
    // 表示不允許索引中存在重復記錄
    // 調用 row_ins_dupl_error_with_rec()
    // 確定新插入記錄是否會導致索引沖突
    if (!is_next && !index->allow_duplicates) {
      if (row_ins_dupl_error_with_rec(rec, entry, index, offsets)) {
        // 返回 true,說明會導致索引沖突
        // 把錯誤碼賦值給 err 變量
        // 作為方法的返回值
        err = DB_DUPLICATE_KEY;
        ...
        goto end_scan;
      }
    } else /* else_3 */ {
      ut_a(is_next || index->allow_duplicates);
      goto end_scan;
    }
  } while (pcur.move_to_next(mtr));

end_scan:
  /* Restore old value */
  dtuple_set_n_fields_cmp(entry, n_fields_cmp);

  return err;
}

以下 3 種 SQL,allow_duplicates = true,表示 SQL 包含解決主鍵、唯一索引沖突的邏輯:

  • load datafile replace
  • replace into
  • insert ... on duplicate key update

解決沖突的方式:

  • load datafile replace、replace into,刪除表中的沖突記錄,插入新記錄。
  • insert ... on duplicate key update,用 update 后面的各字段值更新表中沖突記錄對應的字段。

如果 SQL 包含解決主鍵、唯一索引沖突的邏輯,會更新或刪除沖突記錄,所以需要加排他鎖(LOCK_X)。

對于示例 SQL,allow_duplicates = false,執行流程會進入 else_1 分支。

因為示例 SQL 不包含解決主鍵、唯一索引沖突的邏輯,不會更新、刪除沖突記錄,所以,只需要對沖突記錄加共享鎖(LOCK_S),加鎖的精確模式為 next-key 鎖(對應 else_2 分支)。

和變量 allow_duplicates 的含義不同,if (!is_next && !index->allow_duplicates) 中的 index->allow_duplicates 表示唯一索引是否允許存在重復記錄:

  • 對于 MySQL 內部臨時表的二級索引,index->allow_duplicates = true。
  • 對于其它表,index->allow_duplicates = false。

對于示例 SQL,if (!is_next && !index->allow_duplicates) 條件成立,調用 row_ins_dupl_error_with_rec() 得到返回值 true,說明新插入記錄和唯一索引中的原有記錄沖突。

執行流程進入 if (row_ins_dupl_error_with_rec(rec, entry, index, offsets)) 分支,設置變量 err 的值為 DB_DUPLICATE_KEY。

那么,問題來了:插入記錄到唯一索引時,發現插入目標位置已經有一條相同的記錄了,這不能說明新插入記錄和唯一索引中原有記錄沖突嗎?

還真不能,因為唯一索引有個特殊場景要處理,那就是 NULL 值。

InnoDB 認為 NULL 表示未知,NULL 和 NULL 也是不相等的,所以,唯一索引中可以包含多條字段值為 NULL 的記錄。

本文中,唯一索引都是指的二級索引。InnoDB 主鍵的字段值是不允許為 NULL 的。

舉個例子:對于測試表 t6,假設某條記錄的 i1 字段值為 NULL,新記錄的 i1 字段值也為 NULL,就可以插入成功,而不會報 Duplicate key 錯誤。

(3)回滾語句

row_ins_step() 執行結束之后,row_insert_for_mysql_using_ins_graph() 從 trx->error_state 中得到錯誤碼 DB_DUPLICATE_KEY,說明新插入記錄導致唯一索引沖突,調用 row_mysql_handle_errors() 處理沖突的善后邏輯,堆棧如下:

| > row_mysql_handle_errors(...) storage/innobase/row/row0mysql.cc:701
| + > // 插入記錄導致唯一索引沖突,需要回滾
| + > trx_rollback_to_savepoint(trx_t*, trx_savept_t*) storage/innobase/trx/trx0roll.cc:151
| + - > trx_rollback_to_savepoint_low(trx_t*, trx_savept_t*) storage/innobase/trx/trx0roll.cc:114
| + - x > que_run_threads(que_thr_t*) storage/innobase/que/que0que.cc:1001
| + - x = > que_run_threads_low(que_thr_t*) storage/innobase/que/que0que.cc:966
| + - x = | > que_thr_step(que_thr_t*) storage/innobase/que/que0que.cc:913
| + - x = | + > row_undo_step(que_thr_t*) storage/innobase/row/row0undo.cc:362
| + - x = | + - > row_undo(undo_node_t*, que_thr_t*) storage/innobase/row/row0undo.cc:296
| + - x = | + - x > row_undo_ins(undo_node_t*, que_thr_t*) storage/innobase/row/row0uins.cc:500
| + - x = | + - x = > row_undo_ins_remove_clust_rec(undo_node_t*) storage/innobase/row/row0uins.cc:118
| + - x = | + - x = | > row_convert_impl_to_expl_if_needed(btr_cur_t*, undo_node_t*) storage/innobase/row/row0undo.cc:338
| + - x = | + - x = | + > // 把主鍵索引記錄上的隱式鎖轉換為顯式鎖
| + - x = | + - x = | + > lock_rec_convert_impl_to_expl(...) storage/innobase/lock/lock0lock.cc:5544
| + - x = | + - x = | + - > lock_rec_convert_impl_to_expl_for_trx(...) storage/innobase/lock/lock0lock.cc:5496
| + - x = | + - x = | + - x > lock_rec_add_to_queue(...) storage/innobase/lock/lock0lock.cc:1613
| + - x = | + - x = | + - x = > lock_rec_other_has_expl_req(...) storage/innobase/lock/lock0lock.cc:900
| + - x = | + - x = | + - x = > // 創建鎖結構
| + - x = | + - x = | + - x = > RecLock::create(trx_t*, lock_prdt const*) storage/innobase/lock/lock0lock.cc:1356
| + - x = | + - x = | > // 先進行樂觀刪除,如果樂觀刪除失敗,后面會進行悲觀刪除
| + - x = | + - x = | > btr_cur_optimistic_delete(...) storage/innobase/include/btr0cur.h:466
| + - x = | + - x = | + > btr_cur_optimistic_delete_func(...) storage/innobase/btr/btr0cur.cc:4562
| + - x = | + - x = | + - > lock_update_delete(...) storage/innobase/lock/lock0lock.cc:3350
| + - x = | + - x = | + - x > // 剛剛插入的記錄,因為唯一索引沖突需要刪除,讓它的下一條記錄繼承 GAP 鎖
| + - x = | + - x = | + - x > lock_rec_inherit_to_gap(...) storage/innobase/lock/lock0lock.cc:2588
| + - x = | + - x = | + - x = > lock_rec_add_to_queue(...) storage/innobase/lock/lock0lock.cc:1681
| + - x = | + - x = | + - x = | > // 為被刪除的主鍵記錄的下一條記錄創建鎖結構
| + - x = | + - x = | + - x = | > RecLock::create(trx_t*, lock_prdt const*) storage/innobase/lock/lock0lock.cc:1356

row_mysql_handle_errors() 的核心邏輯是個 switch,根據不同的錯誤碼進行相應的處理。

// storage/innobase/row/row0mysql.cc
bool row_mysql_handle_errors(...)
{
  ...
  switch (err) {
    ...
    case DB_DUPLICATE_KEY:
    ...
      if (savept) {
        /* Roll back the latest, possibly incomplete insertion
        or update */

        trx_rollback_to_savepoint(trx, savept);
      }
      /* MySQL will roll back the latest SQL statement */
      break;
      ...
    }
    ...
}

對于錯誤碼 DB_DUPLICATE_KEY,row_mysql_handle_errors() 會調用 trx_rollback_to_savepoint() 回滾示例 SQL 對于主鍵索引所做的插入記錄操作。

savept 是調用 row_ins_step() 插入記錄到主鍵、唯一索引之前的保存點,trx_rollback_to_savepoint() 可以利用 savept 中的保存點,刪除 row_ins_step() 剛剛插入到主鍵索引中的記錄,讓主鍵索引回到 row_ins_step() 執行之前的狀態。

對于示例 SQL,trx_rollback_to_savepoint() 經過多級之后,調用 row_undo_ins_remove_clust_rec() 刪除已插入到主鍵索引的記錄。

// storage/innobase/row/row0uins.cc
[[nodiscard]] static dberr_t row_undo_ins_remove_clust_rec(
    undo_node_t *node) /*!< in: undo node */
{
  ...
  // 把新插入到主鍵索引中的記錄上的隱式鎖
  // 轉換為顯式鎖
  row_convert_impl_to_expl_if_needed(btr_cur, node);
  // 先進行樂觀刪除
  if (btr_cur_optimistic_delete(btr_cur, 0, &mtr)) {
    err = DB_SUCCESS;
    goto func_exit;
  }
  ...
  // 如果樂觀刪除失敗,再進行悲觀刪除
  btr_cur_pessimistic_delete(&err, false, btr_cur, 0, true, node->trx->id,
                             node->undo_no, node->rec_type, &mtr, &node->pcur,
                             nullptr);
}

刪除主鍵索引記錄之前,需要給它加鎖。因為插入操作包含隱式鎖的邏輯,所以這里的加鎖操作是把即將被刪除記錄上的隱式鎖轉換為顯式鎖。

當然,需要滿足一定的條件,row_convert_impl_to_expl_if_needed() 才會把主鍵索引中即將被刪除記錄上的隱式鎖轉換為顯式鎖。

// storage/innobase/row/row0undo.cc
void row_convert_impl_to_expl_if_needed(btr_cur_t *cursor, undo_node_t *node) {
  ...
  // 滿足以下 3 種條件之一,不需要把隱式鎖轉換為顯式鎖:
  // 1. !node->partial = true,即 node->partial = false
  //    表示整個事務回滾
  // 2. node->trx == nullptr
  // 3. node->trx->isolation_level < trx_t::REPEATABLE_READ
  //    事務隔離級別為:讀未提交(RU)、讀已提交(RC)
  if (!node->partial || (node->trx == nullptr) ||
      node->trx->isolation_level < trx_t::REPEATABLE_READ) {
    return;
  }
  ...
  // 滿足以下 4 種條件,需要把隱式鎖轉換顯式鎖:
  // 1. heap_no 對應的記錄不是 supremum
  // 2. 當前索引不是空間索引
  // 3. 不是用戶臨時表
  // 4. 不是 MySQL 內部臨時表
  if (/* 1 */ heap_no != PAGE_HEAP_NO_SUPREMUM && 
      /* 2 */ !dict_index_is_spatial(index) &&
      /* 3 */ !index->table->is_temporary() && 
      /* 4 */ !index->table->is_intrinsic()) {
    lock_rec_convert_impl_to_expl(block, rec, index,
                                  Rec_offsets().compute(rec, index));
  }
}

對于示例 SQL,第 1 個 if 條件不成立,所以不會執行 return,而是會繼續判斷第 2 個 if 條件。

第 2 個 if 條件成立,執行流程進入 if 分支,調用 lock_rec_convert_impl_to_expl() 把隱式鎖轉換為顯式鎖。

執行流程回到 row_undo_ins_remove_clust_rec(),調用 row_convert_impl_to_expl_if_needed() 把主鍵索引中即將被刪除記錄上的隱式鎖轉換為顯式鎖之后,接下就是刪除記錄了。

先調用 btr_cur_optimistic_delete() 進行樂觀刪除。

樂觀刪除指的是刪除數據頁中的記錄之后,不會因為數據頁中的記錄數量過少而觸發相鄰的數據頁合并。

如果樂觀刪除成功,直接返回 DB_SUCCESS。

如果樂觀刪除失敗,再調用 btr_cur_pessimistic_delete() 進行悲觀刪除。

悲觀刪除指的是刪除數據頁中的記錄之后,因為數據頁中的記錄數量過少,會觸相鄰的數據頁合并。

(4)主鍵索引記錄的隱式鎖轉換

上一小節中,我們沒有深入介紹主鍵索引中即將被刪除記錄上的隱式鎖轉換為顯式鎖的邏輯,接下來,我們來看看這個邏輯。

// storage/innobase/lock/lock0lock.cc
void lock_rec_convert_impl_to_expl(...) {
  trx_t *trx;
  ...
  // 主鍵索引
  if (index->is_clustered()) {
    trx_id_t trx_id;
    // 獲取 rec 記錄中 DB_TRX_ID 字段的值
    // 拿到插入 rec 記錄的事務 ID
    trx_id = lock_clust_rec_some_has_impl(rec, index, offsets);
    // 判斷事務是否處于活躍狀態
    // 如果事務是活躍狀態,返回事務的 trx_t 對象
    // 如果事務已提交,返回 nullptr
    trx = trx_rw_is_active(trx_id, true);
  } else { // 二級索引
    ...
  }

  if (trx != nullptr) {
    ulint heap_no = page_rec_get_heap_no(rec);
    ...
    // 如果事務是活躍狀態
    // 把 rec 記錄上的隱式鎖轉換為顯式鎖
    lock_rec_convert_impl_to_expl_for_trx(block, rec, index, offsets, trx,
                                          heap_no);
  }
}

InnoDB 主鍵索引的記錄中,都有一個隱藏字段 DB_TRX_ID。

lock_rec_convert_impl_to_expl() 先調用 lock_clust_rec_some_has_impl() 讀取主鍵索引中即將被刪除記錄的 DB_TRX_ID 字段。

然后調用 trx_rw_is_active() 判斷 DB_TRX_ID 對應的事務是否處于活躍狀態(事務未提交)。

如果事務處于活躍狀態,調用 lock_rec_convert_impl_to_expl_for_trx() 把 rec 記錄上的隱式鎖轉換為顯式鎖。

// storage/innobase/lock/lock0lock.cc
static void lock_rec_convert_impl_to_expl_for_trx(...)
{
  ...
  {
    locksys::Shard_latch_guard guard{UT_LOCATION_HERE, block->get_page_id()};
    ...
    trx_mutex_enter(trx);
    ...
    // 判斷事務的狀態不是 TRX_STATE_COMMITTED_IN_MEMORY
    if (!trx_state_eq(trx, TRX_STATE_COMMITTED_IN_MEMORY) &&
        // heap_no 對應記錄上沒有顯式的排他鎖
        !lock_rec_has_expl(LOCK_X | LOCK_REC_NOT_GAP, block, heap_no, trx)) {
      ulint type_mode;
      // 加鎖粒度:記錄(LOCK_REC)
      // 加鎖模式:寫鎖(LOCK_X)
      // 加鎖的精確模式:記錄(LOCK_REC_NOT_GAP)
      type_mode = (LOCK_REC | LOCK_X | LOCK_REC_NOT_GAP);
      lock_rec_add_to_queue(type_mode, block, heap_no, index, trx, true);
    }
    trx_mutex_exit(trx);
  }
  trx_release_reference(trx);
  ...
}

lock_rec_convert_impl_to_expl_for_trx() 也不會照單全收,它還會進一步判斷:

  • 事務狀態不是 TRX_STATE_COMMITTED_IN_MEMORY,因為處于這個狀態的事務就算是已經提交成功了,已提交成功的事務修改的記錄不包含隱藏式鎖邏輯,也就不需要把隱式鎖轉換為顯式鎖了。
  • 記錄上沒有顯式的排他鎖。

滿足上面 2 個條件之后,才會調用 lock_rec_add_to_queue() 創建鎖對象(RecLock)并加入到全局鎖對象的 hash 表中,這就最終完成了把主鍵索引中即將被刪除記錄上的隱式鎖轉換為顯式鎖。

(5)主鍵索引記錄的鎖轉移

主鍵索引中即將被刪除記錄上的顯式鎖,只是個過渡,它是用來為鎖轉移做準備的。

不管是樂觀刪除,還是悲觀刪除,刪除剛插入到主鍵索引的記錄之前,需要把該記錄上的鎖轉移到它的下一條記錄上,轉移操作由 lock_update_delete() 完成。

// storage/innobase/lock/lock0lock.cc
void lock_update_delete(const buf_block_t *block, const rec_t *rec) {
  ...
  if (page_is_comp(page)) {
    // 獲取即將被刪除的記錄的編號
    heap_no = rec_get_heap_no_new(rec);
    // 獲取即將被刪除記錄的下一條記錄的編號
    next_heap_no = rec_get_heap_no_new(page + rec_get_next_offs(rec, true));
  } else {
    ...
  }
  ...
  /* Let the next record inherit the locks from rec, in gap mode */
  // 把即將被刪除記錄上的鎖轉移到它的下一條記錄上
  lock_rec_inherit_to_gap(block, block, next_heap_no, heap_no);
  ...
}

lock_update_delete() 調用 rec_get_heap_no_new() 獲取即將被刪除記錄的下一條記錄的編號,然后調用 lock_rec_inherit_to_gap() 把即將被刪除記錄上的鎖轉移到它的下一條記錄上。

// storage/innobase/lock/lock0lock.cc
static void lock_rec_inherit_to_gap(...)
{
  lock_t *lock;
  ...
  // heap_no 是主鍵索引中即將被刪除的記錄編號
  for (lock = lock_rec_get_first(lock_sys->rec_hash, block, heap_no);
       lock != nullptr; lock = lock_rec_get_next(heap_no, lock)) {
    /* Skip inheriting lock if set */
    if (lock->trx->skip_lock_inheritance) {
      continue;
    }

    if (!lock_rec_get_insert_intention(lock) &&
        !lock->index->table->skip_gap_locks() &&
        (!lock->trx->skip_gap_locks() || lock->trx->lock.inherit_all.load())) {
      lock_rec_add_to_queue(LOCK_REC | LOCK_GAP | lock_get_mode(lock),
                            heir_block, heir_heap_no, lock->index, lock->trx);
    }
  }
}

for 循環中,lock_rec_get_first() 獲取主鍵索引中即將被刪除記錄上的鎖。

能否獲取到鎖,取決于前面的 row_convert_impl_to_expl_if_needed() 是否已經把記錄上的隱式鎖轉換為顯式鎖。

row_convert_impl_to_expl_if_needed() 會對多個條件進行判斷,以決定是否把記錄上的隱式鎖轉換為顯式鎖。其中,比較重要的判斷條件是事務隔離級別:

  • 如果事務隔離級別是 READ-COMMITTED,隱式鎖不轉換為顯式鎖。
  • 如果事務隔離級別是 REPEATABLE-READ,再結合其它判斷條件,決定是否把隱式鎖轉換為顯式鎖。

我們以測試表和示例 SQL 為例,來看看 lock_rec_inherit_to_gap() 的執行流程。

示例 SQL 執行于 REPEATABLE-READ 隔離級別之下,并且滿足其它判斷條件,row_convert_impl_to_expl_if_needed() 會把記錄上的隱式鎖轉換為顯式鎖。

所以,lock_rec_get_first() 會獲取到主鍵索引中即將被刪除記錄上的鎖,并且 for 循環中的第 2 個 if 條件成立,執行流程進入 if 分支。

對于示例 SQL,即將被刪除記錄的下一條記錄是 supremum,調用 lock_rec_add_to_queue() 把即將被刪除記錄上的鎖轉移到 supremum 記錄上。

接下來,介紹 lock_rec_add_to_queue() 代碼之前,我們先看一下傳給該方法的第 1 個參數的值。

lock_get_mode() 會返回即將被刪除記錄上的鎖:LOCK_REC_NOT_GAP | LOCK_REC | LOCK_X。

第 1 個參數的值為:LOCK_REC | LOCK_GAP | lock_get_mode(lock)。

把 lock_get_mode() 的返回值代入其中,得到:LOCK_REC | LOCK_GAP | LOCK_REC_NOT_GAP | LOCK_REC | LOCK_X。

去重之后,得到傳給 lock_rec_add_to_queue() 的第 1 個參數(type_mode)的值:LOCK_REC | LOCK_GAP | LOCK_REC_NOT_GAP | LOCK_X。

// storage/innobase/lock/lock0lock.cc
static void lock_rec_add_to_queue(ulint type_mode, ...) {
  ...
  // 對 supremum 偽記錄進行特殊處理
  if (heap_no == PAGE_HEAP_NO_SUPREMUM) {
    ...
    // 去掉 LOCK_GAP、LOCK_REC_NOT_GAP
    type_mode &= ~(LOCK_GAP | LOCK_REC_NOT_GAP);
  }
  ...
  // 實例化鎖對象
  RecLock rec_lock(index, block, heap_no, type_mode);
  ...
  // 把鎖對象加入全局鎖對象 hash 表
  rec_lock.create(trx);
  ...
}

type_mode 就是 lock_rec_inherit_to_gap() 函數中傳過來的第 1 個參數,它的值為:LOCK_REC | LOCK_GAP | LOCK_REC_NOT_GAP | LOCK_X。

對于示例 SQL,即將被刪除記錄的下一條記錄是 supremum,執行流程會命中 if (heap_no == PAGE_HEAP_NO_SUPREMUM) 分支,執行代碼:type_mode &= ~(LOCK_GAP | LOCK_REC_NOT_GAP)。

從 type_mode 中去掉 LOCK_GAP、LOCK_REC_NOT_GAP,得到 LOCK_REC | LOCK_X,表示給 supremum 加 next-key 排他鎖。

5、總結

REPEATABLE-READ 隔離級別下,如果插入一條記錄,導致唯一索引沖突,執行流程如下:

  • 插入記錄到主鍵索引,成功。
  • 插入記錄到唯一索引,沖突,插入失敗。
  • 給唯一索引中沖突的記錄加鎖。
    對于 load datafile replace、replace into、insert ... on duplicate key update 語句,加排他鎖(LOCK_X)。對于其它語句,加共享鎖(LOCK_S)。
  • 把主鍵索引中對應記錄上的隱式鎖轉換為顯式鎖 [Not RC]。
  • 把主鍵索引記錄上的顯式鎖轉移到它的下一條記錄上 [Not RC]。
  • 刪除主鍵索引記錄。

順便說一下,對于 READ-COMMITTED 隔離級別,大體流程相同,不同之處在于,它沒有上面流程中打了 [Not RC] 標記的兩個步驟。

對于示例 SQL,READ-COMMITTED 隔離級別下,不會給主鍵索引的 supremum 記錄加鎖,加鎖情況如下:

圖片

最后,把示例 SQL 在 REPEATABLE-READ 隔離級別下的加鎖情況放在這里,作個對比:

圖片

本文轉載自微信公眾號「一樹一溪」,可以通過以下二維碼關注。轉載本文請聯系一樹一溪公眾號。

責任編輯:姜華 來源: 一樹一溪
相關推薦

2022-04-29 11:39:28

MySQL幻讀Gap Lock

2024-09-04 08:44:18

MySQL核心模塊

2023-11-06 11:13:58

Bean占位符標記

2021-06-05 18:02:20

MySQL加鎖范圍

2021-06-08 09:41:26

MySQL加鎖范圍

2025-08-15 07:55:20

2024-06-12 14:03:31

MySQLInnoDB

2025-04-24 10:56:01

MySQLInnoDB數據庫鎖

2022-08-04 08:22:49

MySQL索引

2014-10-13 10:41:48

安防布線

2018-09-16 23:14:18

MySQL索引約束主鍵

2018-09-14 09:12:00

數據庫MySQL索引約束

2025-09-01 09:05:00

2023-02-10 10:14:59

普通索引唯一索引

2012-02-24 10:28:32

2024-10-16 11:11:51

隔離InnoDB死鎖

2022-09-15 07:54:59

awaitPromise

2024-01-11 08:12:20

重量級監視器

2025-11-14 00:00:15

2012-10-31 09:41:47

WAN優化SSLHTTPS
點贊
收藏

51CTO技術棧公眾號

午夜精品免费观看| 久久久久国产精品区片区无码| 在线视频91p| 狠狠色狠狠色综合日日91app| 欧美日韩成人精品| 中文字幕国产专区| 国产精区一区二区| 欧美视频在线视频| 精品国产无码在线| 天堂av电影在线观看| 看国产成人h片视频| 久久久女女女女999久久| a毛片毛片av永久免费| 精品三级国产| 色8久久人人97超碰香蕉987| 国产盗摄视频在线观看| 免费在线黄色网址| 国产成人精品影院| 国产精品精品视频| www日韩精品| 亚洲成人三区| 国产亚洲免费的视频看| 久久久久亚洲av无码网站| 亚洲成人看片| 亚洲国产欧美在线| 最新精品视频| 欧美日韩免费做爰大片| 国产成人欧美日韩在线电影| 青青在线视频一区二区三区 | 91在线国内视频| 国产在线98福利播放视频| 日韩精品久久久久久久酒店| 99久久婷婷国产综合精品电影√| 日韩av在线一区| 欧美一级大片免费看| **精品中文字幕一区二区三区| 欧美丝袜一区二区| 免费视频爱爱太爽了| 日韩理伦片在线| 国产亚洲欧美日韩日本| 国产午夜精品一区| www.激情五月| 国产精品亚洲视频| 国产主播欧美精品| 国产偷人爽久久久久久老妇app| 亚洲国产精品第一区二区| 欧美猛少妇色xxxxx| 美女网站视频色| 日本a口亚洲| 国产亚洲精品久久| 中文字幕第4页| 免费成人av| 国产一区二区激情| 免费一级黄色录像| 青青草97国产精品麻豆| 在线播放亚洲激情| 国产精品酒店视频| 欧美成人自拍| 久久偷看各类女兵18女厕嘘嘘| 日韩免费av一区| 国产国产精品| 欧美成人黄色小视频| 亚洲国产成人精品综合99| 欧美黄色一级视频| 美日韩精品视频免费看| 国产这里有精品| 欧美日韩国产高清| 97视频人免费观看| 亚洲免费在线视频观看| 日韩成人av影视| 国产欧美日韩中文字幕| 国产精品怡红院| 国产成人在线免费观看| 国产在线精品一区| 日韩av地址| 久久精品人人爽人人爽| 日韩免费av电影| 69av亚洲| 亚洲国产精品麻豆| 国产精品沙发午睡系列| 亚洲伦乱视频| 9191精品国产综合久久久久久| 天天久久综合网| 国产欧美啪啪| 亚洲精品有码在线| www.99re6| 影音先锋久久久| 国产精品成人播放| av av片在线看| 91在线云播放| 日韩国产一区久久| 自拍亚洲图区| 欧美性xxxx在线播放| 99re精彩视频| 久久久久影视| 色噜噜亚洲精品中文字幕| 欧美成人精品欧美一级| 亚洲欧美成人| 亚洲a在线观看| 四虎影院在线播放| 中文字幕一区二区三区在线播放 | 欧美日韩ab| 欧美在线视频免费观看| 国产一区二区在线视频聊天 | аⅴ天堂中文在线网| 亚洲最新av| 国产精品成人一区二区| 不卡av中文字幕| 欧美国产日韩一二三区| 无码 制服 丝袜 国产 另类| 性欧美freehd18| 亚洲国产精品久久久久| 2014亚洲天堂| 午夜在线精品| www.久久久| 激情成人四房播| 91福利视频在线| 亚洲香蕉中文网| 中文不卡在线| 国产精品视频999| 深夜福利免费在线观看| 亚洲精品国产成人久久av盗摄| 亚洲人成色77777| 日本欧美高清| 久久久久久久一| a视频免费在线观看| 国产欧美日韩综合精品一区二区| 免费一级特黄特色毛片久久看| 国产美女视频一区二区| 色悠悠久久久久| 国产日韩在线免费观看| 久久蜜桃一区二区| av动漫在线看| 日韩手机在线| 国模gogo一区二区大胆私拍 | 黄色小说在线观看视频| 国产中文字幕精品| 一区二区三区三区在线| 99欧美精品| 国产一区二区三区在线| 亚洲第一网站在线观看| 91在线国内视频| 欧美精品一区免费| 日韩电影不卡一区| 2021国产精品视频| 亚洲人在线观看视频| 亚洲v日本v欧美v久久精品| 精人妻一区二区三区| 欧美福利影院| 粉嫩高清一区二区三区精品视频 | 精品亚洲成人| 国产精品久久9| 91最新在线| 欧美精品日韩一本| 欧美肥妇bbwbbw| 国模大尺度一区二区三区| 影音先锋在线亚洲| 国产亚洲综合视频| 免费毛片一区二区三区久久久| caoporn免费在线| 欧美一三区三区四区免费在线看| 久久久久久久麻豆| 国产麻豆视频一区二区| 精品免费久久久久久久| 亚洲综合影院| 97碰碰碰免费色视频| 污视频网站免费观看| 一道本成人在线| 女人黄色一级片| 狠狠色狠狠色综合日日91app| www亚洲国产| 亚洲国产中文在线| 国产91成人在在线播放| 国产51人人成人人人人爽色哟哟| 精品视频在线免费看| 青青操在线视频观看| 国产精品1区二区.| 鲁一鲁一鲁一鲁一色| 狠狠做深爱婷婷综合一区| 国产精品99久久久久久白浆小说 | 国产精品自拍在线| 青青在线免费观看| 欧美日韩123| 91在线高清免费观看| 爱情岛论坛亚洲品质自拍视频网站| 精品亚洲一区二区三区四区五区| 波多野结衣电车痴汉| 亚洲精品亚洲人成人网| 欧美xxxxx精品| 日本v片在线高清不卡在线观看| 咪咪色在线视频| 牛牛影视久久网| 国产精品中文久久久久久久| 色呦呦在线播放| 伊人久久综合97精品| 亚洲欧美激情国产综合久久久| 日韩欧美亚洲国产一区| 日本黄色片免费观看| 99久久综合狠狠综合久久| 毛片毛片毛片毛片毛片毛片毛片毛片毛片 | 日本美女一区二区三区| 欧美日韩福利在线| 青青草91久久久久久久久| 国产日韩精品一区观看| 欧美视频在线视频精品| 97视频在线看| 成人在线观看亚洲| 亚洲欧洲在线观看| www.97av| 欧美二区在线观看| 亚洲视频 欧美视频| 亚洲一区在线播放| 极品色av影院| 国产清纯白嫩初高生在线观看91 | 色综合视频在线| 欧美巨大另类极品videosbest| 91精品国产乱码久久久张津瑜| 亚洲欧洲国产日本综合| 国产三级国产精品| 国产成人av电影在线观看| 91制片厂毛片| 久久午夜精品| 国产精品无码av在线播放| 亚洲一区二区三区无吗| 深夜福利成人| 中文字幕精品影院| 久久综合九色欧美狠狠| 都市激情久久| 国产富婆一区二区三区| 国产精品18| 91欧美精品成人综合在线观看| 写真福利精品福利在线观看| 午夜精品久久久久久久99热| 在线看女人毛片| 久久视频在线看| 九色porny丨首页在线| 中文字幕在线亚洲| a视频网址在线观看| 亚洲天堂av女优| 九色视频在线播放| 亚洲精品午夜精品| 桃花色综合影院| 亚洲欧美三级伦理| 免费观看成年在线视频网站| 亚洲激情视频在线播放| 少妇av在线播放| 亚洲精品www久久久| 少妇一区二区三区四区| 亚洲精品国产品国语在线| 天堂av手机版| 亚洲欧美日本精品| 国产对白叫床清晰在线播放| 正在播放亚洲1区| 欧美18hd| 欧美成人一区二区三区电影| 亚洲制服国产| 久久久久久久久久亚洲| av在线视屏| 欧美影院在线播放| 户外露出一区二区三区| 国产日本欧美视频| 国产精久久一区二区| 鬼打鬼之黄金道士1992林正英| 亚洲精品观看| 久久久水蜜桃| 色综合久久一区二区三区| 亚洲免费av网| 黑丝一区二区| 国内外免费激情视频| 免费xxxx性欧美18vr| 亚洲精品mv在线观看| 高清av一区二区| 日韩乱码人妻无码中文字幕久久| 久久九九99视频| 欧美激情精品久久久久久免费| 伊人色综合久久天天| 久草视频在线观| 欧美亚洲自拍偷拍| 性色av蜜臀av| 日韩精品亚洲视频| 日本不卡视频| 91精品国产91久久久久久久久| 欧美日韩123区| 亚洲一区二区久久久久久久| 精品自拍偷拍| 一本色道久久综合亚洲二区三区| 欧美日本一区二区视频在线观看 | 国产伦精品一区二区三区视频金莲| 国产精品www网站| 中文字幕一区二区三区中文字幕| 欧美午夜欧美| 午夜精品久久99蜜桃的功能介绍| 日韩在线综合网| 精品无码三级在线观看视频| 中文字幕第3页| 国产精品乱码一区二三区小蝌蚪| 强乱中文字幕av一区乱码| 色综合一区二区| www.久久成人| 日韩在线小视频| 九色porny丨首页入口在线| 成人免费直播live| 精品在线99| 欧美一级视频免费看| 捆绑变态av一区二区三区| 无套内谢大学处破女www小说| 中文字幕一区二区三区色视频| 久久黄色精品视频| 日韩一区二区三区电影在线观看| 韩国福利在线| 午夜精品蜜臀一区二区三区免费| 欧美成人家庭影院| 欧美激情导航| 亚洲大胆在线| 亚洲AV无码久久精品国产一区| 国产日韩精品视频一区| 久久综合激情网| 91精品国产欧美一区二区18| 国产福利片在线| 69影院欧美专区视频| 日韩欧美激情电影| 中文字幕在线亚洲精品| 日本成人在线视频网站| 国产吞精囗交久久久| 亚洲国产视频在线| 亚洲av永久无码国产精品久久| 神马久久桃色视频| 日韩精品三区| 欧洲av一区| 先锋亚洲精品| 成人精品在线观看视频| 亚洲成人在线网站| 好男人在线视频www| 欧美日本精品在线| 亚洲91网站| 日韩视频 中文字幕| 国产美女av一区二区三区| 国产精品嫩草影院俄罗斯| 欧美精品在线观看播放| 91xxx在线观看| 国产在线观看精品| 天天av综合| 爱情岛论坛亚洲自拍| 亚洲男同性视频| 性一交一乱一乱一视频| 欧美老女人性视频| 成人av综合网| 日日摸日日碰夜夜爽无码| av电影在线观看一区| 国产成人无码精品久在线观看| 亚洲精品720p| 欧美特大特白屁股xxxx| 日韩欧美亚洲区| 奇米精品一区二区三区四区| 国产一区在线观看免费| 欧美日韩第一区日日骚| 老司机精品视频在线观看6| 91精品国产综合久久香蕉922| 婷婷综合五月| 苍井空张开腿实干12次| 亚洲高清免费观看| 青青操在线视频| 国产精品嫩草视频| 亚洲色图欧美| 伦理片一区二区| 欧美午夜精品久久久久久人妖 | 久久久久99精品久久久久| 电影中文字幕一区二区| 日韩精品一区二区在线视频 | 制服丝袜亚洲播放| 免费网站在线观看人| 国产伦精品一区二区三区照片91 | 亚洲成人高清在线| 色就是色亚洲色图| 国产精品欧美一区二区| 羞羞答答成人影院www| 图片区偷拍区小说区| 色综合久久久久久久久| 黄色网页在线播放| 国产91视觉| 日韩一区欧美二区| 女性裸体视频网站| 精品国产一区二区三区忘忧草| 中文日产幕无线码一区二区| 亚洲精品视频一二三| 盗摄精品av一区二区三区| 成人午夜淫片100集| 精品国产一区二区三区久久| 第四色在线一区二区| 国内自拍视频一区| 一区二区三区.www| 国产在线观看黄| 999在线免费观看视频| 视频在线观看一区| 激情视频在线播放| 亚洲欧美制服另类日韩| 久久国产精品美女| 久草综合在线观看| 亚洲在线观看免费| 91美女视频在线|