⭐《MySQL 實戰45講》筆記

[TOC]mysql

一. 索引與優化

本篇內容主要來自極客時間《MySQL實戰45講》中的:算法

  • 04 - 深刻淺出索引(上)
  • 05 - 深刻淺出索引(下)

基本數據存儲模型

  • 有序數組
  • 哈希表
  • 搜索樹
  • 跳錶sql

    Redis 的有序集合使用的就是這個結構
  • LSM樹 等

有序數組:

優勢: 查找很快, 支持範圍查詢數據庫

缺點: 插入代價高, 必須邏輯上移動後續的全部記錄segmentfault

搜索樹:

  • 二叉搜索樹

    搜索效率最高, 但實際並不採用, 由於索引是存在磁盤的.數組

    假設一棵樹高爲20, 一次搜索就須要20個數據塊, 對應磁盤就是20次隨機查找. 對於普通硬盤來講, 一次尋址約 10ms, 則一次搜索就須要 20x10ms = 200ms.若是要讓一個查詢儘可能少讀磁盤, 那就必須儘可能少地查詢數據塊, 應該使用下面的多叉樹.緩存

  • 多叉樹session

    爲了減小磁盤訪問次數, 可使用 "N叉"樹, 這裏的 N 取決於數據塊的大小.併發

    以 InnoDB 中 一個整數字段爲例, 這個N差很少是1200.函數

    計算方法: 補充!!

    若是樹高爲4, 則能夠存儲 1200^3 個值, 考慮樹根數據塊基本都在內存中, 所以一次搜索只須要3次磁盤查找, 考慮到第2層數據塊也有很大機率在內存中, 那麼訪問磁盤次數就更少了.

引伸: InnoDB 裏N叉樹中的N如何調整

N = 頁page的大小 / 每一個索引項大小

N叉樹中非葉子節點存放的是索引信息, 每一個索引項包含Key和Point指針及其餘輔助數據, 其中Point指針固定大小6字節, 默認索引頁的大小是16KB. 所以主鍵爲int時, int佔用4個字節, 加上輔助數據差很少每一個索引項佔用13字節, 所以非葉子節點大約能夠存儲 16k/13 ≈ 1260 個左右.

N的大小大體是根據上述式子決定的, 所以若要調整N, 則有2個防線:

  • MySQL 5.6之後能夠修改 page 大小, 參數: innodb_page_size

    未測試
  • 經過修改Key字段類型, 好比 int 佔用4字節, bitint 佔用8字節.

哈希表

優勢: 新增和查找都很快

缺點: 沒法進行範圍遍歷, 必須一個一個查找.

InnoDB 索引的存儲結構

索引組織表: 按照主鍵順序, 以索引形式存放的表.

InnoDB 使用了 B+樹 做爲索引的存儲結構.

InnoDB 中的索引, 按照葉子節點內容來區分, 分爲兩類:

  1. 主鍵索引(聚簇索引, clustered index)
  2. 非主鍵索引(二級索引, secondary index)

InnoDB 中 B+ 樹的葉子節點存放的是 , 一頁裏面能夠存多個行.

這裏的頁指的是 InnoDB 的頁, 而非磁盤頁, 默認大小是 16KB.

索引的維護涉及 插入刪除, 這兩個操做可能致使 頁分裂頁合併 的問題.

  • 插入: 若是插入不是有序遞增的, 就須要邏輯上移動插入點後面的數據. 更糟糕的是, 若是插入點所在的數據塊已滿, 根據B+樹的算法, 此時須要進行 頁分裂 操做(新申請一個頁, 將部分數據挪動過去). 頁分裂 操做除了影響性能外, 還會影響頁的利用率, 下降了約 50% 的利用率.
  • 刪除: 當兩個相鄰頁因爲刪除元素致使利用率很低後, 會將數據頁作合併, 合併的過程能夠理解爲頁分裂的逆過程.

索引可能由於刪除或頁分裂的緣由致使數據頁有空洞, 而重建索引的過程會建立一個新的索引, 並將數據順序插入, 使得索引更緊湊, 空間利用率更高.


Q. 爲何表刪除了一半數據, 文件大小卻沒變?

A. 簡單回答一下.

刪除時僅僅是將數據從所在的數據頁上標記刪除, 遺留的空位還會保留着, 供後續插入新記錄時直接存放.

這種狀況能夠考慮重建索引以減小磁盤空間佔用

optimize table 表名;
-- 或
alter table 表名 engine=InnoDB;

注意 alter table 表名 = engine=InnoDB; 會加 MDL 讀鎖.

若是是 MySQL 5.7, 則會使用 OnlineDDL, 避免長時間的 MDL 鎖致使業務不可用.


Q. 主鍵索引和非主鍵索引的區別

A. 主要區別在於:

  • 主鍵索引(葉子節點)存儲的是行記錄, 非主鍵索引(葉子節點)存儲的是對應主鍵的內容.
  • 非主鍵索引查詢時, 須要先在該索引上查找到對應主鍵, 再去主鍵索引查找, 這個過程叫作回表.所以在應用中應儘可能使用主鍵索引, 避免多一次回表

Q. 非主鍵索引中字段值相同的索引項是如何存儲的?

A. 結論: 獨立存儲.

以索引c爲例, id是主鍵, 假設有兩個記錄 (c=10, id=1), (c=10, id=2), 這其實在索引c上是兩條不一樣的索引項, 它的存放順序是先按照c遞增, c等值狀況下再按照id遞增, 所以能夠理解爲索引c 是 (c, id)


Q. 若不給表設置主鍵會怎樣?

A. InnoDB 會爲每一行隱式分配一個 RowId 做爲主鍵. 因此其實仍是有主鍵索引的


Q. 聯合索引的存儲結構是怎樣的?

A. 《高性能MySQL 第三版》P144,關於索引類型的插圖,說明了聯合索引是N個字段組合成一個索引的。


Q. 在聯合索引中多個字段順序是怎樣的?

A. 以 (a,b) 爲例, id 是主鍵. 則在該索引上, 是先按照 a 遞增, 再根據 b 遞增, 最後根據 id 遞增的順序排序.

能夠和下面寫到的 最左前綴 一塊兒理解.


Q. 若是表用到了聯合主鍵, 那麼在二級索引中是如何存儲的?

A. 假設聯合主鍵是 (a,b), 此時表中還有個字段 c, 能夠分3種狀況考慮:

  1. 若是創建了索引 (c), 則先按照 c遞增, 其次 a 遞增, 最後是 b 遞增.
  2. 若是創建了索引 (c,a), 那麼順序同1, 這種狀況下是不必單首創建 (c,a), 而只須要索引(c)便可
  3. 若是創建了索引 (c,b), 那麼會先按照 c遞增, 而後是 b 遞增, 最後是 a 遞增.

索引的選擇

主鍵的選擇

主鍵儘可能使用自增主鍵, 緣由:

  • 自增主鍵是有序遞增的, 往索引插入時都是追加操做, 避免了頁分裂的問題, 而業務上的主鍵通常不知足有序遞增.
  • 自增主鍵一般是 int not null primary key auto_incrementbigint not null primary key auto_increment, 使用整形作主鍵只須要4個字節, 使用長整型則是8個字節.
  • 主鍵的字段越小, 普通索引的葉子節點也就越小, 佔用的空間就越小.

所以從性能存儲空間看, 自增主鍵一般是最好的選擇.


那麼何時能夠考慮用業務字段做爲主鍵:

  1. 沒有其餘二級索引(無需考慮二級索引葉子節點大小)
  2. 業務字段惟一

↑ 這就是典型的 KV 場景了, 考慮到查詢時儘可能用主鍵索引, 避免回表, 此時就能夠將這個索引設置爲主鍵.

覆蓋索引

當查詢語句中涉及的全部字段都在同一個索引中, 此時因爲只須要在該索引樹上查找而不須要回表, 這成爲覆蓋索引.

覆蓋索引能夠減小樹的搜索次數, 顯著提高性能, 所以是經常使用的優化手段.

注意: 索引的維護是有代價的, 所以是否新增冗餘索引來支持覆蓋索引時須要權衡考量.

以索引 (code, name) 爲例, 當使用以下語句時是能夠用到覆蓋索引, 避免回表的:

select name from 表 where code = "xxx";

-- 或

select id from 表 where code = "xxx";

Q. 是否有必要爲了覆蓋索引而設立聯合索引?

A. 分狀況:

  • 若是是高頻請求, 那麼能夠創建聯合索引來使用覆蓋索引優化, 避免回表
  • 若是是低頻請求, 若已有現成的可利用最左前綴優化的索引, 或單獨索引, 則不必. 此時索引帶來的優化好處可能已經被維護索引的代價蓋掉了.

