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

MySQL 自適應(yīng)哈希索引—構(gòu)造

數(shù)據(jù)庫(kù) MySQL
AHI 構(gòu)造流程的前三步都是在判斷是否滿(mǎn)足某些條件,這些條件的范圍從大到小。先是索引級(jí)別,判斷索引被命中的次數(shù)。然后,是索引級(jí)別的構(gòu)造信息計(jì)數(shù)。

曾經(jīng)優(yōu)化慢查詢(xún)時(shí),經(jīng)常在日志中看到 truncate,當(dāng)時(shí)一直疑惑 truncate 為什么會(huì)慢。

轉(zhuǎn)到數(shù)據(jù)庫(kù)方向之后,又碰到過(guò)幾次 truncate 執(zhí)行時(shí)間過(guò)長(zhǎng),導(dǎo)致 MySQL 短暫卡住的問(wèn)題。

經(jīng)過(guò)源碼分析和同事測(cè)試驗(yàn)證,發(fā)現(xiàn)這幾次的問(wèn)題都跟自適應(yīng)哈希索引有關(guān),所以,深入研究下自適應(yīng)哈希索引就很有必要了。

自適應(yīng)哈希索引大概會(huì)有 3 篇文章。第 1 篇,我們來(lái)看看自適應(yīng)哈希索引的構(gòu)造過(guò)程。

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

1、準(zhǔn)備工作

創(chuàng)建測(cè)試表:

CREATE TABLE `t1` (
  `id` int unsigned NOT NULL AUTO_INCREMENT,
  `i1` int DEFAULT '0',
  PRIMARY KEY (`id`) USING BTREE,
  KEY `idx_i1` (`i1`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3

插入測(cè)試數(shù)據(jù):

INSERT INTO `t1`(`id`, `i1`)
VALUES (10, 101), (20, 201), (30, 301);

示例 SQL:

SELECT * FROM `t1`
WHERE `i1` >= 101 AND `i1` < 301

2、AHI 有什么好處?

自適應(yīng)哈希索引,英文全稱(chēng) Adaptive Hash Index,簡(jiǎn)稱(chēng) AHI。

根據(jù)上下文需要,后續(xù)內(nèi)容會(huì)混用自適應(yīng)哈希索引和 AHI,不再單獨(dú)說(shuō)明。

顧名思義,自適應(yīng)哈希索引也是一種索引,使用哈希表作為存儲(chǔ)結(jié)構(gòu),自適應(yīng)的意思是我們無(wú)法干涉它的創(chuàng)建、使用、更新、刪除,而是由 InnoDB 自行決定什么時(shí)候創(chuàng)建、怎么創(chuàng)建、什么時(shí)候更新和刪除。

我們知道 InnoDB 的主鍵索引、二級(jí)索引都是 B+ 樹(shù)結(jié)構(gòu)。執(zhí)行 SQL 語(yǔ)句時(shí),以下幾種場(chǎng)景都需要定位到索引葉子結(jié)點(diǎn)中的某條記錄:

  • insert 需要定位到插入目標(biāo)位置的前一條記錄。
  • 查詢(xún)優(yōu)化階段,select、update、delete 預(yù)估掃描區(qū)間記錄數(shù)量時(shí),需要定位到掃描區(qū)間的第一條最后一條記錄。
  • 查詢(xún)執(zhí)行階段,select、update、delete 需要定位到掃描區(qū)間的第一條記錄。

得益于多叉結(jié)構(gòu),B+ 樹(shù)的層級(jí)一般都比較少,通常情況下,從根結(jié)點(diǎn)開(kāi)始,最多經(jīng)過(guò) 3 ~ 4 層就能定位到葉子結(jié)點(diǎn)中的記錄。

這個(gè)定位過(guò)程看起來(lái)挺快的,單次執(zhí)行也確實(shí)比較快,但是對(duì)于頻繁執(zhí)行的場(chǎng)景,還是有一點(diǎn)優(yōu)化空間的。

為了追求極致性能,InnoDB 實(shí)現(xiàn)了 AHI,目的就是能夠根據(jù)搜索條件直接定位到索引葉子結(jié)點(diǎn)中的記錄。

圖片圖片

上圖是一棵高為 4 的 B+ 樹(shù)示意圖,定位索引葉子結(jié)點(diǎn)記錄,需要從根結(jié)點(diǎn)開(kāi)始,經(jīng)過(guò)內(nèi)結(jié)點(diǎn)、葉結(jié)點(diǎn),最后定位到記錄,路徑為:① -> ② -> ③ -> ④

圖片圖片

AHI 會(huì)使用多個(gè) hash 桶來(lái)存儲(chǔ)哈希值到記錄地址的映射,上圖是一個(gè) hash 桶的示意圖。

橙色塊表示記錄地址組成的鏈表,因?yàn)槎鄺l記錄可能被映射到 hash 桶中的同一個(gè)位置。

AHI 定位一條記錄時(shí),根據(jù) where 條件中的某些字段計(jì)算出哈希值,然后直接到 hash 桶中找到對(duì)應(yīng)的記錄。

如果多條記錄被映射到 hash 桶中的同一個(gè)位置,那么找到的是個(gè)鏈表,需要遍歷這個(gè)鏈表以找到目標(biāo)記錄。

3、原理介紹

AHI 能提升定位記錄的效率,但是,有得到必然有付出,天上掉餡餅的事在計(jì)算機(jī)世界里是不會(huì)發(fā)生的。

為了簡(jiǎn)潔,這里我們把定位索引葉子結(jié)點(diǎn)中的記錄簡(jiǎn)稱(chēng)為定位記錄,后面內(nèi)容也依此定義,不再單獨(dú)說(shuō)明。

高效定位記錄,付出的代價(jià)是存儲(chǔ)哈希值到記錄地址的映射占用的內(nèi)存空間、以及構(gòu)造映射花費(fèi)的時(shí)間。

因?yàn)橛袝r(shí)間、空間成本,所以,InnoDB 希望構(gòu)造 AHI 之后,能夠盡可能多的用上,做到收益大于成本。直白點(diǎn)說(shuō),就是需要滿(mǎn)足一定的條件,才能構(gòu)造 AHI。

AHI 采用的是組團(tuán)構(gòu)造邏輯,也就是以數(shù)據(jù)頁(yè)為單位,滿(mǎn)足一定條件之后,就會(huì)為數(shù)據(jù)頁(yè)中的所有記錄構(gòu)造 AHI,主要流程如下:

圖片圖片

第 1 步,為數(shù)據(jù)頁(yè)所屬的索引計(jì)數(shù)(index->search_info->hash_analysis)。

SQL 執(zhí)行過(guò)程中,每次定位某個(gè)索引葉子結(jié)點(diǎn)中的記錄,該索引的計(jì)數(shù)都會(huì)加 1。

如果索引計(jì)數(shù)達(dá)到 17,進(jìn)入第 2 步,否則,執(zhí)行流程到此結(jié)束。

因?yàn)榈?2、3 步為構(gòu)造信息計(jì)數(shù)、為數(shù)據(jù)頁(yè)計(jì)數(shù)也是需要時(shí)間成本的,所以,這里設(shè)置了第 1 道檻,只有索引被使用一定次數(shù)之后,才會(huì)執(zhí)行第 2、3 步。

第 2 步,為構(gòu)造信息計(jì)數(shù)(index->search_info->n_hash_potential)。