最左前綴

最左前綴指的是聯合索引的前幾個字段, 以及字符串索引的前幾個字符.

因爲索引是以B+樹結構存儲的, 而B+樹這種索引結構是能夠利用索引的最左前綴來定位記錄的.

以 (name, age) 這個聯合索引爲例, 它的大體示意圖以下:

能夠看出索引項的順序是按照索引定義的字段順序來排序的.

如下語句會用到上面的這個索引的最左前綴:

-- 聯合索引上的最左N個字段
select * from 表 where name = "xx";

-- 字符串的最左N個字符
select * from 表 where name like '張%';

Q. 聯合索引上的字段順序如何肯定?

A. 優先考慮複用能力, 其次考慮存儲空間.

原則1: 若是經過調整順序能夠少建立一個索引, 那麼一般就會優先考慮調整後的這個順序了.

原則2: 優先考慮原則1, 其次應考慮空間佔用.

以聯合索引 (a,b) 爲例, 因爲最左前綴優化的緣由, 在該表上就不須要單獨再創建索引 (a) 了, 所以這種狀況只須要創建一個聯合索引 (a,b) 便可.

可是, 若是此時一樣須要用到索引 (b), 那麼這時候有兩個選擇:

  1. 建立 (a,b) 及 (b)
  2. 建立 (b,a) 及 (a)

此時若字段a比較大, 則應考慮方案1, 不然應考慮方案2.


索引下推 index condition pushdown

對於聯合索引, 對於不知足最左前綴的部分, 在某些狀況下是能夠用到 索引下推 的.

索引下推: 在索引遍歷過程當中, 利用索引中已有的字段過濾不知足條件的記錄, 避免每次判斷都回表.

先明確:

  • 索引下推 是在 MySQL 5.6 引入的.
  • 在 explain 的時候能夠在 Extra 看到 Using index condition , 說明能夠用到索引下推

    "能夠"用, 但不必定用/沒有.

    這個地方還不大明確

以索引 (name, age) 爲例, 查看一下SQL語句:

select * from 表 where name like '張%' and age > 20;

此時會先利用索引, 快速找到 name以"張"開頭的記錄, 而後依次向右遍歷:

  • 如果在 MySQL 5.6 之前, 則須要一個一個回表並篩選 age > 20 的記錄
  • 如果在 MySQL 5.6 及之後, 則根據 索引下推 則會在索引遍歷過程當中對索引包含的字段先作判斷, 過濾不知足條件的記錄, 減小回表次數.

Change Buffer 之普通索引和惟一索引的選擇

前提: 業務能保證記錄是惟一的狀況下, 才須要考慮.

理解這部份內容的 意義:

在遇到大量插入數據慢, 內存命中率低的狀況下, 多一個排查思路.

相關配置:

## 最大佔用 innodb_buffer_poll 內存空間的百分比
innodb_change_buffer_max_size=50

Change Buffer

  • 只會針對普通索引 (確定是二級索引了)
  • 可以在不影響數據一致性前提下將數據更新操做(DML, insert/update/delete)緩存在 Change Buffer 中, 而無需當即讀取(磁盤)數據頁. 當下次須要訪問這個數據頁的時候, 會將該數據頁讀取到內存中, 再將這些緩存的操做應用上去.
  • 記錄的操做存儲在 Change Buffer 中, 它佔用的是InnoDB Buffer Pool, 同時它是可持久化的.
  • Change Buffer 減小的是隨機讀的次數(無需每次更新都讀取), 若在讀取記錄前保存在該Buffer中操做越多, 則受益更大. 所以它同時也提升了內存利用效率(所以讀取數據頁是會佔用內存空間的)
  • 從磁盤讀取索引數據頁並將Change Buffer緩存的操做應用上去, 這個過程稱爲 Merge
  • Merge 發生的狀況:

    1. 讀取記錄時應用Change Buffer
    2. 後臺線程按期Merge
    3. 正常關閉(shutdown)數據庫
想象一下, 一張表有4,5個普通二級索引, 這些索引的使用率並不高.

同時該表會頻繁更新數據, 若沒有Change Buffer, 那麼每次更新操做維護二級索引時都須要從磁盤讀入索引對應的數據頁, 而有了Change Buffer後只需將這些操做保存在該Buffer中, 極大減小了磁盤隨機讀次數, 最後統一Merge便可.


查詢過程的區別:

  • 普通索引

    從索引樹根目錄, 逐層查找對應記錄所在數據頁.

    若不在內存中, 則須要先從磁盤上讀入內存.

    若數據所在頁已經在內存中, 則讀取該記錄, 並向右遍歷直到不符合條件. 因爲數據的讀取是以數據頁爲單位(默認16KB), 所以這個過程是在內存中, 對性能影響極小, 除非是記錄恰好在數據頁的最後一條.(考慮到機率, 能夠忽略)

  • 惟一索引

    相似普通索引, 只是在找到對應一條記錄後就中止了.

結論: 在查詢過程當中性能區別不大.


更新過程的區別:

若數據都在內存中則沒有什麼區別, 所以如下只討論不在內存中的狀況.

  • 普通索引

    將更新語句記錄在 Change Buffer 中, 更新結束.

  • 惟一索引

    因爲更新操做須要判斷是否違反數據一致性約束, 所以沒法使用 Change Buffer, 須要先將數據頁從磁盤讀取到內存, 進行判斷, 再作更新操做.


Q. Change Buffer 何時會成爲負優化?

A. 在下述普通索引場景:

當每次更新操做後立刻讀取, 因爲更新操做會緩存在Change Buffer中, 下一次立刻讀取時須要當即 Merge.

此時反而多了維護 Change Buffer的代價, 同時隨機訪問IO不會減小.


Q. Change Buffer 適合什麼場景?

A. 寫多讀少業務

Change Buffer 會將更新的操做緩存起來, 緩存得越多, 則在 Merge 操做的時候收益就越大.

常見業務模型: 帳單類, 日誌類系統.

聯合索引的字段順序 - 根據區分度

當須要建立聯合索引的狀況下, 在 不考慮索引複用 前提, 且 字段順序不影響索引完整使用 前提下, 如何肯定聯合索引中的字段順序?

!!! 注意這裏的前提:

  1. 不考慮索引複用
  2. 字段順序不影響索引完整使用

    若是是 where a = xx order by b 這類語句, 那麼直接就是聯合索引 (a, b) 了.

此時應該按照字段的區分度, 區分度高的在前.

以索引 (status, product_id) 爲例, 分別查看其區分度:

SELECT
    COUNT(DISTINCT status)/COUNT(*) as status_disc,
    COUNT(DISTINCT product_id)/COUNT(*) as product_id_disc
FROM
    表名;

當前這個例子很清楚, status 就幾種取值, 基數很小, 區分度不好, 所以應該創建聯合索引 (product_id, status)

如何建立字符串索引

通常有如下幾種選擇:

  1. 完整索引

    最耗費空間

  2. 前綴索引, 只選擇前N個字符

    適用: 前N個字符具備足夠區分度的狀況.

    缺點: 增長額外掃描行數, 同時沒法使用覆蓋索引.

  3. 字符串倒序 + 前綴索引

    適用: 字符串前N個字符區分度不夠的狀況下, 且後N個字符有足夠區分度

    存儲: 存儲的時候直接存儲倒序的字符串

    使用: update 表 set s = reverse("123456");

    缺點: 除了前綴索引的缺點外, 每次更新/查找都須要額外的 reverse 函數調用消耗, 同時沒法利用索引進行範圍查找.

  4. 額外字段存儲hash值

    存儲: 新增額外字段存儲字符串對應的hash值, 若使用 crc32 函數, 則額外佔用4個字節

    優勢: 查找性能穩定, 基本在 O(1)

    使用: 因爲hash值會衝突, 所以查找時除了hash字段判斷外, 還要判斷原始字符串是否一致. select * from 表 where s_hash = crc32("123456") and s = "123456";

    缺點: 佔用額外的存儲空間, 沒法利用索引進行範圍查找

索引建立命令

CREATE TABLE 時建立

CREATE TABLE IF NOT EXISTS `users` (
    -- 省略字段定義
    
    PRIMARY KEY (`id`),
    UNIQUE KEY `users_phone` (`phone`),
    KEY `users_name` (`name`),
) Engine=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci

ALTER TABLE用來建立普通索引、UNIQUE索引或PRIMARY KEY索引。

ALTER TABLE table_name ADD INDEX index_name (column_list)
-- 可忽略索引名
-- ALTER TABLE table_name ADD INDEX (column_list)

ALTER TABLE table_name ADD UNIQUE (column_list)

ALTER TABLE table_name ADD PRIMARY KEY (column_list)

-- 一個語句建多個索引
ALTER TABLE HeadOfState ADD PRIMARY KEY (ID), ADD INDEX (LastName,FirstName);

其中table_name是要增長索引的表名,column_list指出對哪些列進行索引,多列時各列之間用逗號分隔。索引名index_name可選,缺省時,MySQL將根據第一個索引列賦一個名稱。另外,ALTER TABLE容許在單個語句中更改多個表,所以能夠在同時建立多個索引。

CREATE INDEX可對錶增長普通索引或UNIQUE索引。

CREATE INDEX index_name ON table_name (column_list)

CREATE UNIQUE INDEX index_name ON table_name (column_list)

索引失效狀況

索引失效的狀況我的認爲主要是如下狀況:

  1. 區分度過低, 致使優化器認爲全表掃描會更快.
  2. 對索引字段使用函數、進行計算、類型轉換

    WHERE a + 1 = 2 這種語句也會致使索引 a 失效, 此時應該改寫 SQL 語句爲: WHERE a = 1
  3. 包括顯式轉換及隱式轉換

    若是字段 phone 是 char 類型, 那麼 WHERE phone = 18612345678 一樣可能會致使索引失效, 應該改寫成 WHERE phone = '18612345678 '
  4. 不知足最左前綴

    包括聯合索引和字符串最左前綴

  5. 索引列存在NULL且查詢條件是 is not null, 若索引沒有覆蓋查詢字段和查詢條件時, 此時會符合如下的<u>狀況6</u>, 致使全表掃描.

    如下是我的測試

    -- UserName 是 varchar, nullable
    
    explain select Uid from new_light_user where UserName is  null;
    -- SIMPLE    new_light_user    ref    UserName    UserName    768    const    10    Using where; Using index
    
    explain select * from new_light_user where UserName is not null;
    -- SIMPLE    new_light_user    ALL    UserName    null         null  null   17979    Using where
  6. 根據查詢條件, 沒法使用索引快速定位, 但可使用索引掃描時, 若innodb認爲代價太大也會直接走全表掃描.

其餘注意點

索引設計規範

  • 單表索引建議控制在5個之內
  • 但索引字段不容許超過5個
  • 索引字段要定義爲 NOT NULL, 不然:

    1. 佔用額外存儲空間(1字節)
    2. 致使索引的使用更加複雜, 在某些狀況下會致使索引失效
    3. 條件判斷更麻煩, 須要 IS NULL, IS NOT NULL
  • 區分度不高的字段不建議創建索引

    除非查詢值的篩選力度很高, 好比 status = 0 (表示未完成), 由於大多數值是 1, 所以這種狀況下建索引仍是有意義的.

  • 創建聯合索引時, 優先將區分度高的字段放前面.

二. 加鎖規則及案例

本文內容主要是 《MySQL實戰45講》 課程中第 20,21,30 課程的我的筆記及相關理解.

主要是對於加鎖規則的理解及分析.

如下僅針對 MySQL 的 InnoDB 引擎.

MyISM 引擎就只有表鎖

基本概念

鎖的種類

MySQL 中的鎖主要分爲:

  • 全局鎖

    flush tables with read lock;
  • 表級鎖

    • 表鎖

      lock table 表名 read;
      lock table 表名 write;
    • 元數據鎖(Meta Data Lock, MDL)

      在 MySQL 5.5 引入 MDL 鎖.

      MySQL 5.6 之後支持 OnlineDDL

  • 行鎖
還有個自增鎖, 後續補充.

意向鎖在此先不作討論.

表級鎖

元數據鎖 MDL

MDL支持的版本:

  • 在 MySQL 5.5 引入 MDL 鎖.
  • MySQL 5.6 之後支持 OnlineDDL.

MDL鎖目的: 控制對錶元數據修改的併發.

MDL鎖類型分爲:

  1. MDL 讀鎖(讀鎖之間不衝突)
  2. MDL 寫鎖(讀寫鎖衝突, 寫鎖之間也衝突)

普通的增刪改查會自動獲取MDL讀鎖, 而對錶的字段修改或建立索引等修改表元數據的操做會自動獲取MDL寫鎖, 在此期間增刪改查就會被阻塞掉.

OnlineDDL 是一種近似不鎖表的特性, 它的過程以下:

  1. 獲取MDL寫鎖

    這個期間會阻塞

  2. 降級爲MDL讀鎖
  3. 執行DDL語句

    大部分時間消耗在這裏, 好比重建表(alter table 表 Engine=Innodb)時, 須要將數據從舊錶按主鍵順序逐一添加到新表, 而大部分時間就消耗在這裏.

    同時在此期間, 全部對數據庫的增刪改操做都會記錄在特定日誌中, 待這部分執行完畢後再應用這些日誌, 從而保證數據一致性.

  4. 升級爲MDL寫鎖

    這個期間會也阻塞

  5. 釋放MDL寫鎖

也就是說 OnlineDDL 其實仍是會鎖表, 但只會在開始跟結束的時候鎖, 中間大部分時間是不鎖的.

對於 ALTER TABLE 表名 Engine=Innodb 這種DDL操做:

  • 5.6以前是在Server層面上經過建立臨時表來實現的(鎖表+建立臨時表+拷貝數據+替換表)
  • 5.7及以後的OnlineDDL是在InnoDB層面上處理的, 它會建立臨時文件.

部分DDL操做不支持OnlineDDL, 好比添加全文索引(FULLTEXT)和空間索引(SPATIAL)

InnoDB 中的鎖

行鎖

行鎖也叫作記錄鎖, 這個鎖是加在具體的索引項上的.

行鎖分爲兩種:

  • 讀鎖: 共享鎖
  • 寫鎖: 排它鎖

行鎖衝突狀況:

  • 讀鎖與寫鎖衝突
  • 寫鎖與寫鎖衝突

須要明確:

  • 鎖的對象是索引

間隙鎖

記錄之間是存在間隙的, 這個間隙也是能夠加上鎖實體, 稱爲間隙鎖.

間隙鎖存在的目的: 解決幻讀問題.

間隙鎖衝突狀況:

  • 間隙鎖之間是不衝突的, 它們都是爲了防止插入新的記錄.
  • 間隙鎖與插入操做(插入意向鎖)產生衝突

須要明確:

  • 間隙鎖僅在 可重複讀隔離級別下才存在.
  • 間隙鎖的概念是動態的

    對間隙(a,b)加鎖後, 存在間隙鎖 (a,b).

    此時若 a 不存在(刪除), 則間隙鎖會向左延伸直到找到一條記錄.

    若b不存在了(刪除), 則間隙鎖會向右延伸直到找到一條記錄.

    假設主鍵上存在記錄 id=5 和 id=10 和 id=15 的3條記錄, 當存在某個間隙鎖 (10,15) 時, 若咱們將 id=10 這一行刪掉, 則間隙鎖 (10, 15) 會動態擴展成 (5, 15), 此時想要插入 id=7 的記錄會被阻塞住.

    此處的刪除指的是事務提交後, 不然間隙鎖依舊是 (10,15)

next-key lock

next-key lock = 行鎖 + 間隙鎖

next-key lock 的加鎖順序:

  1. 先加間隙鎖
  2. 再加行鎖
若是加完間隙鎖後, 再加行鎖時被阻塞進入鎖等待時, 間隙鎖在此期間是不會釋放的.

兩階段鎖協議

兩階段鎖協議指的是:

  1. 在用到的時候會加鎖
  2. 在事務提交的時候纔會釋放鎖

瞭解這個協議的啓發在於:

  • 在一個事務中須要對多個資源進行加鎖時, 應儘可能把最可能形成鎖衝突的放在最後, 這邊能夠避免持有這個鎖的時間太久致使線程長時間等待, 下降併發度.

索引搜索

索引搜索指的是就是:

  1. 在索引樹上利用樹搜索快速定位找到第一個值
  2. 而後向左或向右遍歷

order by desc 就是用最大的值來找第一個

order by 就是用最小的值來找第一個

等值查詢

等值查詢指的是:

  • 在索引樹上利用樹搜索快速定位 xx=yy的過程

    where xx > yy 時, 也是先找到 xx = yy 這條記錄, 這一個步驟是等值查詢.但後續的向右遍歷則屬於範圍查詢.
  • 以及在找到具體記錄後, 使用 xx=yy 向右遍歷的過程.