對(duì)于 select、insert、update、delete,定位記錄時(shí),搜索條件和葉子結(jié)點(diǎn)中的記錄比較,會(huì)產(chǎn)生兩個(gè)邊界,左邊為下界,右邊為上界,基于下界和上界可以得到用于構(gòu)造 AHI 的信息,我們稱(chēng)之為構(gòu)造信息。

圖片圖片

以上是定位記錄時(shí)產(chǎn)生的下界、上界示意圖。定位記錄過(guò)程中,下界和上界會(huì)不斷向目標(biāo)記錄靠近,最終,下界或上界的其中一個(gè)會(huì)指向目標(biāo)記錄。

如果某次定位記錄時(shí),基于下界或上界得到的構(gòu)造信息,和索引對(duì)象中保存的構(gòu)造信息一致,該構(gòu)造信息計(jì)數(shù)加 1。否則,該索引計(jì)數(shù)清零,構(gòu)造信息計(jì)數(shù)清零或重置為 1(具體見(jiàn)下一小節(jié)的介紹)。

第 3 步,為數(shù)據(jù)頁(yè)計(jì)數(shù)(block->n_hash_helps)。

定位到索引葉子結(jié)點(diǎn)記錄之后,就知道了該記錄所屬的數(shù)據(jù)頁(yè),如果本次得到的構(gòu)造信息和數(shù)據(jù)頁(yè)對(duì)象中保存的構(gòu)造信息相同,數(shù)據(jù)頁(yè)計(jì)數(shù)加 1,否則數(shù)據(jù)頁(yè)計(jì)數(shù)重置為 1。

第 4 步,為數(shù)據(jù)頁(yè)構(gòu)造 AHI。

如果滿(mǎn)足以下兩個(gè)條件,第 3 步的數(shù)據(jù)頁(yè)就具備了構(gòu)造 AHI 的資格:

  • 構(gòu)造信息計(jì)數(shù)大于等于 100。
  • 數(shù)據(jù)頁(yè)計(jì)數(shù)大于數(shù)據(jù)頁(yè)中記錄數(shù)量的十六分之一。

具備構(gòu)造 AHI 的資格之后,對(duì)于以下三種情況之一,會(huì)為數(shù)據(jù)頁(yè)構(gòu)造 AHI:

  • 該數(shù)據(jù)頁(yè)之前沒(méi)有構(gòu)造過(guò) AHI。
  • 該數(shù)據(jù)頁(yè)之前構(gòu)造過(guò) AHI,并且構(gòu)造 AHI 之后,數(shù)據(jù)頁(yè)會(huì)從零開(kāi)始重新計(jì)數(shù),重新計(jì)數(shù)大于數(shù)據(jù)頁(yè)中記錄數(shù)量的兩倍。
  • 該數(shù)據(jù)頁(yè)之前構(gòu)造過(guò) AHI,但是本次確定的構(gòu)造信息和之前不一樣了。

注意:從各個(gè)條件判斷,到最終構(gòu)造 AHI 的整個(gè)流程,并不是在執(zhí)行一條 SQL 的過(guò)程中完成的,而是在執(zhí)行多條 SQL 的過(guò)程中完成的。

到這里,構(gòu)造 AHI 的主要流程就介紹完了,構(gòu)造過(guò)程的具體細(xì)節(jié),請(qǐng)繼續(xù)往下看。

4、構(gòu)造過(guò)程

定位記錄會(huì)調(diào)用 btr_cur_search_to_nth_level(),這也是 AHI 的構(gòu)造入口:

// storage/innobase/btr/btr0cur.cc
void btr_cur_search_to_nth_level(...) {
  ...
  if (btr_search_enabled && 
      !index->disable_ahi) {
    // AHI 的構(gòu)造入口
    btr_search_info_update(cursor);
  }
  ...
}

btr_search_enabled = true(默認(rèn)值),表示 InnoDB 啟用了 AHI。

!index->disable_ahi = true,即 index->disable_ahi = false,表示 index 對(duì)應(yīng)的索引沒(méi)有禁用 AHI。

只有內(nèi)部臨時(shí)表、沒(méi)有主鍵的表禁用了 AHI。

也就是說(shuō),對(duì)于正常的用戶(hù)表、系統(tǒng)表,!index->disable_ahi = true,會(huì)調(diào)用 btr_search_info_update(),進(jìn)入 AHI 的構(gòu)造流程。

(1)索引計(jì)數(shù)

構(gòu)造 AHI 的第 1 步,就是調(diào)用 btr_search_info_update() 進(jìn)行索引計(jì)數(shù)。

// storage/innobase/include/btr0sea.ic
static inline void btr_search_info_update(btr_cur_t *cursor) {
  const auto index = cursor->index;
  ...
  // 索引計(jì)數(shù)加 1
  const auto hash_analysis_value = ++index->search_info->hash_analysis;
  // BTR_SEARCH_HASH_ANALYSIS = 17(硬編碼)
  if (hash_analysis_value < BTR_SEARCH_HASH_ANALYSIS) {
    /* Do nothing */
    return;
  }
  ...
  btr_search_info_update_slow(cursor);
}

btr_search_info_update() 每次被調(diào)用都會(huì)增加索引計(jì)數(shù)(++index->search_info->hash_analysis)。

自增之后,如果索引計(jì)數(shù)小于 17,不需要進(jìn)入 AHI 構(gòu)造流程的下一步,直接返回。

如果索引計(jì)數(shù)大于等于 17,調(diào)用 btr_search_info_update_slow(),進(jìn)入 AHI 構(gòu)造流程的下一步。

看到這里,大家是否會(huì)有疑問(wèn):對(duì)于一條能使用索引的 select 語(yǔ)句,如果 where 條件只有一個(gè)掃描區(qū)間,執(zhí)行過(guò)程中,btr_search_info_update() 最多會(huì)被調(diào)用幾次?

我們通過(guò) 1. 準(zhǔn)備工作小節(jié)的示例 SQL 來(lái)揭曉答案,SQL 如下:

SELECT * FROM `t1`
WHERE `i1` >= 101 AND `i1` < 301

執(zhí)行計(jì)劃如下:

圖片圖片

通過(guò)執(zhí)行計(jì)劃可知,示例 SQL 執(zhí)行過(guò)程中,會(huì)使用索引 idx_i1。

查詢(xún)優(yōu)化階段,MySQL 需要定位到掃描區(qū)間的第一條和最后一條記錄,用于計(jì)算掃描區(qū)間覆蓋的記錄數(shù)量:

  • 定位掃描區(qū)間的第一條記錄,即滿(mǎn)足 id >= 101 的第一條記錄,第 1 次調(diào)用 btr_cur_search_to_nth_level()。
  • 定位掃描掃描區(qū)間的最后一條記錄,即滿(mǎn)足 id < 301 的最后一條記錄,第 2 次調(diào)用 btr_cur_search_to_nth_level()。

查詢(xún)執(zhí)行階段,從存儲(chǔ)引擎讀取第一條記錄之前,需要定位掃描區(qū)間的第一條記錄,即滿(mǎn)足 id >= 101 的第一條記錄,第 3 次調(diào)用 btr_cur_search_to_nth_level()。

定位掃描區(qū)間的第一條和最后一條記錄,都是定位索引葉子結(jié)點(diǎn)中的記錄。

到這里,我們就得到了前面那個(gè)問(wèn)題的答案:3 次。

(2)構(gòu)造信息計(jì)數(shù)