加鎖規則

該部分源自《MySQL實戰45講》中的 《21-爲何我只改了一行的語句, 鎖這麼多》

如下僅針對 MySQL 的 InnoDB 引擎在 可重複讀隔離級別, 具體MySQL版本:

  • 5.x 系列 <= 5.7.24
  • 8.0 系列 <=8.0.13

如下測試若未指定, 則默認使用如下表, 相關案例爲了不污染原始數據, 所以在不影響測試結果前提下, 都放在事務中執行, 且最終不提交.

create table c20(
    id int not null primary key, 
    c int default null, 
    d int default null, 
    key `c`(`c`)
) Engine=InnoDB;

insert into c20 values(0,0,0),(5,5,5),(10,10,10),(15,15,15),(20,20,20),(25,25,25);

/*
+----+------+------+
| id | c    | d    |
+----+------+------+
|  0 |    0 |    0 |
|  5 |    5 |    5 |
| 10 |   10 |   10 |
| 15 |   15 |   15 |
| 20 |   20 |   20 |
| 25 |   25 |   25 |
+----+------+------+
*/

2個"原則", 2個"優化", 1個"BUG"

  1. 原則1: 加鎖的基本單位是next-key lock, 前開後閉區間
  2. 原則2: 訪問到的對象纔會加鎖

    select id from t where c = 15 lock in share mode;

    加讀鎖時, 覆蓋索引優化狀況下, 不會訪問主鍵索引, 所以若是要經過 lock in share mode 給行加鎖避免數據被修改, 那就須要繞過索引優化, 如 select 一個不在索引中的值.

    但若是改爲 for update , 則 mysql 認爲接下來會更新數據, 所以會將對應主鍵索引也一塊兒鎖了

  3. 優化1: 索引上的等值查詢, 對惟一索引加鎖時, next-key lock 會退化爲行鎖

    select * from t where id = 10 for update;

    引擎會在主鍵索引上查找到 id=10 這一行, 這一個操做是等值查詢.

    鎖範圍是

  4. 優化2: 索引上的等值查詢, 向右遍歷時且最後一個值不知足等值條件時, next-key Lock 會退化爲間隙鎖

    select * from t where c = 10 for update;

    因爲索引c是普通索引, 引擎在找到 c=10 這一條索引項後繼續向右遍歷到 c=15 這一條, 此時鎖範圍是 (5, 10], (10, 15)

  5. BUG 1: 惟一索引上的範圍查詢會訪問到不知足條件的第一個值

    id> 10 and id <=15, 這時候會訪問 id=15 以及下一個記錄.
對索引上的更新操做, 本質上是 刪除+插入

讀提交與可重複讀的加鎖區別

  1. 讀提交下沒有間隙鎖
  2. 讀提交下有一個針對 update 語句的 "semi-consistent" read 優化.

    若是 update 語句碰到一個已經被鎖了的行, 會讀入最新的版本, 而後判斷是否是知足查詢條件, 若知足則進入鎖等待, 若不知足則直接跳過.

    注意這個策略對 delete 是無效的.

  3. ?????? 語句執行過程當中加上的行鎖, 會在語句執行完成後將"不知足條件的行"上的行鎖直接釋放, 無需等到事務提交.

insert into ... select ... 加鎖

https://time.geekbang.org/col...

在可重複讀隔離級別, binlog_format = statement 時, 該語句會對被 select 的那個表訪問到的記錄和間隙加鎖

小夥子, 很危險的.

生產環境大表複製數據通常用 pt-archiver 工具來處理, 避免 insert ... select ... 鎖致使的長阻塞.

pt-archiver: 數據歸檔工具

或者簡單用 select ... into outfile 和 load data infile 組合來代替 insert ... select 完成插入操做.

簡單例子

例子1

begin;
select * from c20 where id=5 for update;

在主鍵索引 id 上快速查找到 id=5 這一行是等值查詢

例子2

begin;
select * from c20 where id > 9 and id < 12 for update;

在主鍵索引 id 上找到首個大於 9 的值, 這個過程實際上是在索引樹上快速找到 id=9 這條記錄(不存在), 找到了 (5,10) 這個間隙, 這個過程是等值查詢.

而後向右遍歷, 在遍歷過程當中就不是等值查詢了, 依次掃描到 id=10 , id=15 這兩個記錄, 其中 id=15 不符合條件, 所以最終鎖範圍是 (5,10], (10, 15]

例子3

begin;
select * from c20 where id > 9 and id < 12 order by id desc for update;

根據語義 order by id desc, 優化器必須先找到第一個 id < 12 的值, 在主鍵索引樹上快速查找 id=12 的值(不存在), 此時是向右遍歷到 id=15, 根據優化2, 僅加了間隙鎖 (10,15) , 這個過程是等值查詢.

接着向左遍歷, 遍歷過程就不是等值查詢了, 最終鎖範圍是: (0,5], (5, 10], (10, 15)

我的理解:

  1. 因爲有 order by id desc, 所以首先是等值查詢 id=12 不存在, 向右遍歷不知足, 優化, 所以加了間隙鎖 (10, 15)
  2. 向左遍歷到 id=10, next-key lock, (5,10]
  3. 向左遍歷到 id=5, next-key lock, (0,5], 不知足條件, 中止遍歷

例子4

begin;
select * from c20 where c>=15 and c<=20 order by c desc lock in share mode;

執行過程:

  1. 在索引c上搜索 c=20 這一行, 因爲索引c是普通索引, 所以此處的查找條件是 <u>最右邊c=20</u> 的行, 所以須要繼續向右遍歷, 直到找到 c=25 這一行, 這個過程是等值查詢. 根據優化2, 鎖的範圍是 (20, 25)?
  2. 接着再向左遍歷, 以後的過程就不是等值查詢了.

我的理解:

  1. 因爲 order by c desc, 所以首先等值查詢 c=20 存在, 加鎖 (15, 20]
  2. 向右遍歷到 c=25, 不知足, 但可優化, 加鎖 (20,25)
  3. 向左遍歷到 c=15, 加鎖 (10, 15]
  4. 向左遍歷到 c=10, 加鎖 (5,10]

例子5

begin;
select * from c20 where c<=20 order by c desc lock in share mode;

這裏留意一下 , 加鎖範圍並非 (20, 25], (15, 20], (10,15], (5,10], (0, 5], (-∞, 5], 而是

...........

..........

.........

........

.......

......

.....

......

.......

........

.........

..........

...........

全部行鎖+間隙鎖.

具體爲何, 其實只要 explain 看一下就明白了.

+------+-------------+-------+------+---------------+------+---------+------+------+-----------------------------+
| id   | select_type | table | type | possible_keys | key  | key_len | ref  | rows | Extra                       |
+------+-------------+-------+------+---------------+------+---------+------+------+-----------------------------+
|    1 | SIMPLE      | c20   | ALL  | c             | NULL | NULL    | NULL |   14 | Using where; Using filesort |
+------+-------------+-------+------+---------------+------+---------+------+------+-----------------------------+

但若是是 c<=19, 則會使用索引 c, 這說明 innodb 引擎有本身一套規則用於"估算"當前使用二級索引仍是主鍵索引哪一個開銷會更小.

explain select * from c20 where c<=19 order by c desc lock in share mode;
+------+-------------+-------+-------+---------------+------+---------+------+------+-------------+
| id   | select_type | table | type  | possible_keys | key  | key_len | ref  | rows | Extra       |
+------+-------------+-------+-------+---------------+------+---------+------+------+-------------+
|    1 | SIMPLE      | c20   | range | c             | c    | 5       | NULL |    4 | Using where |
+------+-------------+-------+-------+---------------+------+---------+------+------+-------------+

例子6

begin;
select * from c20 where c>=10 and c<15 for update;

加鎖範圍是

  • 索引 c 的 (5,10], (10,15]

    這裏對索引 c 的 15 好像是退化成行鎖了, 不是很理解.
  • 主鍵索引的 [10]

    訪問到的纔會加鎖, 因爲沒有訪問主鍵 id=15, 所以不會對齊加鎖.

例子7 - 我的不理解的地方

-- T1 事務A
begin;
select * from c20 where id>=15 and id<=20 order by id desc lock in share mode;

-- T2 事務B
begin;
update c20 set d=d+1 where id=25;    -- OK
insert into c20 values(21,21,21);    -- 阻塞

-- T3 事務A 人爲製造死鎖, 方便查看鎖狀態
update c20 set d=d+1 where id=25;    -- OK
/*
此時 事務B 提示:
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
*/

我的不理解的:

根據order by id desc, T1 時刻事務A首先在主鍵索引上搜索 id=20 這一行, 正常來講主鍵索引上 id=20 的只有一行, 不必向右遍歷.

加鎖範圍:

  • (5,10]
  • (10,15]
  • (15,20]
  • (20,25)
mysql> show engine innodb status
------------------------
LATEST DETECTED DEADLOCK
------------------------
2019-09-27 10:34:29 0xe2e8
*** (1) TRANSACTION:
TRANSACTION 1645, ACTIVE 100 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1080, 4 row lock(s), undo log entries 1
MySQL thread id 82, OS thread handle 77904, query id 61115 localhost ::1 root update
insert into c20 values(21,21,21)
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 23 page no 3 n bits 80 index PRIMARY of table `test_yjx`.`c20` trx id 1645 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 7 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
 0: len 4; hex 80000019; asc     ;;
 1: len 6; hex 00000000066d; asc      m;;
 2: len 7; hex 6e0000019a0110; asc n      ;;
 3: len 4; hex 80000019; asc     ;;
 4: len 4; hex 8000001a; asc     ;;

*** (2) TRANSACTION:
TRANSACTION 1646, ACTIVE 271 sec starting index read
mysql tables in use 1, locked 1
5 lock struct(s), heap size 1080, 5 row lock(s)
MySQL thread id 81, OS thread handle 58088, query id 61120 localhost ::1 root updating
update c20 set d=d+1 where id=25
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 23 page no 3 n bits 80 index PRIMARY of table `test_yjx`.`c20` trx id 1646 lock mode S locks gap before rec
Record lock, heap no 7 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
 0: len 4; hex 80000019; asc     ;;
 1: len 6; hex 00000000066d; asc      m;;
 2: len 7; hex 6e0000019a0110; asc n      ;;
 3: len 4; hex 80000019; asc     ;;
 4: len 4; hex 8000001a; asc     ;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 23 page no 3 n bits 80 index PRIMARY of table `test_yjx`.`c20` trx id 1646 lock_mode X locks rec but not gap waiting
Record lock, heap no 7 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
 0: len 4; hex 80000019; asc     ;;
 1: len 6; hex 00000000066d; asc      m;;
 2: len 7; hex 6e0000019a0110; asc n      ;;
 3: len 4; hex 80000019; asc     ;;
 4: len 4; hex 8000001a; asc     ;;

*** WE ROLL BACK TRANSACTION (1)

上述的:

  • (1) TRANSACTION(事務1) 指的是事務B
  • (2) TRANSACTION(事務2) 指的是事務A

注意與上面的 事務A, 事務B 順序是相反了, 別看錯了.

分析:

  • (1) TRANSACTION

    • insert into c20 values(21,21,21) 最後一句執行語句
  • (1) WAITING FOR THIS LOCK TO BE GRANTED

    • index PRIMARY of table test_yjx.c20 說明在等表 c20 主鍵索引上的鎖
    • lock_mode X locks gap before rec insert intention waiting 說明在插入一條記錄, 試圖插入一個意向鎖, 與間隙鎖產生衝突了
    • 0: len 4; hex 80000019; asc ;; 衝突的間隙鎖: 16進制的 19, 即 10進制的 id=25 左邊的間隙.
  • (2) TRANSACTION 事務2信息

    • update c20 set d=d+1 where id=25 最後一句執行語句
  • (2) HOLDS THE LOCK(S) 事務2持有鎖的信息

    • index PRIMARY of table test_yjx.c20 說明持有c20表主鍵索引上的鎖
    • lock mode S locks gap before rec 說明只有間隙鎖
    • 0: len 4; hex 80000019; asc ;; 間隙鎖: id=25 左邊的間隙
  • (2) WAITING FOR THIS LOCK TO BE GRANTED: 事務2正在等待的鎖

    • index PRIMARY of table test_yjx.c20 說明在等待 c20 表主鍵索引上的鎖
    • lock_mode X locks rec but not gap waiting 須要對行加寫鎖
    • 0: len 4; hex 80000019; asc ;; 等待給 id=25 加行鎖(寫)
  • WE ROLL BACK TRANSACTION (1) 表示回滾了事務1

我的猜想實際狀況是:

  1. 首先找到 id=20 這一條記錄, 因爲bug, 引擎認爲可能存在不止一條的 id=20 的記錄(即將其認爲是普通索引), 所以向右遍歷, 找到了 id=25 這一行, 因爲此時是等值查詢, 根據優化2, 鎖退化爲間隙鎖, 即 (20,25)
  2. 以後正常向左遍歷.

沒法證明本身的猜想. 已在課程21和課程30留下如下留言, 等待解答(或者無人解答). 2019年9月27日

-- T1 事務A
begin;
select * from c20 where id>=15 and id<=20 order by id desc lock in share mode;

-- T2 事務B
begin;
update c20 set d=d+1 where id=25;    -- OK
insert into c20 values(21,21,21);    -- 阻塞

不能理解, 爲何事務A執行的語句會給 間隙(20,25) 加上鎖.
經過 show engine innodb status; 查看發現事務A確實持有上述間隙鎖.
經過 explain select * from c20 where id>=15 and id<=20 order by id desc lock in share mode; 查看 Extra 也沒有 filesort, key=PRIMARY, 所以我的認爲是按照主鍵索引向左遍歷獲得結果.

按照個人理解, 因爲 order by id desc , 所以首先是在主鍵索引上搜索 id=20, 同時因爲主鍵索引上這個值是惟一的, 所以沒必要向右遍歷. 然而事實上它確實這麼作了, 這讓我想到了 BUG1: 主鍵索引上的範圍查詢會遍歷到不知足條件的第一個.
可是這一步的搜索過程應該是等值查詢纔對, 徹底一臉懵住了...
不知道老師如今還能看到這條評論不?

加鎖案例

案例: 主鍵索引 - 等值查詢 - 間隙鎖

-- T1 事務A
begin;
update c20 set d=d+1 where id=7;
/*
1. 在主鍵索引上不存在id=7記錄, 根據規則1: 加鎖基本單位是 next-key lock, 所以加鎖範圍是(5,10]
2. 因爲id=7是一個等值查詢, 根據優化2, id=10不知足條件, 所以鎖退化爲間隙鎖 (5,10)
*/

-- T2 事務B
begin;
insert into c20 values(8,8,8);        -- 阻塞
update c20 set d=d+1 where id=10;    -- OK
對應課程的案例一

案例: 非惟一索引 - 等值查詢 - 間隙鎖

-- T1 事務A
begin;
update c20 set d=d+1 where c=7;
/* 分析
1. 加鎖基本單位是next-key lock, 加鎖範圍就是 (5,10]   -- 此時只是分析過程, 並不是加鎖過程
2. 根據優化2, 索引上的等值查詢(c=7)向右遍歷且最後一個值不知足條件時, next-key lock 退化爲間隙鎖, 加鎖範圍變爲 (5, 10)
3. 因爲是在索引c上查詢, 所以加鎖範圍其實是索引 c 上的 ((5,5), (10,10)) , 格式 (c, id)
*/

-- T2 事務B
begin;
insert into c20 values(4,5,4);    -- OK
insert into c20 values(6,5,4);    -- 被間隙鎖堵住
insert into c20 values(9,10,9);    -- 被間隙鎖堵住
insert into c20 values(11,10,9);    -- OK

案例: 非惟一索引 - 等值查詢 - 覆蓋索引

關注重點: 覆蓋索引優化致使無需回表的狀況對主鍵索引影響

-- T1 事務A
begin;
select id from c20 where c = 5 lock in share mode;    
-- 索引c是普通索引, 所以會掃描到 c=10 這一行, 所以加鎖範圍是 (0,5], (5,10)
-- 同時因爲優化2: 索引上的等值查詢向右遍歷且最後一個值不知足條件時next-key lock退化爲間隙鎖, 即加鎖範圍實際是  (0,5], (5,10)
-- 注意, 該條查詢因爲只 select id, 實際只訪問了索引c, 並無訪問到主鍵索引, 根據規則2: 訪問到的對象纔會加鎖, 所以最終只對索引c 的範圍 (0,5], (5,10) 加鎖

-- T2 事務B
begin;
update c20 set d=d+1 where id=5;    -- OK, 由於覆蓋索引優化致使並無給主鍵索引上加鎖
insert into c20 values(7,7,7);
對應課程的案例二

注意, 上面是使用 lock in share mode 加讀鎖, 所以會被覆蓋索引優化.

若是使用 for update, mysql認爲你接下來要更新行, 所以也會鎖上對應的主鍵索引.