如果某個(gè)索引的計(jì)數(shù)達(dá)到了 17,就會(huì)進(jìn)入 AHI 構(gòu)造流程的第 2 步,根據(jù)本次定位記錄過(guò)程中得到的下界和上界,確定使用索引的前幾個(gè)字段構(gòu)造 AHI,以及對(duì)于索引中前幾個(gè)字段值相同的一組記錄,構(gòu)造 AHI 時(shí)選擇這組記錄的第一條還是最后一條。

3.原理介紹小節(jié)的第 2 步,我們已經(jīng)把這些信息命名為構(gòu)造信息。

基于定位記錄時(shí)得到的下界和上界確定構(gòu)造信息、為構(gòu)造信息計(jì)數(shù)的邏輯由 btr_search_info_update_hash() 完成。

// storage/innobase/btr/btr0sea.cc
void btr_search_info_update_slow(btr_cur_t *cursor) {
  ...
  const auto block = btr_cur_get_block(cursor);
  ...
  btr_search_info_update_hash(cursor);
  ...
}

btr_search_info_update_hash() 的代碼有點(diǎn)長(zhǎng),我們分 2 段介紹。

介紹第 1 段代碼之前,我們先來(lái)看看表示構(gòu)造信息的結(jié)構(gòu)體定義(index->search_info->prefix_info):

// storage/innobase/include/buf0buf.h
// 為了方便閱讀,以下結(jié)構(gòu)體定義對(duì)源碼做了刪減
struct btr_search_prefix_info_t {
  /** recommended prefix: number of bytes in an incomplete field */
  uint32_t n_bytes;
  /** recommended prefix length for hash search: number of full fields */
  uint16_t n_fields;
  /** true or false, depending on whether the leftmost record of several records
  with the same prefix should be indexed in the hash index */
  bool left_side;
}

btr_search_prefix_info_t 包含 3 個(gè)屬性:

  • n_fields、n_bytes:索引的前 n_fields 個(gè)字段,第 n_fields + 1 個(gè)字段的前 n_bytes 字節(jié)用于構(gòu)造 AHI。
  • left_side:如果索引中多條記錄的前 n_fields 個(gè)字段內(nèi)容、第 n_fields + 1 個(gè)字段前 n_bytes 字節(jié)的內(nèi)容相同,我們把這樣的一組記錄稱(chēng)為前綴相同的記錄。對(duì)于前綴相同的記錄:left_side = true 時(shí),選擇最左邊的記錄(第一條記錄)構(gòu)造 AHI。left_side = false 時(shí),選擇最右邊的記錄(最后一條記錄)構(gòu)造 AHI。

接下來(lái),我們開(kāi)始介紹 btr_search_info_update_hash() 的第 1 段代碼邏輯。

// storage/innobase/btr/btr0sea.cc
/****** 第 1 段 ******/
static void btr_search_info_update_hash(btr_cur_t *cursor) {
  dict_index_t *index = cursor->index;
  int cmp;
  ...
  // 索引中,通過(guò)幾個(gè)字段能唯一確定一條記錄?
  // 對(duì)于主鍵索引,n_unique = 主鍵段字?jǐn)?shù)量
  // 對(duì)于二級(jí)索引,n_unique = 二級(jí)索引字段數(shù)量 + 主鍵字段數(shù)量
  const uint16_t n_unique =
      static_cast<uint16_t>(dict_index_get_n_unique_in_tree(index));
  const auto info = index->search_info;
  
  /****** if_1 ******/
  // 構(gòu)造信息計(jì)數(shù)不等于 0
  // 說(shuō)明之前已經(jīng)確定過(guò)構(gòu)造信息了
  if (info->n_hash_potential != 0) {
    // info->prefix_info 中
    // 保存了之前確定的構(gòu)造信息
    const auto prefix_info = info->prefix_info.load();
    ...

    /****** if_2 ******/
    // prefix_info.n_fields
    //   表示之前確定的構(gòu)造信息的字段數(shù)量
    // std::max(up_match, low_match)
    //   表示下界或上界字段數(shù)量中較大的那個(gè)
    if (prefix_info.n_fields == n_unique &&
        std::max(cursor->up_match, cursor->low_match) == n_unique) {
      // 兩個(gè)條件都滿(mǎn)足,說(shuō)明:
      //   如果本次通過(guò)下界、上界確定構(gòu)造信息
      //   會(huì)和之前確定的構(gòu)造信息相同
      //   那么,構(gòu)造信息計(jì)數(shù)加 1
      info->n_hash_potential++;

      return;
    }
    ...
    const bool low_matches_prefix =
        0 >= ut_pair_cmp(prefix_info.n_fields, prefix_info.n_bytes,
                         cursor->low_match, cursor->low_bytes);
    const bool up_matches_prefix =
        0 >= ut_pair_cmp(prefix_info.n_fields, prefix_info.n_bytes,
                         cursor->up_match, cursor->up_bytes);
    /****** if_3 ******/
    // 這里的構(gòu)造信息指的是:
    //   索引對(duì)象中保存的之前確定的構(gòu)造信息
    // prefix_info.left_side = true
    //   如果構(gòu)造信息【大于】下界,且【小于等于】上界
    //   構(gòu)造信息計(jì)數(shù)(n_hash_potential)加 1
    // prefix_info.left_side = false
    //   如果構(gòu)造信息【小于等于】下界,且【大于】上界
    //   構(gòu)造信息計(jì)數(shù)(n_hash_potential)加 1
    if (prefix_info.left_side ? (!low_matches_prefix && up_matches_prefix)
                              : (low_matches_prefix && !up_matches_prefix)) {
      info->n_hash_potential++;
      return;
    }
  }
  ...
}

如果構(gòu)造信息計(jì)數(shù)(info->n_hash_potential)不等于 0,if_1 條件成立,說(shuō)明索引對(duì)象中已經(jīng)保存了之前確定的構(gòu)造信息。

但是,確定索引的 AHI 構(gòu)造信息之后,還需要該索引的構(gòu)造信息計(jì)數(shù)、某個(gè)數(shù)據(jù)頁(yè)的計(jì)數(shù)滿(mǎn)足條件,InnoDB 才會(huì)為該索引的該數(shù)據(jù)頁(yè)構(gòu)造 AHI。

所以,索引對(duì)象中已經(jīng)保存了之前確定的構(gòu)造信息對(duì)應(yīng)兩種情況:

  • 已經(jīng)用索引中保存的構(gòu)造信息為某個(gè)(些)數(shù)據(jù)頁(yè)構(gòu)造了 AHI。
  • 只確定了構(gòu)造信息,還沒(méi)有用它構(gòu)造過(guò) AHI。

構(gòu)造信息計(jì)數(shù)被命名為 n_hash_potential(潛在的),就是因?yàn)榇嬖诘?2 種情況。

if_2、if_3 用于判斷:通過(guò)本次定位記錄時(shí)產(chǎn)生的下界或上界得到構(gòu)造信息,是否和索引對(duì)象中保存的構(gòu)造信息一致,如果一致,則增加構(gòu)造信息計(jì)數(shù)。

if_2 包含兩個(gè)表達(dá)式,如果值都為 true,說(shuō)明上面的判斷結(jié)果為 true,構(gòu)造信息計(jì)數(shù)加 1(info->n_hash_potential++)。

如果 if_2 不成立,再判斷 if_3 是否成立。

前面介紹過(guò),對(duì)于前綴相同的一組記錄,構(gòu)造 AHI 時(shí),由 left_side 決定選擇最左邊還是最右邊的記錄。

對(duì)于本次定位記錄得到的下界、上界,left_side 決定它們?cè)趺春退饕龑?duì)象中保存的構(gòu)造信息比較。

left_side = true 時(shí),如果以下兩個(gè)條件同時(shí)滿(mǎn)足,構(gòu)造信息計(jì)數(shù)(n_hash_potential)加 1:

  • prefix_info.n_fields、n_bytes 大于下界(low_match、low_bytes)。
  • prefix_info.n_fields、n_bytes 小于等于上界(up_match、up_bytes)。

left_side = false 時(shí),如果以下兩個(gè)條件同時(shí)滿(mǎn)足,構(gòu)造信息計(jì)數(shù)(n_hash_potential)加 1:

  • prefix_info.n_fields、n_bytes 小于等于下界(low_match、low_bytes)。
  • prefix_info.n_fields、n_bytes 大于上界(up_match、up_bytes)。

如果 if_3 成立,說(shuō)明本次定位記錄得到的下界或上界的字段數(shù)量、字節(jié)數(shù),和索引對(duì)象中保存的構(gòu)造信息的字段數(shù)量(n_fields)、字節(jié)數(shù)(n_bytes)一致,構(gòu)造信息計(jì)數(shù)加 1(info->n_hash_potential++)。

如果 if_3 不成立,說(shuō)明構(gòu)造信息變了,需要執(zhí)行第 2 段代碼,確定新的構(gòu)造信息,并且重置構(gòu)造信息計(jì)數(shù)。

// storage/innobase/btr/btr0sea.cc
/****** 第 2 段 ******/
static void btr_search_info_update_hash(btr_cur_t *cursor) {
  ...
  info->hash_analysis = 0;

  cmp = ut_pair_cmp(cursor->up_match, cursor->up_bytes, cursor->low_match,
                    cursor->low_bytes);
  /****** if_4 ******/
  if (cmp == 0) {
    // where 條件沒(méi)有定位到匹配的記錄
    // 構(gòu)造信息計(jì)數(shù)清零
    info->n_hash_potential = 0;
    // 雖然給構(gòu)造信息賦值了,但是這個(gè)信息不會(huì)被使用
    /* For extra safety, we set some sensible values here */
    info->prefix_info = {0, 1, true};

  /****** elseif_5 ******/
  } else if (cmp > 0) {
    // 上界(up_match、up_bytes)大于下界(low_match、low_bytes)
    // left_side 都會(huì)設(shè)置為 true
    // 構(gòu)造信息計(jì)數(shù)重置為 1
    info->n_hash_potential = 1;

    ut_ad(cursor->up_match <= n_unique);
    // 如果上界字段數(shù)量等于 n_unique
    // 使用上界作為新的構(gòu)造信息
    if (cursor->up_match == n_unique) {
      info->prefix_info = {
        /* n_bytes   */ 0,
        /* n_fields  */ n_unique,
        /* left_side */ true
      };

    // 下界字段數(shù)量(low_match)小于上界字段數(shù)量(up_match)
    // 使用下界作為新的構(gòu)造信息
    } else if (cursor->low_match < cursor->up_match) {
      info->prefix_info = {
        /* n_bytes   */ 0,
        /* n_fields  */ static_cast<uint16_t>(cursor->low_match + 1),
        /* left_side */ true
      };

    // 下界字段數(shù)量(low_match)等于上界字段數(shù)量(up_match)
    // 下界字節(jié)數(shù)(low_bytes)小于上界字節(jié)數(shù)(up_bytes)
    // 使用下界作為新的構(gòu)造信息
    } else {
      info->prefix_info = {
        /* n_bytes   */ static_cast<uint32_t>(cursor->low_bytes + 1),
        /* n_fields  */ static_cast<uint16_t>(cursor->low_match),
        /* left_side */ true
      };
    }

  /****** else_1 ******/
  } else {
    // 上界(up_match、up_bytes)小于下界(low_match、low_bytes)
    // left_side 都會(huì)設(shè)置為 false
    // 構(gòu)造信息計(jì)數(shù)重置為 1
    info->n_hash_potential = 1;

    ut_ad(cursor->low_match <= n_unique);
    // 如果下界字段數(shù)量等于 n_unique
    // 使用下界作為新的構(gòu)造信息
    if (cursor->low_match == n_unique) {
      info->prefix_info = {
        /* n_bytes   */ 0,
        /* n_fields  */ n_unique,
        /* left_side */ false
      };

    // 下界字段數(shù)量(low_match)大于上界字段數(shù)量(up_match)
    // 使用上界作為新的構(gòu)造信息
    } else if (cursor->low_match > cursor->up_match) {
      info->prefix_info = {
        /* n_bytes   */ 0,
        /* n_fields  */ static_cast<uint16_t>(cursor->up_match + 1),
        /* left_side */ false
      };
    
    // 下界字段數(shù)量(low_match)等于上界字段數(shù)量(up_match)
    // 下界字節(jié)數(shù)(low_bytes)大于上界字節(jié)數(shù)(up_bytes)
    // 使用上界作為新的構(gòu)造信息
    } else {
      info->prefix_info = {
        /* n_bytes   */ static_cast<uint32_t>(cursor->up_bytes + 1),
        /* n_fields  */ static_cast<uint16_t>(cursor->up_match),
        /* left_side */ false
      };
    }
  }
}

第 2 段代碼用于首次或者重新確定構(gòu)造信息,主要邏輯如下:

  • 索引計(jì)數(shù)(info->hash_analysis)清零,下次調(diào)用 btr_search_info_update_hash() 時(shí)重新開(kāi)始計(jì)數(shù)。
  • 如果 left_side = true,并且本次定位記錄得到的下界字段數(shù)量等于 n_unique,使用下界作為新的構(gòu)造信息。
  • 如果 left_side = false,并且本次定位記錄得到的上界字段數(shù)量等于 n_unique,使用上界作為新的構(gòu)造信息。
  • 否則,選擇本次定位記錄得到的上界、下界中較小的那個(gè)作為新的構(gòu)造信息。

ut_pair_cmp(up_match, up_bytes, low_match, low_bytes) 比較本次定位記錄得到的上界、下界,比較結(jié)果保存到 cmp 中。

如果 cmp 等于 0,命中 if_4,說(shuō)明 btr_cur_search_to_nth_level() 定位記錄時(shí),上界、下界相同(up_match 等于 low_match、up_bytes 等于 low_bytes),也就是沒(méi)有找到匹配的記錄,構(gòu)造信息計(jì)數(shù)(info->n_hash_potential)重置為 0。

如果 cmp 大于 0,命中 elseif_5,說(shuō)明本次定位記錄時(shí),下界小于上界,構(gòu)造信息(prefix_info)的 left_side 屬性都會(huì)被設(shè)置為 true。

if (cursor->up_match == n_unique) 條件成立,說(shuō)明搜索條件能夠唯一確定索引中的一條記錄,使用 up_match 作為新構(gòu)造信息的字段數(shù)量,構(gòu)造信息計(jì)數(shù)(info->n_hash_potential)重置為 1,重新開(kāi)始計(jì)數(shù)。

否則,剩下兩種情況,取下界(因?yàn)楸壬辖缧。┳鳛樾聵?gòu)造信息。

cursor->low_match、low_bytes 都從 0 開(kāi)始,變成數(shù)量時(shí)需要加 1。