案例: 非主鍵索引 - 範圍查詢 - 對主鍵的影響

關注重點在於: 普通索引上的範圍查詢時對不符合條件的索引加鎖時, 是否會對對應的主鍵索引產生影響.

-- T1 事務A
begin;
select * from c20 where c>=10 and c<11 for update;
/*
1. 首先查找到 c=10 這一行, 鎖範圍 (5,10]
2. 接着向右遍歷(這時候不是等值查詢, 是遍歷查詢), 找到 c=15 這一行, 不符合條件, 查詢結束. 根據規則2: 只有訪問到的對象纔會加鎖, 因爲不須要訪問c=15對應的主鍵索引項, 所以這裏的鎖範圍是索引c上的 (5,10], (10,15], 以及主鍵上的行鎖[10]
*/

-- T2 事務B
begin;
select * from c20 where c=15 for update;     -- 阻塞
select * from c20 where id=15 for update;    -- OK

加鎖範圍

  • 索引 c

    • (5,10]
    • (10,15]
  • 主鍵

    • [10]

案例: 主鍵索引 - 範圍鎖

-- T1 事務A
begin;
select * from c20 where id>=10 and id<11 for update;
/*
1. 首先在主鍵索引上查找 id=10 這一行, 根據優化1: 索引上的等值查詢在對惟一索引加鎖時, next-key lock 退化爲行鎖, 此時加鎖範圍是 [10]
2. 繼續向右遍歷到下一個 id=15 的行, 此時並不是等值查詢, 所以加鎖範圍是 [10], (10,15]
*/

-- T2 事務B
begin;
insert into c20 values(8,8,8);        -- OK
insert into c20 values(13,13,13);    -- 阻塞
update c20 set d=d+1 where id=15;    -- 阻塞
對應課程案例三

這裏要注意, 事務A首次定位查找id=10這一行的時候是等值查詢, 然後續向右掃描到id=15的時候是範圍查詢判斷.

主鍵索引的加鎖範圍

  • [10]
  • (10,15]

案例: 非惟一索引 - 範圍鎖

-- T1 事務A
begin;
select * from c20 where c >= 10 and c < 11 for update;
/*
1. 首先在索引c上找到 c=10 這一行, 加上鎖 (5,10]
2. 向右遍歷找到 c=15 這一行, 不知足條件, 最終加鎖範圍是 索引c上的 (5,10], (10,15], 及主鍵索引 [5]
*/

-- T2 事務B
begin;
insert into c20 values(8,8,8);        -- 阻塞
update c20 set d=d+1 where c=15;    -- 阻塞
update c20 set d=d+1 where id=15;    -- 阻塞
對應課程案例四

主鍵的加鎖範圍

  • (5,10]
  • (10,15]

案例: 惟一索引 - 範圍鎖 - bug

-- T1 事務A
begin;
select * from c20 where id>10 and id<=15 for update

-- T2 事務B
begin;
update c20 set d=d+1 where id=20;    -- 阻塞
insert into c20 values(16,16,16);    -- 阻塞

順便提一下:

begin;
select * from c20 where id>10 and id<15 for update;
/*
1. 在主鍵索引上找到id=15這一行, 不知足條件, 根據原則1, 加鎖 (10,15]
*/

對應課程案例五

主鍵的加鎖範圍

  • (10,15]
  • (15,20]

案例: 非惟一索引 - 等值

-- T1 事務A
begin;
insert into c20 values(30,10,30);
commit;
/*
在索引c上, 此時有兩行 c=10 的行
因爲二級索引上保存着主鍵的值, 所以並不會有兩行徹底一致的行, 以下:
c    0    5    10    10    15    20    25
id    0    5    10    30    15    20    25

此時兩個 (c=10, id=10) 和 (c=10, id=30) 之間也是存在間隙的
*/

-- T2 事務B
begin;
delete from c20 where c=10;
/*
1. 首先找到索引c上 (c=10, id=10) 這一行, 加鎖 (5,10]
2. 向右遍歷, 找到 (c=10, id=30) 這一行, 加鎖 ( (c=10,id=10), (c=10,id=30) ]
3. 向右遍歷, 找到 c=20 這一行, 根據優化2, 索引上的等值查詢向右遍歷且最後一個值不匹配時, next-key lock 退化爲間隙鎖, 即加鎖 (10,15)
4. 總的加鎖範圍是 (5,10], ( (c=10,id=10), (c=10,id=30) ], (10,15]
*/

-- T3 事務C
begin;
insert into c20 values(12,12,12);    -- 阻塞
update c20 set d=d+1 where c=15;    -- OK


-- T4 掃尾, 無視
delete from c20 where id=30;
對應課程案例六

delete 的加鎖邏輯跟 select ... for update 是相似的.

事務 B 對索引 c 的加鎖範圍

  • (5,10]
  • (10,15)

案例: 非惟一索引 - limit

-- T0 初始環境
insert into c20 values(30,10,30);

-- T1 事務A
begin;
delete from c20 where c=10 limit 2;
/*
1. 找到 c=10 的第一條, 加鎖 (5,10]
2. 向右遍歷, 找到 c=10,id=30 的記錄, 加鎖 ( (c=10,id=10), (c=10,id=30) ], 此時知足 limit 2
*/

-- T2, 事務B
begin;
insert into c20 values(12,12,12);    -- OK

若是不加 limit 2 則會繼續向右遍歷找到 c=15 的記錄, 新增長鎖範圍 (10,15)

對應課程案例七

指導意義:

  • 在刪除數據時儘可能加 limit, 不只能夠控制刪除的條數, 還能夠減少加鎖的範圍.

案例: 死鎖例子

-- T1 事務A
begin;
select id from c20 where c=10 lock in share mode;
/*
1. 在索引c上找到 c=10 這一行, 因爲覆蓋索引的優化, 沒有回表, 所以只會在索引c上加鎖 (5,10]
2. 向右遍歷, 找到 c=15, 不知足, 根據優化2, 加鎖範圍退化爲 (10,15)
3. 總的加鎖範圍是在索引c上的 (5,10], (10,15)
*/

-- T2 事務B
begin;
update c20 set d=d+1 where c=10;    -- 阻塞
/*
1. 找到 c=10 這一行, 試圖加上鎖 (5,10], 按照順序先加上間隙鎖(5,10), 因爲間隙鎖之間不衝突, OK. 以後再加上 [10] 的行鎖, 但被T1時刻的事務A阻塞了, 進入鎖等待
*/

-- T3 事務A
insert into t values(8,8,8);    -- OK, 但形成 事務B 回滾
/*
往 (5,10) 這個間隙插入行, 此時與 T2時刻事務B 加的間隙鎖產生衝突.
同時因爲 事務B 也在等待 T1時刻事務A 加的行鎖, 兩個事務間存在循環資源依賴, 形成死鎖.
此時事務B被回滾了, 報錯以下:
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
*/
對應課程案例八

案例: 非主鍵索引 - 逆序

-- T1 事務A
begin;
select * from c20 where c>=15 and c<=20 order by c desc lock in share mode;
/*
1. 在索引c上找到 c=20 這一行, 加鎖 (15,20]
2. 向左遍歷, 找到 c=15 這一行, 加鎖 (10,15]
3. 繼續向左遍歷, 找到 c=10 這一行, 因爲不知足優化條件, 所以直接加鎖 (5,10], 不知足查詢條件, 中止遍歷. 
4. 最終加鎖範圍是 (5,10], (10,15], (15, 20]
*/

-- T2 事務B
insert into c20 values(6,6,6);    -- 阻塞
對應課程的上期答疑

索引 c 的加鎖範圍

  • (5,10]
  • (10,15]
  • (15,20]
  • (20, 25)

案例: 讀提交級別 - semi-consistent 優化

-- 表結構
create table t(a int not null, b int default null)Engine=Innodb;
insert into t values(1,1),(2,2),(3,3),(4,4),(5,5);

-- T1 事務A
set session transaction isolation level read committed;
begin;
update t set a=6 where b=1;
/*
b沒有索引, 所以全表掃描, 對主鍵索引上全部行加上行鎖
*/

-- T2 事務B
set session transaction isolation level read committed;
begin;
update t set a=7 where b=2;    -- OK
/*
在讀提交隔離級別下, 若是 update 語句碰到一個已經被鎖了的行, 會讀入最新的版本, 而後判斷是否是知足查詢條件, 若知足則進入鎖等待, 若不知足則直接跳過.
*/
delete from t where b=3;    -- 阻塞
/*
注意這個策略對 delete 是無效的, 所以delete語句被阻塞
*/
對應課程評論下方 @時隱時現 2019-01-30 的留言