如果 cmp 小于 0,命中 else_1,說(shuō)明本次定位記錄時(shí),下界大于上界,構(gòu)造信息(prefix_info)的 left_side 屬性都會(huì)被設(shè)置為 false。

if (cursor->low_match == n_unique) 條件成立,說(shuō)明搜索條件能夠唯一確定索引中的一條記錄,使用 low_match 作為新構(gòu)造信息的字段數(shù)量,構(gòu)造信息計(jì)數(shù)(info->n_hash_potential)重置為 1,重新開(kāi)始計(jì)數(shù)。

否則,剩下兩種情況,取上界(因?yàn)楸认陆缧。┳鳛樾聵?gòu)造信息。

cursor->up_match、up_bytes 都從 0 開(kāi)始,變成數(shù)量時(shí)需要加 1。

(3)數(shù)據(jù)頁(yè)計(jì)數(shù)

btr_search_update_block_hash_info() 的主要邏輯分為 2 段:

  • 第 1 段:更新數(shù)據(jù)頁(yè)計(jì)數(shù)。
  • 第 2 段:根據(jù)構(gòu)造信息計(jì)數(shù)、數(shù)據(jù)頁(yè)計(jì)數(shù),決定是否需要為 block 對(duì)應(yīng)的數(shù)據(jù)頁(yè)構(gòu)造或重新構(gòu)造 AHI。

先來(lái)看第 1 段代碼:

// storage/innobase/btr/btr0sea.cc
/****** 第 1 段 ******/
static bool btr_search_update_block_hash_info(...) {
  ...
  const auto info = cursor->index->search_info;
  // last_hash_succ 用于判斷 where 條件是否命中了 AHI
  // 先設(shè)置為 false
  info->last_hash_succ = false;
  ...
  // 數(shù)據(jù)頁(yè)計(jì)數(shù)是否大于 0
  if (block->n_hash_helps > 0 && 
      // 構(gòu)造信息計(jì)數(shù)是否大于 0
      info->n_hash_potential > 0 &&
      // 數(shù)據(jù)頁(yè)對(duì)象中保存的構(gòu)造信息
      //  (可能還沒(méi)有用來(lái)構(gòu)造過(guò) AHI)
      // 和本次確定的構(gòu)造信息是否相同
      block->ahi.recommended_prefix_info.load() == info->prefix_info.load()) {
    if (block->ahi.index &&
        block->ahi.prefix_info.load() == info->prefix_info.load()) {
      // 數(shù)據(jù)頁(yè)對(duì)象中保存的構(gòu)造信息(用來(lái)構(gòu)造過(guò) AHI)
      // 和本次確定的構(gòu)造信息相同
      // 說(shuō)明 where 條件命中了 AHI
      // 把 info->last_hash_succ 設(shè)置為 true
      // 下一篇文章講 AHI 命中時(shí)會(huì)用到這個(gè)屬性
      info->last_hash_succ = true;
    }
    // 構(gòu)造信息沒(méi)有變化,構(gòu)造信息計(jì)數(shù)加 1
    block->n_hash_helps++;
  } else {
    // 構(gòu)造信息變了,重置數(shù)據(jù)頁(yè)計(jì)數(shù)
    block->n_hash_helps = 1;
    // 新確定的構(gòu)造信息保存到數(shù)據(jù)頁(yè)對(duì)象中
    block->ahi.recommended_prefix_info = info->prefix_info.load();
  }
  ...
}

如果以下 3 個(gè)條件都滿(mǎn)足:

  • 數(shù)據(jù)頁(yè)計(jì)數(shù)(block->n_hash_helps)大于 0。
  • 構(gòu)造信息計(jì)數(shù)(info->n_hash_potential)大于 0。
  • 數(shù)據(jù)頁(yè)對(duì)象中保存的構(gòu)造信息(block->ahi.recommended_prefix_info)和本次確定的構(gòu)造信息相同。

說(shuō)明數(shù)據(jù)頁(yè)的構(gòu)造信息沒(méi)有變化,數(shù)據(jù)頁(yè)計(jì)數(shù)加 1(block->n_hash_helps++)。

否則,數(shù)據(jù)頁(yè)計(jì)數(shù)重置為 1,并且保存新的構(gòu)造信息到數(shù)據(jù)頁(yè)對(duì)象中(block->ahi.recommended_prefix_info)。

// storage/innobase/btr/btr0sea.cc
/****** 第 2 段 ******/
static bool btr_search_update_block_hash_info(...) {
  ...
  // BTR_SEARCH_BUILD_LIMIT = 100
  // 同一個(gè)索引的 AHI 構(gòu)造信息
  // 連續(xù) 100 次相同
  if (info->n_hash_potential >= BTR_SEARCH_BUILD_LIMIT &&
      // BTR_SEARCH_PAGE_BUILD_LIMIT = 16
      // 同樣的 AHI 構(gòu)造信息
      // 命中同一個(gè)數(shù)據(jù)頁(yè)的次數(shù)
      // 達(dá)到了該數(shù)據(jù)頁(yè)中記錄數(shù)量的十六分之一
      block->n_hash_helps >
          page_get_n_recs(block->frame) / BTR_SEARCH_PAGE_BUILD_LIMIT) {
    // 滿(mǎn)足上面 2 個(gè)條件之后
    // 如果之前沒(méi)有構(gòu)造過(guò) AHI,則返回 true
    // 表示本次需要構(gòu)造 AHI
    if (!block->ahi.index ||
        // 如果之前已經(jīng)構(gòu)造過(guò) AHI
        // 需要命中同一個(gè)數(shù)據(jù)頁(yè)的次數(shù)
        //   達(dá)到該數(shù)據(jù)頁(yè)中記錄數(shù)量的 2 倍
        // 或者 AHI 構(gòu)造信息發(fā)生了變化
        // 才會(huì)重新構(gòu)造 AHI
        block->n_hash_helps > 2 * page_get_n_recs(block->frame) ||
        block->ahi.recommended_prefix_info.load() !=
            block->ahi.prefix_info.load()) {
      return true;
    }
  }

  // 不滿(mǎn)足上面的一系列條件
  // 返回 false
  // 表示本次不需要構(gòu)造 AHI
  return false;
}

第 2 段代碼,用于判斷是否需要為數(shù)據(jù)頁(yè)(block)構(gòu)造或重新構(gòu)造 AHI,滿(mǎn)足以下兩個(gè)條件,說(shuō)明具備了為該數(shù)據(jù)頁(yè)構(gòu)造 AHI 的資格:

  • 構(gòu)造信息計(jì)數(shù)(info->n_hash_potential)大于等于 100。
  • 數(shù)據(jù)頁(yè)計(jì)數(shù)(block->n_hash_helps)大于數(shù)據(jù)頁(yè)中記錄數(shù)量的十六分之一。

數(shù)據(jù)頁(yè)(block)具備構(gòu)造 AHI 的資格之后,只有以下三種情況會(huì)構(gòu)造或重新構(gòu)造 AHI:

  • 該數(shù)據(jù)頁(yè)之前沒(méi)有構(gòu)造過(guò) AHI(!block->ahi.index 為 true)。
  • 該數(shù)據(jù)頁(yè)之前構(gòu)造過(guò) AHI,構(gòu)造信息沒(méi)有發(fā)生變化,但是數(shù)據(jù)頁(yè)計(jì)數(shù)大于數(shù)據(jù)頁(yè)中記錄數(shù)量的兩倍(2 * page_get_n_recs(block->frame))。
  • 該數(shù)據(jù)頁(yè)之前構(gòu)造過(guò) AHI,但是構(gòu)造信息變了。

(4)構(gòu)造 AHI

滿(mǎn)足前面一系列條件之后,就可以為數(shù)據(jù)頁(yè)構(gòu)造 AHI 了。

// storage/innobase/btr/btr0sea.cc
static void btr_search_build_page_hash_index(...) {
  ...
  // block 是本次需要構(gòu)造 AHI 的數(shù)據(jù)頁(yè)控制塊
  // page 是數(shù)據(jù)頁(yè)對(duì)象
  const auto page = buf_block_get_frame(block);
  // 數(shù)據(jù)頁(yè)的 AHI 構(gòu)造信息
  const auto prefix_info = block->ahi.recommended_prefix_info.load();
  const auto n_fields_for_offsets = btr_search_get_n_fields(prefix_info);

  // 刪除之前為該數(shù)據(jù)頁(yè)構(gòu)造的 AHI 記錄
  if (block->ahi.index && block->ahi.prefix_info.load() != prefix_info) {
    btr_search_drop_page_hash_index(block);
  }
  ...
  // 數(shù)據(jù)頁(yè)中的記錄數(shù)量
  const auto n_recs = page_get_n_recs(page);
  ...
  // page_get_infimum_rec(page) 讀取 infimum 偽記錄
  // page_rec_get_next() 讀取數(shù)據(jù)頁(yè)中的第 1 條用戶(hù)記錄
  auto rec = page_rec_get_next(page_get_infimum_rec(page));

  Rec_offsets offsets;
  ...
  // 用于構(gòu)造第 1 條用戶(hù)記錄的 AHI 的 hash 種子
  const auto index_hash = btr_hash_seed_for_record(index);
  // 計(jì)算第 1 條用戶(hù)記錄的 AHI hash 值
  auto hash_value =
      rec_hash(rec, offsets.compute(rec, index, n_fields_for_offsets),
               prefix_info.n_fields, prefix_info.n_bytes, index_hash, index);

  size_t n_cached = 0;
  // left_side = true
  // 對(duì)于前綴相同的一組記錄(可能有一條或多條記錄)
  // 標(biāo)記需要為這一組的第一條記錄構(gòu)造 AHI
  if (prefix_info.left_side) {
    hashes[n_cached] = hash_value;
    recs[n_cached] = rec;
    n_cached++;
  }

  // 循環(huán),標(biāo)記需要為數(shù)據(jù)頁(yè)中第 2 條及以后的用戶(hù)記錄構(gòu)造 AHI
  for (;;) {
    const auto next_rec = page_rec_get_next(rec);
    if (page_rec_is_supremum(next_rec)) {
      // left_side = false 時(shí)
      // 標(biāo)記需要為 supremum 偽記錄構(gòu)造 AHI
      // 然后結(jié)束循環(huán)
      if (!prefix_info.left_side) {
        hashes[n_cached] = hash_value;
        recs[n_cached] = rec;
        n_cached++;
      }

      break;
    }
    // 為當(dāng)前循環(huán)的記錄計(jì)算用于構(gòu)造 AHI 的 hash 值
    const auto next_hash_value = rec_hash(
        next_rec, offsets.compute(next_rec, index, n_fields_for_offsets),
        prefix_info.n_fields, prefix_info.n_bytes, index_hash, index);
    // hash_value != next_hash_value
    // 說(shuō)明換了一組記錄
    if (hash_value != next_hash_value) {
      /* Insert an entry into the hash index */
      // left_side = true
      // 為下一組的第一條記錄構(gòu)造 AHI
      if (prefix_info.left_side) {
        hashes[n_cached] = next_hash_value;
        recs[n_cached] = next_rec;
        n_cached++;
      } else {
        // left_side = false
        // 為本組的最后一條記錄構(gòu)造 AHI
        hashes[n_cached] = hash_value;
        recs[n_cached] = rec;
        n_cached++;
      }
    }

    rec = next_rec;
    hash_value = next_hash_value;
  }
  ...
  // 為數(shù)據(jù)頁(yè)構(gòu)造 AHI 之后
  // 重置數(shù)據(jù)頁(yè)計(jì)數(shù)
  block->n_hash_helps = 0;
  // 已經(jīng)用來(lái)構(gòu)造過(guò) AHI 構(gòu)造信息保存到
  // 數(shù)據(jù)頁(yè)對(duì)象的 ahi.prefix_info 屬性中
  block->ahi.prefix_info = prefix_info;
  block->ahi.index = index;
  // 把每條記錄的 AHI hash 值和記錄地址
  // 插入到 AHI hash 表中
  const auto table = btr_get_search_table(index);
  for (size_t i = 0; i < n_cached; i++) {
    ha_insert_for_hash(table, hashes[i], block, recs[i]);
  }
  ...
}

為數(shù)據(jù)頁(yè)構(gòu)造 AHI 主要分為三大步驟:

  • 循環(huán)讀取數(shù)據(jù)頁(yè)中的記錄,每讀取一條記錄,根據(jù) AHI 構(gòu)造信息計(jì)算記錄的哈希值,把哈希值保存到 hashes 數(shù)組中、記錄地址保存到 recs 數(shù)組中,一條記錄在 hashs、recs 數(shù)組中的下標(biāo)一樣,都是 n_cached。這一步有個(gè)特殊邏輯需要處理,就是對(duì)于前綴相同的一組記錄,根據(jù) left_side 決定為第一條還是最后一條記錄構(gòu)造 AHI。
  • 數(shù)據(jù)頁(yè)計(jì)數(shù)(block->n_hash_helps)重置為 0,重新開(kāi)始計(jì)數(shù),用于為該數(shù)據(jù)頁(yè)重新構(gòu)造 AHI 作準(zhǔn)備。然后,把構(gòu)造信息(prefix_info)、數(shù)據(jù)頁(yè)所屬的索引(index)分別保存到數(shù)據(jù)頁(yè)對(duì)象的 ahi.prefix_info、ahi.index 屬性中,btr_search_update_block_hash_info() 會(huì)用到這兩個(gè)屬性。
  • 把 hashs、recs 數(shù)組中的哈希值、記錄地址一一對(duì)應(yīng)的插入到哈希表中,每條記錄的哈希值映射到該記錄的地址。

4、總結(jié)

AHI 構(gòu)造流程的前三步都是在判斷是否滿(mǎn)足某些條件,這些條件的范圍從大到小。

先是索引級(jí)別,判斷索引被命中的次數(shù)。

然后,是索引級(jí)別的構(gòu)造信息計(jì)數(shù)。

構(gòu)造信息來(lái)源于定位記錄過(guò)程中產(chǎn)生的下界、上界,其源頭是 where 條件,我們可以把它看成對(duì) where 條件的抽象,或者更具體點(diǎn),把它看成 where 條件的分類(lèi)。

某個(gè)構(gòu)造信息的計(jì)數(shù)達(dá)到指定次數(shù),意味著如果根據(jù)這個(gè)構(gòu)造信息(或者說(shuō)這類(lèi) where 條件)構(gòu)造 AHI,命中率會(huì)比較高。

InnoDB 以數(shù)據(jù)頁(yè)為單位,一次性為某個(gè)數(shù)據(jù)頁(yè)中的所有記錄構(gòu)造 AHI。