案例: 主鍵索引 - 動態間隙鎖 - delete

-- T1 事務A
begin;
select * from c20 where id>10 and id<=15 for update;
/*
加鎖 (10,15], (15, 20]???
*/

-- T2 事務B 注意此處沒加 begin, 是立刻執行並提交的單個事務.
delete from c20 where id=10;    -- OK
/*
事務A在T1時刻加的間隙鎖 (10,15) 此時動態擴展成 (5,15)
*/

-- T3 事務C
insert into c20 values(10,10,10);    -- 阻塞
/*
被新的間隙鎖堵住了
*/
對應課程評論下方 @Geek_9ca34e 2019-01-09 的留言

若是將上方的 T2時刻的事務B 和 T3時刻的事務C 合併在一個事務裏, 則不會出現這種狀況.

我的理解是, 事務未提交時, 期間刪除/修改的數據僅僅是標記刪除/修改, 此時記錄還在, 所以間隙鎖範圍不變.

只有在事務提價後纔會進行實際的刪除/修改, 所以間隙鎖才"會動態擴大範圍"

案例: 普通索引 - 動態間隙鎖 - update

-- T1 事務A
begin;
select c from c20 where c>5 lock in share mode;
/*
找到 c=5, 不知足, 向右遍歷找到 c=10, 加鎖 (5,10], 繼續遍歷, 繼續加鎖...
*/

-- T2 事務B
update c20 set c=1 where c=5;    -- OK
/*
刪除了 c=5 這一行, 致使 T1時刻事務A 加的間隙鎖 (5,10) 變爲 (1,10)
*/

-- T3 事務C
update c20 set c=5 where c=1;    -- 阻塞
/*
將 update 理解爲兩步:
1. 插入 (c=5, id=5) 這個記錄    -- 被間隙鎖阻塞
2. 刪除 (c=1, id=5) 這個記錄
*/

案例: 非主鍵索引 - IN - 等值查詢

begin;
select id from c20 where c in (5,20,10) lock in share mode;

經過 explain 分析語句:

mysql> explain select id from c20 where c in (5,20,10) lock in share mode;
+----+-------------+-------+-------+---------------+------+---------+------+------+---------------------
| id | select_type | table | type  | possible_keys | key  | key_len | ref  | rows | Extra     
+----+-------------+-------+------------+-------+---------------+------+---------+------+------+---------
|  1 | SIMPLE      | c20   | range | c             | c    | 5       | NULL |    3 | Using where; Using index |
+----+-------------+-------+------------+-------+---------------+------+---------+------+------+---------
1 row in set, 1 warning (0.00 sec)
顯示結果太長, 所以將 partitions, filtered 列刪除了

結果分析:

  • 使用了索引 c
  • rows = 3 說明這3個值都是經過 B+ 樹搜索定位的

語句分析:

  1. 在索引c上查找 c=5, 加鎖 (0,5], 向右遍歷找到 c=10, 不知足條件, 根據優化2, 加鎖 (5,10)
  2. 在索引c上查找 c=10, 相似步驟1, 加鎖 (5,10], (10, 15)
  3. 在索引c上查找 c=20, 加鎖 (15,20], (20, 25)

注意上述鎖是一個個逐步加上去的, 而非一次性所有加上去.

考慮如下語句:

begin;
select id from c20 where c in (5,20,10) order by id desc for update;

根據語義 order by id desc, 會依次查找 c=20, c=10, c=5.

因爲加鎖順序相反, 所以若是這兩個語句併發執行的時候就有可能發生死鎖.

相關命令

查看最後一個死鎖現場

show engine innodb status;

查看 LATEST DETECTED DEADLOCK 這一節, 記錄了最後一次死鎖信息.

示例

------------------------
LATEST DETECTED DEADLOCK
------------------------
2019-09-24 16:24:18 0x5484
*** (1) TRANSACTION:
TRANSACTION 1400, ACTIVE 191 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1080, 3 row lock(s)
MySQL thread id 54, OS thread handle 74124, query id 36912 localhost ::1 root updating
update c20 set d=d+1 where c=10
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 23 page no 4 n bits 80 index c of table `test_yjx`.`c20` trx id 1400 lock_mode X waiting
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 4; hex 8000000a; asc     ;;
 1: len 4; hex 8000000a; asc     ;;

*** (2) TRANSACTION:
TRANSACTION 1401, ACTIVE 196 sec inserting
mysql tables in use 1, locked 1
5 lock struct(s), heap size 1080, 3 row lock(s), undo log entries 1
MySQL thread id 53, OS thread handle 21636, query id 36916 localhost ::1 root update
insert into c20 values(8,8,8)
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 23 page no 4 n bits 80 index c of table `test_yjx`.`c20` trx id 1401 lock mode S
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 4; hex 8000000a; asc     ;;
 1: len 4; hex 8000000a; asc     ;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 23 page no 4 n bits 80 index c of table `test_yjx`.`c20` trx id 1401 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 4; hex 8000000a; asc     ;;
 1: len 4; hex 8000000a; asc     ;;

*** WE ROLL BACK TRANSACTION (1)

結果分爲3個部分:

  • (1) TRANSACTION 第一個事務的信息

    • WAITING FOR THIS LOCK TO BE GRANTED, 表示這個事務在等待的鎖資源
  • (2) TRANSACTION 第二個事務的信息

    • HOLDS THE LOCK(S) 顯示該事務持有哪些鎖
  • WE ROLL BACK TRANSACTION (1) 死鎖檢測的處理: 回滾了第一個事務

第一個事務的信息中:

  • update c20 set d=d+1 where c=10 致使死鎖時執行的最後一條 sql 語句
  • WAITING FOR THIS LOCK TO BE GRANTED

    • index c of table test_yjx.c20, 說明在等的是表 c20 的索引 c 上面的鎖
    • lock_mode X waiting 表示這個語句要本身加一個寫鎖, 當前狀態是等待中.
    • Record lock 說明這是一個記錄鎖
    • n_fields 2 表示這個記錄是兩列, 即 字段c 和 主鍵字段 id
    • 0: len 4; hex 8000000a; asc ;; 是第一個字段(即字段c), 值(忽略裏面的8)是十六進制 a, 即 10

      值 8000000a 中的 8...我也不理解爲何, 先忽略
    • 1: len 4; hex 8000000a; asc ;; 是第二個字段(即字段id), 值是 10
    • 上面兩行裏的 asc 表示, 接下來要打印出值裏面的"可打印字符", 但10不是可打印字符, 所以就顯示空格

      這裏不太理解
  • 第一個事務信息只顯示出等鎖的狀態, 在等待 (c=10, id=10) 這一行的鎖
  • 沒有顯示當前事務持有的鎖, 但能夠從第二個事務中推測出來.

第二個事務的信息中:

  • insert into c20 values(8,8,8) 致使死鎖時最後執行的語句
  • HOLDS THE LOCK(S)

    • index c of table test_yjx.c20 trx id 1401 lock mode S 表示鎖是在表 c20 的索引 c 上, 加的是讀鎖
    • hex 8000000a;表示這個事務持有 c=10 這個記錄鎖
  • WAITING FOR THIS LOCK TO BE GRANTED

    • index c of table test_yjx.c20 trx id 1401 lock_mode X locks gap before rec insert intention waiting

      • insert intention 表示試圖插入一個記錄, 這是一個插入意向鎖, 與間隙鎖產生鎖衝突
      • gap before rec 表示這是一個間隙鎖, 而不是記錄鎖.

補充:

  • lock_mode X waiting 表示 next-key lock
  • lock_mode X locks rec but not gap 表示只有行鎖
  • locks gap before rec 就是隻有間隙鎖

從上面信息能夠知道:

  • 第一個事務

    • 推測出持有間隙鎖 (?, 10)
    • 試圖更新 c=10 這一行, 但被索引c 的 行鎖 c=10 阻塞了
  • 第二個事務

    • 持有行鎖 c=10
    • 試圖插入 (8,8,8), 但被間隙鎖 (?, 10) 阻塞了
  • 檢測到死鎖時, InnoDB 認爲 第二個事務回滾成本更高, 所以回滾了第一個事務.

待整理

案例

-- 前提: 表 T 上有普通索引 k

-- 語句1
select * from T where k in (1,2,3,4,5);

-- 語句2
select * from T where k between 1 and 5;

這兩條語句的區別是:

語句1: 在索引k上進行了5次樹查找

語句2: 在索引k上進行了1次樹查找(k=1), 以後向右遍歷直到id>5

很明顯, 語句2 性能會更好.