構(gòu)造信息計(jì)數(shù)滿(mǎn)足條件之后,還需要進(jìn)一步?jīng)Q定為哪些數(shù)據(jù)頁(yè)構(gòu)造 AHI,于是就有了數(shù)據(jù)頁(yè)計(jì)數(shù)(實(shí)際上是數(shù)據(jù)頁(yè)級(jí)別的構(gòu)造信息計(jì)數(shù))。

當(dāng)索引計(jì)數(shù)、構(gòu)造信息計(jì)數(shù)、數(shù)據(jù)頁(yè)計(jì)數(shù)都滿(mǎn)足條件之后,某個(gè)數(shù)據(jù)頁(yè)就初步具備了構(gòu)造 AHI 的資格,最后,還會(huì)根據(jù)該數(shù)據(jù)頁(yè)是否構(gòu)造過(guò) AHI、構(gòu)造信息是否發(fā)生變化等條件,做出終極決定:是否為該數(shù)據(jù)頁(yè)構(gòu)造 AHI。

本文轉(zhuǎn)載自微信公眾號(hào)「一樹(shù)一溪」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系一樹(shù)一溪公眾號(hào)。

責(zé)任編輯:姜華 來(lái)源: 一樹(shù)一溪
相關(guān)推薦

2025-04-08 08:20:00

2023-04-12 16:45:07

MySQL索引數(shù)據(jù)結(jié)構(gòu)

2017-06-06 10:30:12

前端Web寬度自適應(yīng)

2012-05-09 10:58:25

JavaMEJava

2014-09-05 10:10:32

Android自適應(yīng)布局設(shè)計(jì)

2010-08-30 09:52:03

DIV高度自適應(yīng)

2010-08-30 10:26:20

DIV自適應(yīng)高度

2022-04-26 10:13:00

哈希索引MySQLInnoDB

2023-10-23 08:48:04

CSS寬度標(biāo)題

2024-05-22 09:31:07

2025-01-21 08:00:00

自適應(yīng)框架框架開(kāi)發(fā)

2022-04-12 07:48:57

云技術(shù)SDN網(wǎng)絡(luò)

2022-10-24 17:57:06

CSS容器查詢(xún)

2011-05-12 11:28:20

按比例縮放

2009-04-23 11:24:09

2021-11-01 23:57:03

數(shù)據(jù)庫(kù)哈希索引

2017-08-16 14:08:46

Android O圖標(biāo)視覺(jué)

2010-08-26 16:27:46

CSS高度

2017-04-13 11:20:37

圖片寬度解決方案前端

2014-04-15 13:09:08

Android配色colour
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)