三. WAL 機制及髒頁刷新

文章連接: https://segmentfault.com/a/11...

本部分主要來自: 極客時間《MySQL實戰45講》的第12講 - 爲何個人MySQL會「抖」一下

WAL(Write-Ahead Loggin)

WAL 是預寫式日誌, 關鍵點在於先寫日誌再寫磁盤.

在對數據頁進行修改時, 經過將"修改了什麼"這個操做記錄在日誌中, 而沒必要立刻將更改內容刷新到磁盤上, 從而將隨機寫轉換爲順序寫, 提升了性能.

但由此帶來的問題是, 內存中的數據頁會和磁盤上的數據頁內容不一致, 此時將內存中的這種數據頁稱爲 髒頁

Redo Log(重作日誌)

這裏的日誌指的是Redo Log(重作日誌), 這個日誌是循環寫入的.

它記錄的是在某個數據頁上作了什麼修改, 這個日誌會攜帶一個LSN, 同時每一個數據頁上也會記錄一個LSN(日誌序列號).

這個日誌序列號(LSN)能夠用於數據頁是不是髒頁的判斷, 好比說 write pos對應的LSN比某個數據頁的LSN大, 則這個數據頁確定是乾淨頁, 同時當髒頁提早刷到磁盤時, 在應用Redo Log能夠識別是否刷過並跳過.

這裏有兩個關鍵位置點:

  • write pos 當前記錄的位置, 一邊寫一邊後移.
  • checkpoint 是當前要擦除的位置, 擦除記錄前要把記錄更新到數據文件.

髒頁

當內存數據頁和磁盤數據頁內容不一致的時候, 將內存頁稱爲"髒頁".
內存數據頁寫入磁盤後, 兩邊內容一致, 此時稱爲"乾淨頁".
將內存數據頁寫入磁盤的這個操做叫作"刷髒頁"(flush).

InnoDB是以緩衝池(Buffer Pool)來管理內存的, 緩衝池中的內存頁有3種狀態:

  • 未被使用
  • 已被使用, 而且是乾淨頁
  • 已被使用, 而且是髒頁

因爲InnoDB的策略一般是儘可能使用內存, 所以長時間運行的數據庫中的內存頁基本都是被使用的, 未被使用的內存頁不多.

刷髒頁(flush)

時機

刷髒頁的時機:

  1. Redo Log寫滿了, 須要將 checkpoint 向前推動, 以便繼續寫入日誌

    checkpoint 向前推動時, 須要將推動區間涉及的全部髒頁刷新到磁盤.

  2. 內存不足, 須要淘汰一些內存頁(最久未使用的)給別的數據頁使用.

    此時若是是乾淨頁, 則直接拿來複用.

    若是是髒頁, 則須要先刷新到磁盤(直接寫入磁盤, 不用管Redo Log, 後續Redo Log刷髒頁時會判斷對應數據頁是否已刷新到磁盤), 使之成爲乾淨頁再拿來使用.

  3. 數據庫系統空閒時

    固然平時忙的時候也會盡可能刷髒頁.

  4. 數據庫正常關閉

    此時須要將全部髒頁刷新到磁盤.

InnoDB須要控制髒頁比例來避免Redo Log寫滿以及單次淘汰過多髒頁過多的狀況.

Redo Log 寫滿

這種狀況儘可能避免, 所以此時系統就不接受更新, 全部更新語句都會被堵住, 此時更新數爲0.

對於敏感業務來講, 這是不能接受的.

此時須要將 write pos 向前推動, 推動範圍內Redo Log涉及的全部髒頁都須要flush到磁盤中.

Redo Log設置太小或寫太慢的問題: 此時因爲Redo Log頻繁寫滿, 會致使頻繁觸發flush髒頁, 影響tps.

內存不足

這種狀況實際上是常態.

當從磁盤讀取的數據頁在內存中沒有內存時, 就須要到緩衝池中申請一個內存頁, 這時候根據LRU(最近最少使用算法)就須要淘汰掉一個內存頁來使用.

此時淘汰的是髒頁, 則須要將髒頁刷新到磁盤, 變成乾淨頁後才能複用.

注意, 這個過程 Write Pos 位置是不會向前推動的.

當一個查詢要淘汰的髒頁數太多, 會致使查詢的響應時間明顯變長.

策略

InnoDB 控制刷髒頁的策略主要參考:

  • 髒頁比例

    當髒頁比例接近或超過參數 innodb_max_dirty_pages_pct 時, 則會全力, 不然按照百分比.

  • redo log 寫盤速度

    N = (write pos 位置的日誌序號 - checkpoint對應序號), 當N越大, 則刷盤速度越快.

最終刷盤速度取上述二者中最快的.

參數 innodb_io_capacity

InnoDB 有一個關鍵參數: innodb_io_capacity, 該參數是用於告知InnoDB你的磁盤能力, 該值一般建議設置爲磁盤的寫IOPS.

該參數在 MySQL 5.5 及後續版本才能夠調整.

測試磁盤的IOPS:

fio -filename=/data/tmp/test_randrw -direct=1 -iodepth 1 -thread -rw=randrw -ioengine=psync -bs=16k -size=500M -numjobs=10 -runtime=10 -group_reporting -name=mytest
注意, 上面的 -filename 要指定具體的文件名, 千萬不要指定分區, 不然會致使分區不可用, 須要從新格式化.

innodb_io_capacity 通常參考 寫能力的IOPS

innodb_io_capacity 設置太低致使的性能問題案例:

MySQL寫入速度很慢, TPS很低, 可是數據庫主機的IO壓力並不大.

innodb_io_capacity 設置太小時, InnoDB會認爲磁盤性能差, 致使刷髒頁很慢, 甚至比髒頁生成速度還慢, 就會形成髒頁累積, 影響查詢和更新性能.

innodb_io_capacity 大小設置:

  • 配置小, 此時因爲InnoDB認爲你的磁盤性能差, 所以刷髒頁頻率會更高, 以此來確保內存中的髒頁比例較少.
  • 配置大, InnoDB認爲磁盤性能好, 所以刷髒頁頻率會下降, 抖動的頻率也會下降.

參數innodb_max_dirty_pages_pct

innodb_max_dirty_pages_pct 指的是髒頁比例上限(默認值是75%), 內存中的髒頁比例越是接近該值, 則InnoDB刷盤速度會越接近全力.

如何計算內存中的髒頁比例:

show global status like 'Innodb_buffer_pool_pages%';

髒頁比例 = 100 * Innodb_buffer_pool_pages_dirty / Innodb_buffer_pool_pages_total 的值

參數 innodb_flush_neighbors

當刷髒頁時, 若髒頁旁邊的數據頁也是髒頁, 則會連帶刷新, 注意這個機制是會蔓延的.

innodb_flush_neighbors=1 時開啓該機制, 默認是1, 但在 MySQL 8.0 中默認值是 0.

因爲機械硬盤時代的IOPS通常只有幾百, 該機制能夠有效減小不少隨機IO, 提升系統性能.

但在固態硬盤時代, 此時IOPS高達幾千, 此時IOPS每每不是瓶頸, "只刷本身"能夠更快執行完查詢操做, 減小SQL語句的響應時間.

若是Redo Log 設置過小

這裏有一個案例:

測試在作壓力測試時, 剛開始 insert, update 很快, 可是一會就變慢且響應延遲很高.

↑ 出現這種狀況大部分是由於 Redo Log 設置過小引發的.

由於此時 Redo Log 寫滿後須要將 checkpoint 前推, 此時須要刷髒頁, 可能還會連坐(innodb_flush_neighbors=1), 數據庫"抖"的頻率變高.

其實此時內存的髒頁比例可能還很低, 並無充分利用到大內存優點, 此時須要頻繁flush, 性能會變差.

同時, 若是Redo Log中存在change buffer, 一樣須要作相應的merge操做, 致使 change buffer 發揮不出做用.

對於實際場景:

在一臺高性能機器上配置了很是小的Redo Log.

此時因爲每次都很快寫滿Redo Log, 此時Write Pos會一直追着Check Point, 所以系統就會中止全部更新, 去推動 Check Point.

此時看到的現象就是: 磁盤壓力很小, 可是數據庫出現間歇性性能降低.

待整理

ORDER BY 的工做方式

關鍵字:

  • Using filesort, sort_buffer sort_buffer_size, 磁盤臨時文件
  • 全字段排序, OPTIMIZER_TRACE,sort_mode,num_of_tmp_files
  • rowid 排序, max_length_for_sort_data

原文: https://time.geekbang.org/col...

TODO

相關文章
相關標籤/搜索