在线观看的黄色| 亚洲乱码国产乱码精品| 国产一区二区三区精品在线观看| 亚洲日本欧美天堂| 国产成人一区二区三区免费看| 久久久香蕉视频| 日日天天久久| 欧美日韩国产高清一区二区| 日韩精品在线中文字幕| 黄色国产在线| 国产精品一区二区你懂的| 97激碰免费视频| 久久午夜精品视频| 大桥未久女教师av一区二区| 欧美午夜影院一区| 成人免费播放器| 久操视频在线观看| 久久久久久久久久久久久久久99| 91在线网站视频| 欧美性猛交xxxx乱大交hd| 亚洲一本视频| 日韩一区二区三区在线播放| 久久久午夜精品福利内容| 欧洲精品久久久久毛片完整版| 午夜免费久久看| 黑人巨大国产9丨视频| 国产精品ⅴa有声小说| 高清成人在线观看| 91老司机精品视频| 亚洲国产无线乱码在线观看| 亚洲自啪免费| 国模极品一区二区三区| 老女人性淫交视频| 日韩精品一区二区久久| 亚洲人成啪啪网站| 日本黄色录像片| 亚洲天堂av资源在线观看| 欧美日韩一级大片网址| 18岁视频在线观看| 欧美日韩在线观看首页| 亚洲国产另类精品专区| www国产免费| 免费的黄网站在线观看| 国产精品色哟哟网站| 日韩精品一区二区三区丰满| 免费在线看v| 久久亚洲一级片| 精品久久中出| 丝袜+亚洲+另类+欧美+变态| a在线播放不卡| 国产亚洲欧美一区二区 | 国产三级在线免费| 久久综合九色综合97婷婷女人 | 91官网在线观看| 欧在线一二三四区| 欧美一区国产| 日本道在线观看一区二区| 日韩 欧美 高清| 欧美日一区二区三区| 欧美日韩亚洲丝袜制服| 天天操天天爽天天射| 成人精品国产亚洲| 欧美日韩国产首页在线观看| 爱豆国产剧免费观看大全剧苏畅 | 欧美剧在线免费观看网站| 中文字幕在线观看日| 国产精品一区二区精品| 日韩欧美在线1卡| 国产白袜脚足j棉袜在线观看 | 清纯唯美一区二区三区| 国产在线资源| 亚洲欧洲美洲综合色网| 黄色一级片国产| 激情视频网站在线播放色| 一本一本久久a久久精品综合麻豆| 黄色高清无遮挡| 9.1麻豆精品| 精品国产伦理网| 久久久亚洲av波多野结衣| 日韩中文字幕高清在线观看| 麻豆一区二区在线观看| 日本少妇bbwbbw精品| 久久精品一区| 18成人免费观看网站下载| 天天操天天干天天操| 国产日韩欧美制服另类| 中文字幕一区二区三区四区五区人 | 色爱精品视频一区| 欧美 日韩 国产 一区二区三区| 伊人成年综合电影网| 国产成人精品久久二区二区91| 中文天堂在线播放| 成人手机电影网| 亚洲国产日韩欧美| 波多野结衣中文在线| 欧美性大战久久久久久久| 国产一卡二卡三卡四卡| 成人在线免费观看视频| 欧美精品videofree1080p| 国产第一页在线观看| 国产成人精品免费视频网站| 欧美中文娱乐网| a级影片在线| 色婷婷综合在线| 18禁一区二区三区| 欧美日韩在线网站| 97视频网站入口| 999精品国产| 国产亚洲精品福利| 久操手机在线视频| 久久精品97| 亚洲美女动态图120秒| 丝袜 亚洲 另类 欧美 重口| 日韩主播视频在线| 国产乱子伦精品| 在线欧美三级| 欧美日韩国产天堂| 亚洲第一综合网| 亚洲理论在线| 999国产视频| 免费av在线| 欧美性大战xxxxx久久久| 欧美大片免费播放器| 欧美成人久久| 91精品久久久久久久久久入口| 日中文字幕在线| 亚洲成人综合视频| 亚洲美女精品视频| 欧美 亚欧 日韩视频在线 | 国产黄色一区二区| 国产精品久线观看视频| 国产无套粉嫩白浆内谢的出处| 大型av综合网站| 久久久久五月天| 高潮一区二区三区乱码| 一区二区三区在线观看视频| 911av视频| 久久精品国内一区二区三区水蜜桃| 国产精品草莓在线免费观看| 美女毛片在线看| 日韩欧美中文字幕在线播放| 北岛玲一区二区| 国产欧美午夜| 蜜桃传媒视频麻豆第一区免费观看| а√在线天堂官网| 精品国产成人系列| 国产在线拍揄自揄拍| 国产成人高清在线| 做爰高潮hd色即是空| 成人综合日日夜夜| 美女av一区二区三区| 国产毛片一区二区三区va在线| 自拍偷拍欧美激情| 在线观看网站黄| 欧美日本国产| 国产一区在线免费| 欧美男男tv网站在线播放| 亚洲老头老太hd| 中文人妻av久久人妻18| 国产区在线观看成人精品| 一级在线免费视频| 亚洲成人二区| 国产成人一区二区三区免费看| 9lporm自拍视频区在线| 精品亚洲一区二区三区四区五区 | 日韩一区二区在线看| 欧美三根一起进三p| 成人性生交大片| 国产免费黄色av| 成人vr资源| 亚洲一区二区自拍| bl在线肉h视频大尺度| 亚洲加勒比久久88色综合| 无码人妻久久一区二区三区| 中文字幕亚洲一区二区va在线| 免费观看黄网站| 国产亚洲福利| 一区二区三区四区国产| 日韩三级网址| 热久久美女精品天天吊色| a视频网址在线观看| 日韩精品在线网站| 丰满少妇xoxoxo视频| 亚洲欧洲国产日本综合| 88av在线播放| 免费av成人在线| 久青草视频在线播放| 国产精品亚洲二区| 2014国产精品| 欧美特黄aaaaaaaa大片| 精品国模在线视频| 亚洲av成人精品一区二区三区在线播放| 91福利资源站| 一区二区三区免费高清视频| 国产日韩亚洲欧美综合| 国产chinese中国hdxxxx| 日韩av中文字幕一区二区| 污污污污污污www网站免费| 亚洲小说图片| av一区和二区| 久久久加勒比| 欧美一级电影在线| 成人在线app| 亚洲最大中文字幕| 日韩一级在线播放| 欧美美女一区二区| 无码一区二区三区| 亚州成人在线电影| 天天综合天天做| 国产精品视频第一区| 久久精品国产亚洲av麻豆| 国产福利精品导航| 日韩av片网站| 麻豆精品91| 和岳每晚弄的高潮嗷嗷叫视频| 欧美激情欧美| 亚洲国产精品一区二区第四页av| 欧美毛片免费观看| 97超级碰碰| 国产精品成人3p一区二区三区| 国产成人精品久久二区二区| 日本三级一区| 97人人爽人人喊人人模波多| 91福利国产在线观看菠萝蜜| yellow中文字幕久久| 成年人视频在线免费观看| 日韩国产在线播放| 日韩一级中文字幕| 精品免费日韩av| www.日韩在线观看| 欧美一区二区三区成人| 一级做a爱片久久毛片| 欧美伊人久久久久久久久影院| 国产综合精品视频| 亚洲成人tv网| 日韩激情一区二区三区| 一区二区免费视频| 欧美日韩免费做爰视频| 中文字幕日韩精品一区| 黄色一级片一级片| 中文字幕亚洲精品在线观看| 一二三四在线观看视频| 欧美国产精品劲爆| 国产成人免费观看网站| 国产亚洲综合色| 欧美黄色一级生活片| 久久久美女艺术照精彩视频福利播放| 成年人网站免费看| 国产亚洲美州欧州综合国| 免费视频91蜜桃| 中文av一区特黄| 久久嫩草捆绑紧缚| 亚洲免费资源在线播放| 特级片在线观看| 亚洲www啪成人一区二区麻豆| 日韩精品一区二区在线播放| 欧美日韩另类在线| jizz国产在线观看| 欧美日韩在线播放三区| 国产精品一区二区黑人巨大| 日韩欧美精品三级| 无码国产精品一区二区免费16| 日韩精品在线免费播放| 国产资源在线观看| 久久人体大胆视频| 91jq激情在线观看| 欧洲一区二区视频| 另类一区二区三区| 99久久免费国| 亚洲调教一区| 在线精品亚洲一区二区| 国内视频精品| 欧洲av无码放荡人妇网站| 免费看欧美女人艹b| 激情成人在线观看| 99国产欧美另类久久久精品| 欧美成人另类视频| 亚洲精品伦理在线| 永久免费看片在线播放| 欧美日韩一区高清| 丰满人妻妇伦又伦精品国产| 亚洲丝袜一区在线| 在线欧美三级| 国产精品成人aaaaa网站| 999精品视频在线观看| 国产一区二区视频在线免费观看 | 人妻中文字幕一区二区三区| 在线播放中文一区| 五月天福利视频| 啊v视频在线一区二区三区| ****av在线网毛片| 国产精品爽黄69天堂a| 国产精品15p| 一级日韩一区在线观看| 国产精品日韩久久久| 99精品视频国产| 久久综合久久综合久久综合| 杨钰莹一级淫片aaaaaa播放| 色欧美乱欧美15图片| 精品国产伦一区二区三区| 亚洲深夜福利网站| 91黄页在线观看| 成人国内精品久久久久一区| 日韩欧美四区| 欧美一级爱爱视频| 老司机午夜精品99久久| 菠萝菠萝蜜网站| 亚洲欧美日韩国产综合在线| 午夜视频网站在线观看| 亚洲国产精品va在线观看黑人| 欧美日韩欧美| 国产精品99一区| 全球av集中精品导航福利| 在线亚洲美日韩| 日韩不卡在线观看日韩不卡视频| 熟妇人妻久久中文字幕| 亚洲综合男人的天堂| 97人妻精品一区二区三区软件 | 国产一区二区三区黄| 一区二区三区四区在线观看国产日韩| 国产福利视频在线播放| www.激情成人| 精品亚洲永久免费| 欧美一区二区三区日韩视频| 午夜伦理在线| 国产精品久久久av| 欧美男gay| 欧美女人性生活视频| av激情亚洲男人天堂| 国产乱码久久久久久| 日韩欧美成人一区二区| 91亚洲天堂| 成人黄色在线观看| 999久久久91| 中文字幕永久有效| 国产精品萝li| 一级片在线观看视频| 中文字幕综合一区| 久久69成人| 亚洲视频小说| 九九国产精品视频| 99自拍视频在线| 欧美一区二区免费观在线| av在线免费网址| 97久久人人超碰caoprom欧美| 亚洲女同中文字幕| 久久黄色一级视频| 亚洲国产一区二区视频| 黑人操亚洲女人| 久久免费少妇高潮久久精品99| 第一区第二区在线| 亚洲午夜精品久久久久久人妖| 97久久人人超碰| 少妇高潮av久久久久久| 亚洲色图国产精品| 欧美啪啪网站| 日本黄网站色大片免费观看| 国产大片一区二区| 国产精品7777| 亚洲欧美日本另类| 国产成人毛片| 狠狠精品干练久久久无码中文字幕| 国产成人鲁色资源国产91色综| 国产精品第72页| 亚洲欧美国产日韩中文字幕| 日韩不卡在线| 路边理发店露脸熟妇泻火| 成人性生交大合| 日本黄色中文字幕| 久久久国产在线视频| 国产乱论精品| 日韩欧美黄色大片| 亚洲欧美激情插| 网站黄在线观看| 国产精品视频永久免费播放| 99久久这里只有精品| 精品人妻在线视频| 日本乱人伦一区| 91网在线看| 另类小说综合网| 黑人巨大精品欧美一区| 日韩精品视频播放| 综合国产在线视频| 都市激情亚洲欧美| www.日日操| 亚洲在线观看免费视频| 岛国在线大片| 成人av免费在线看| 日本va欧美va瓶| 国产性70yerg老太| 日韩在线观看免费全| 精品福利网址导航| 天天操天天干天天做| 欧美视频裸体精品| 成人毛片av在线| 欧美重口乱码一区二区| 岛国av在线一区| 夜夜躁很很躁日日躁麻豆| 午夜精品在线视频| 久久久久久久久久久妇女|