日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > 数据库 >内容正文

数据库

Mysql深入浅出学习

發布時間:2023/12/10 数据库 32 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Mysql深入浅出学习 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

文章目錄

  • MySQL邏輯架構圖
  • redo log
    • 為什么要redo log?
  • binLog
    • 為什么會有兩份日志?
    • 兩種日志的區別
  • 簡單update語句執行流程
  • 兩階段提交
  • 事務隔離
    • 什么是事務?
    • 隔離性與隔離級別
    • 事務隔離的實現?
  • 索引
    • InnoDB的索引模型
    • 覆蓋索引
    • 最左前綴原則
    • 索引下推
    • 鎖的類型有哪些?
    • 全局鎖
    • 表級鎖
    • 行鎖
    • 死鎖和死鎖檢測
  • 普通索引和唯一索引,怎么選?
  • MySQL為什么有時候會選錯索引?
  • 怎么給字符串字段加索引?
  • SQL語句突然“變慢”?
  • 為什么表數據刪掉一半,表文件大小不變?
  • 為什么count(*)這么慢?
  • 不同的count用法
  • “order by”是怎么工作的?
    • 按字段排序
    • rowid排序
  • 查詢響應慢排查
    • MDL鎖
    • flush
    • 等行鎖
    • 一致性讀原因導致
    • 什么是幻讀?
    • 幻讀有什么問題?
    • 如何解決幻讀?
  • MySQL加鎖規則解讀
    • MySQL加鎖原則
    • 案例一:等值查詢間隙鎖
    • 案例二:非唯一索引等值鎖
    • 案例三:主鍵索引范圍鎖
    • 案例四:非唯一索引范圍鎖
    • 案例五:唯一索引范圍鎖bug
    • 案例六:非唯一索引上存在“等值”的例子
    • 案例七:非唯一索引上存在“等值”的例子
    • 案例八:死鎖例子
  • 索引失效反面案例
    • 條件字段函數操作
    • 隱式類型轉換
    • 隱式字符編碼轉換
  • 慢查詢處理技巧
    • 索引沒有設計好
    • 語句沒寫好
    • MySQL選錯了索引

學習自jdh莫老師

MySQL邏輯架構圖

redo log

為什么要redo log?

如果每一次的更新操作都需要寫進磁盤,然后磁盤也要找到對應的那條記錄,然后再更新,整個過程 IO 成本、查找成本都很高。為了解決這個問題,MySQL 的設計者采用了WAL技術(Write-Ahead Logging),關鍵點就是先寫日志,再寫磁盤,這個日志就是redo log。redo log是InnoDB引擎特有的日志。

binLog

為什么會有兩份日志?

redo log 是 InnoDB 引擎特有的日志,而 Server 層也有自己的日志,稱為 binlog(歸檔日志)。那為什么會有兩份日志呢?因為最開始 MySQL 里并沒有 InnoDB 引擎。MySQL 自帶的引擎是 MyISAM,但是 MyISAM 沒有 crash-safe 的能力,binlog 日志只能用于歸檔。而 InnoDB 是另一個公司以插件形式引入 MySQL 的,既然只依靠 binlog 是沒有 crash-safe 能力的,所以 InnoDB 使用另外一套日志系統——也就是 redo log 來實現 crash-safe 能力。

兩種日志的區別

redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 層實現的,所有引擎都可以使用。
redo log 是物理日志,記錄的是“在某個數據頁上做了什么修改”;binlog 是邏輯日志,記錄的是這個語句的原始邏輯,比如“給 ID=2 這一行的 c 字段加 1 ”。
redo log 是循環寫的,空間固定會用完;binlog 是可以追加寫入的。“追加寫”是指 binlog 文件寫到一定大小后會切換到下一個,并不會覆蓋以前的日志。

簡單update語句執行流程

update T set c=c+1 where ID=2

兩階段提交

由于 redo log 和 binlog 是兩個獨立的邏輯,如果不用兩階段提交,數據庫的狀態就有可能和用它的日志恢復出來的庫的狀態不一致。redo log 和 binlog 都可以用于表示事務的提交狀態,而兩階段提交就是讓這兩個狀態保持邏輯上的一致。假設不用兩階段提交,會出現如下情況

  • 先寫 redo log 后寫 binlog。假設在 redo log 寫完,binlog 還沒有寫完的時候,MySQL 進程異常重啟。由于我們前面說過的,redo log 寫完之后,系統即使崩潰,仍然能夠把數據恢復回來,所以恢復后這一行 c 的值是 1。但是由于 binlog 沒寫完就 crash 了,這時候 binlog 里面就沒有記錄這個語句。因此,之后備份日志的時候,存起來的 binlog 里面就沒有這條語句。然后你會發現,如果需要用這個 binlog 來恢復臨時庫的話,由于這個語句的 binlog 丟失,這個臨時庫就會少了這一次更新,恢復出來的這一行 c 的值就是 0,與原庫的值不同。先寫 binlog 后寫 redo log。如果在 binlog 寫完之后 crash,由于 redo log 還沒寫,崩潰恢復以后這個事務無效,所以這一行 c 的值是 0。但是 binlog 里面已經記錄了“把 c 從 0 改成 1”這個日志。所以,在之后用 binlog 來恢復的時候就多了一個事務出來,恢復出來的這一行 c 的值就是 1,與原庫的值不同。
  • 先寫 binlog 后寫 redo log。如果在 binlog 寫完之后 crash,由于 redo log
    還沒寫,崩潰恢復以后這個事務無效,所以這一行 c 的值是 0。但是 binlog 里面已經記錄了“把 c 從 0 改成1”這個日志。所以,在之后用 binlog 來恢復的時候就多了一個事務出來,恢復出來的這一行 c 的值就是 1,與原庫的值不同。

事務隔離

什么是事務?

簡單來說,事務就是要保證一組數據庫操作,要么全部成功,要么全部失敗。MySQL 是一個支持多引擎的系統,但并不是所有的引擎都支持事務。比如 MySQL 原生的 MyISAM 引擎就不支持事務,這也是 MyISAM 被 InnoDB 取代的重要原因之一。

隔離性與隔離級別

隔離性是事務ACID(Atomicity、Consistency、Isolation、Durability,即原子性、一致性、隔離性、持久性)中的I,當數據庫上有多個事務同時執行的時候,就可能出現臟讀(dirty read)、不可重復讀(non-repeatable read)、幻讀(phantom read)的問題,為了解決這些問題,就有了“隔離級別”的概念。SQL 標準的事務隔離級別包括:讀未提交(read uncommitted)、讀提交(read committed)、可重復讀(repeatable read)和串行化(serializable )
讀未提交是指,一個事務還沒提交時,它做的變更就能被別的事務看到。
讀提交是指,一個事務提交之后,它做的變更才會被其他事務看到。
可重復讀是指,一個事務執行過程中看到的數據,總是跟這個事務在啟動時看到的數據是一致的。當然在可重復讀隔離級別下,未提交變更對其他事務也是不可見的。
串行化,顧名思義是對于同一行記錄,“寫”會加“寫鎖”,“讀”會加“讀鎖”。當出現讀寫鎖沖突的時候,后訪問的事務必須等前一個事務執行完成,才能繼續執行。

Oracle 數據庫的默認隔離級別是“讀提交”,MySQL則是“可重復讀”

事務隔離的實現?

在 MySQL 中,實際上每條記錄在更新的時候都會同時記錄一條回滾操作。記錄上的最新值,通過回滾操作,都可以得到前一個狀態的值。假設一個值從 1 被按順序改成了 2、3、4,在回滾日志里面就會有類似下面的記錄。當前值是 4,但是在查詢這條記錄的時候,不同時刻啟動的事務會有不同的 read-view。如圖中看到的,在視圖 A、B、C 里面,這一個記錄的值分別是 1、2、4,同一條記錄在系統中可以存在多個版本,就是數據庫的多版本并發控制(MVCC)


索引

InnoDB的索引模型

在 MySQL 中,索引是在存儲引擎層實現的,所以并沒有統一的索引標準,即不同存儲引擎的索引的工作方式并不一樣。在 InnoDB 中,表都是根據主鍵順序以索引的形式存放的,這種存儲方式的表稱為索引組織表。InnoDB 使用了 B+ 樹索引模型,所以數據都是存儲在 B+ 樹中的。每一個索引在 InnoDB 里面對應一棵 B+ 樹。
create table T(id int primary key, k int not null, name varchar(16),index (k))engine=InnoDB;表中 R1~R5 的 (ID,k) 值分別為 (100,1)、(200,2)、(300,3)、(500,5) 和 (600,6),兩棵樹的示例示意圖如下。

覆蓋索引

在一個查詢里面,索引 k 已經“覆蓋了”我們的查詢需求,我們稱為覆蓋索引。由于覆蓋索引可以減少樹的搜索次數,顯著提升查詢性能,所以使用覆蓋索引是一個常用的性能優化手段。例如如果有一個高頻請求,要根據市民的身份證號查詢他的姓名,建立一個(身份證號、姓名)的聯合索引就有意義了,可以在這個高頻請求上用到覆蓋索引,不再需要回表查整行記錄,減少語句的執行時間。

最左前綴原則

B+ 樹這種索引結構,可以利用索引的“最左前綴”,來定位記錄。比如說已經有了(name,age)這個聯合索引,SQL 語句的條件是"where name like ‘張 %’"。這時,你也能夠用上這個索引,查找到第一個符合條件的記錄是 ID3,然后向后遍歷,直到不滿足條件為止。這個最左前綴可以是聯合索引的最左 N 個字段,也可以是字符串索引的最左 M 個字符。已經有了 (a,b) 這個聯合索引后,一般就不需要單獨在 a 上建立索引了

索引下推

MySQL 5.6 引入的索引下推優化(index condition pushdown), 可以在索引遍歷過程中,對索引中包含的字段先做判斷,直接過濾掉不滿足條件的記錄,減少回表次數。

鎖的類型有哪些?

根據加鎖的范圍,MySQL 里面的鎖大致可以分成全局鎖、表級鎖和行鎖三類。

全局鎖

全局鎖就是對整個數據庫實例加鎖。MySQL 提供了一個加全局讀鎖的方法,命令是 Flush tables with read lock (FTWRL)。當你需要讓整個庫處于只讀狀態的時候,可以使用這個命令,之后其他線程的以下語句會被阻塞:數據更新語句(數據的增刪改)、數據定義語句(包括建表、修改表結構等)和更新類事務的提交語句。全局鎖的典型使用場景是,做全庫邏輯備份。一般只有不支持事務的數據庫引擎才會用到這種鎖,如MyISAM

表級鎖

MySQL 里面表級別的鎖有兩種:一種是表鎖,一種是元數據鎖(meta data lock,MDL)。表鎖的語法是 lock tables … read/write。在 MySQL 5.5 版本中引入了 MDL鎖,每執行一條DML、DDL語句時都會申請MDL鎖,DML操作需要MDL讀鎖,DDL操作需要MDL寫鎖(MDL加鎖過程是系統自動控制,無法直接干預,讀讀共享,讀寫互斥,寫寫互斥)。MDL讀鎖之間不互斥,因此可以有多個線程同時對一張表增刪改查。MDL讀寫鎖之間、寫鎖之間是互斥的,用來保證變更表結構操作的安全性。因此,如果有兩個線程要同時給一個表加字段,其中一個要等另一個執行完才能開始執行。

行鎖

MySQL 的行鎖是在引擎層由各個引擎自己實現的。但并不是所有的引擎都支持行鎖,比如 MyISAM 引擎就不支持行鎖。不支持行鎖意味著并發控制只能使用表鎖,對于這種引擎的表,同一張表上任何時刻只能有一個更新在執行,這就會影響到業務并發度。InnoDB 是支持行鎖的,這也是 MyISAM 被 InnoDB 替代的重要原因之一。
在 InnoDB 事務中,行鎖是在需要的時候才加上的,但并不是不需要了就立刻釋放,而是要等到事務結束時才釋放。這個就是兩階段鎖協議。如果事務中需要鎖多個行,要把最可能造成鎖沖突、最可能影響并發度的鎖盡量往后放。

死鎖和死鎖檢測

當并發系統中不同線程出現循環資源依賴,涉及的線程都在等待別的線程釋放資源時,就會導致這幾個線程都進入無限等待的狀態,稱為死鎖。針對死鎖,InnoDB有兩種解決策略
一種策略是,直接進入等待,直到超時。這個超時時間可以通過參數 innodb_lock_wait_timeout 來設置。在 InnoDB 中,innodb_lock_wait_timeout 的默認值是 50s,意味著如果采用這個策略,當出現死鎖以后,第一個被鎖住的線程要過 50s 才會超時退出,然后其他線程才有可能繼續執行。對于在線服務來說,這個等待時間往往是無法接受的。但是超時時間設置太短的話,會出現很多誤傷。
另一種策略是,發起死鎖檢測,發現死鎖后,主動回滾死鎖鏈條中的某一個事務,讓其他事務得以繼續執行。將參數 innodb_deadlock_detect 設置為 on,表示開啟這個邏輯。

普通索引和唯一索引,怎么選?

這兩類索引在查詢能力上是沒差別的,主要考慮的是對更新性能的影響。對唯一索引來說,所有的更新操作都要先判斷這個操作是否違反唯一性約束。比如,要插入 (4,400) 這個記錄,就要先判斷現在表中是否已經存在 k=4 的記錄,而這必須要將數據頁讀入內存才能判斷。如果都已經讀入到內存了,那直接更新內存會更快,就沒必要使用 change buffer 了。因此,唯一索引的更新就不能使用 change buffer,實際上也只有普通索引可以使用。將數據從磁盤讀入內存涉及隨機 IO 的訪問,是數據庫里面成本最高的操作之一。
綜上所述,建議盡量選擇普通索引,如果所有的更新后面,都馬上伴隨著對這個記錄的查詢,應該關閉 change buffer。而在其他情況下,change buffer 都能提升更新性能。在實際使用中,普通索引和 change buffer 的配合使用,對于數據量大的表的更新優化還是很明顯的。

MySQL為什么有時候會選錯索引?

隨著表數據的不斷增刪,可能會出現MySQL 選錯索引,根本原因是MySQL沒能準確地判斷出掃描行數或者錯誤地計算了執行成本,這種是低概率事件,但是某些場景下有可能觸發MySQL的這個bug,解決方法一般有以下幾種
由于索引統計信息不準確導致的問題,可以用 analyze table 來解決
采用 force index 強行選擇一個索引,不過這個弊端也很明顯,一來這么寫不優雅,二來如果索引改了名字,這個語句也得改,顯得很麻煩。而且如果以后遷移到別的數據庫的話,這個語法還可能會不兼容。
修改語句,引導 MySQL 使用我們期望的索引。 如把select * from t where (a between 1 and 1000) and (b between 50000 and 100000) order by b limit 1改成select * from t where (a between 1 and 1000) and (b between 50000 and 100000) order by b,a limit 1
新建一個更合適的索引,來提供給優化器做選擇,或刪掉誤用的索引。

怎么給字符串字段加索引?

主要看該字段的區分度是不是足夠大,建索引是否有必要。如果有必要,建索引有兩種方法,一種是整個字段索引,如果字段值普遍較長,則可能會消耗較大的存儲空間,另一種是使用前綴索引,定義好長度,就可以做到既節省空間,又不用額外增加太多的查詢成本。如:alter table SUser add index index2(email(7))。那問題來了,當要給字符串創建前綴索引時,有什么方法能夠確定應該使用多長的前綴呢?
select count(distinct email) as L from SUser;
select count(distinct left(email,4))as L4, count(distinct left(email,5))as L5, count(distinct left(email,6))as L6, count(distinct left(email,7))as L7,from SUser;

但是使用前綴索引,就用不上覆蓋索引對查詢性能的優化了

小技巧:倒序存儲和使用 hash 字段

SQL語句突然“變慢”?

InnoDB 在處理更新語句的時候,只做了寫日志這一個磁盤操作,這個日志叫作 redo log(重做日志)。平時執行很快的更新操作,其實就是在寫內存和日志,而 MySQL 偶爾“抖”一下的那個瞬間,可能就是在刷臟頁(flush)。MySQL在下面這些情境下會做flush操作
InnoDB 的 redo log 寫滿了。這時候系統會停止所有更新操作,把 checkpoint 往前推進,redo log 留出空間可以繼續寫。這種情況是 InnoDB 要盡量避免的。因為出現這種情況的時候,整個系統就不能再接受更新了
系統內存不足。當需要新的內存頁,而內存不夠用的時候,就要淘汰一些數據頁,空出內存給別的數據頁使用。如果淘汰的是“臟頁”,就要先將臟頁寫到磁盤。這種情況其實是常態。但是出現以下這兩種情況,都是會明顯影響性能的
一個查詢要淘汰的臟頁個數太多,會導致查詢的響應時間明顯變長;
日志寫滿,更新全部堵住,寫性能跌為 0,這種情況對敏感業務來說,是不能接受的。
要用到 innodb_io_capacity 這個參數了,它會告訴 InnoDB 數據庫主機的磁盤能力。這個值建議設置成磁盤的 IOPS。磁盤的 IOPS 可以通過 fio 這個工具來測試。并且平時要多關注臟頁比例(innodb_max_dirty_pages_pct),不要讓它經常接近 75%。如果是SSD盤,innodb_flush_neighbors參數設置成0
MySQL 認為系統“空閑”的時候。
MySQL 正常關閉

為什么表數據刪掉一半,表文件大小不變?

delete 命令其實只是把記錄的位置,或者數據頁標記為了“可復用”,但磁盤文件的大小是不會變的。也就是說,通過 delete 命令是不能回收表空間的。這些可以復用,而沒有被使用的空間,看起來就像是“空洞”。不止是刪除數據會造成空洞,插入數據也會。也就是說,經過大量增刪改的表,都是可能是存在空洞的。所以,如果能夠把這些空洞去掉,就能達到收縮表空間的目的。而重建表,就可以達到這樣的目的。
MySQL 5.6 版本開始引入的 Online DDL,對表重建操作流程做了優化,可以通過執行alter table t engine=InnoDB對表進行重建。通過日志文件記錄和重放操作,在重建表的過程中,允許對表做增刪改操作。重建方法都會掃描原表數據和構建臨時文件。對于很大的表來說,這個操作是很消耗 IO 和 CPU 資源的。因此,如果是線上服務,需要很小心地控制操作時間。

為什么count(*)這么慢?

為什么 InnoDB 不跟 MyISAM 一樣,也把記錄總數存起來呢,查詢的時候直接返回?這是因為即使是在同一個時刻的多個查詢,由于多版本并發控制(MVCC)的原因,InnoDB 表“應該返回多少行”也是不確定的。對于 count(*) 這樣的操作,遍歷哪個索引樹得到的結果邏輯上都是一樣的。因此,MySQL 優化器會找到最小的那棵樹來遍歷,但是如果表的數據十分巨大,因為是整棵樹遍歷,所以性能會非常差。如果有高頻需要獲取大表記錄總數的場景,需要自己進行計數。
比如可以在數據庫里面建一張表,把高頻需要獲取記錄總數的大表的記錄數自己進行維護,獲取總數時候直接從該表獲取。

不同的count用法

對于 count(主鍵 id) 來說,InnoDB 引擎會遍歷整張表,把每一行的 id 值都取出來,返回給 server 層。server 層拿到 id 后,判斷是不可能為空的,就按行累加。
對于 count(1) 來說,InnoDB 引擎遍歷整張表,但不取值。server 層對于返回的每一行,放一個數字“1”進去,判斷是不可能為空的,按行累加。count(1) 執行得要比 count(主鍵 id) 快。因為從引擎返回 id 會涉及到解析數據行,以及拷貝字段值的操作。
對于 count(字段) 來說,如果這個“字段”是定義為 not null 的話,一行行地從記錄里面讀出這個字段,判斷不能為 null,按行累加;如果這個“字段”定義允許為 null,那么執行的時候,判斷到有可能是 null,還要把值取出來再判斷一下,不是 null 才累加。
所以結論是:按照效率排序的話,count(字段)<count(主鍵 id)<count(1)≈count(),所以建議盡量使用 count()。

“order by”是怎么工作的?

按字段排序

select city,name,age from t where city='杭州' order by name limit 1000 ;


圖中“按 name 排序”這個動作,可能在內存中完成,也可能需要使用外部排序,這取決于排序所需的內存和參數sort_buffer_size。sort_buffer_size,就是 MySQL 為排序開辟的內存(sort_buffer)的大小。如果要排序的數據量小于 sort_buffer_size,排序就在內存中完成。但如果排序數據量太大,內存放不下,則不得不利用磁盤臨時文件輔助排序。

rowid排序

剛說的按字段排序算法有一個問題,就是如果查詢要返回的字段很多的話,那么 sort_buffer 里面要放的字段數太多,這樣內存里能夠同時放下的行數很少,要分成很多個臨時文件,排序的性能會很差。所以rowid排序的思想是減少查詢字段的數量,節省排序內存,但是缺點是有可能需要回到原表去取數據。對于 InnoDB 表來說,rowid 排序會要求回表多造成磁盤讀,因此不會被優先選擇。

查詢響應慢排查

MDL鎖

使用 show processlist 命令查看 Waiting for table metadata lock 的示意圖。MySQL 啟動時需要設置 performance_schema=on,通過查詢 sys.schema_table_lock_waits 這張表,就可以直接找出造成阻塞的 process id,把這個連接用 kill 命令斷開即可。

flush

出現 Waiting for table flush 狀態的可能情況是:有一個 flush tables 命令被別的語句堵住了,然后它又堵住了select 語句。

等行鎖

select * from t where id=1 lock in share mode;由于訪問 id=1 這個記錄時要加讀鎖,如果這時候已經有一個事務在這行記錄上持有一個寫鎖,select 語句就會被堵住。這個問題并不難分析,但問題是怎么查出是誰占著這個寫鎖。如果是 MySQL 5.7 版本,可以通過 sys.innodb_lock_waits 表查到。查詢語句:select * from t sys.innodb_lock_waits where locked_table=‘test.t’\G

一致性讀原因導致

session A 先用 start transaction with consistent snapshot 命令啟動了一個事務,之后 session B 才開始執行 update 語句。session B 更新完 100 萬次,生成了 100 萬個回滾日志 (undo log)。
帶 lock in share mode 的 SQL 語句,是當前讀,因此會直接讀到 1000001 這個結果,所以速度很快;而 select * from t where id=1 這個語句,是一致性讀,因此需要從 1000001 開始,依次執行 undo log,執行了 100 萬次以后,才將 1 這個結果返回。所以后者的執行時間大概是前者的4000倍

什么是幻讀?

在可重復讀隔離級別下,普通的查詢是快照讀,是不會看到別的事務插入的數據的。因此,幻讀在“當前讀”下才會出現。上面 session B 的修改結果,被 session A 之后的 select 語句用“當前讀”看到,不能稱為幻讀。幻讀僅專指“新插入的行”。

幻讀有什么問題?

  • 語義被破壞

session A 在 T1 時刻就聲明了,“我要把所有 d=5 的行鎖住,不準別的事務進行讀寫操作”。而實際上,這個語義被破壞了。

  • 數據一致性問題

如何解決幻讀?

產生幻讀的原因是,行鎖只能鎖住行,但是新插入記錄這個動作,要更新的是記錄之間的“間隙”。因此,為了解決幻讀問題,InnoDB 只好引入新的鎖,也就是間隙鎖 (Gap Lock)。顧名思義,間隙鎖,鎖的就是兩個值之間的空隙。跟間隙鎖存在沖突關系的,是“往這個間隙中插入一個記錄”這個操作。間隙鎖之間都不存在沖突關系。間隙鎖的引入,雖然解決了幻讀的問題,但是可能會導致同樣的語句鎖住更大的范圍,這其實是影響了并發度的。間隙鎖是在可重復讀隔離級別下才會生效的。所以,如果把隔離級別設置為讀提交的話,就沒有間隙鎖了。但同時,要解決可能出現的數據和日志不一致問題,需要把 binlog 格式設置為 row。這,也是現在不少公司使用的配置組合。
next-key lock:間隙鎖和行鎖合稱 next-key lock,每個 next-key lock 是前開后閉區間。表 t(下圖上方數字是主鍵值) 初始化以后,如果用 select * from t for update 要把整個表所有記錄鎖起來,就形成了 7 個 next-key lock,分別是 (-∞,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20, 25]、(25, +supremum]。

MySQL加鎖規則解讀

MySQL加鎖原則

原則 1:加鎖的基本單位是 next-key lock。
原則 2:查找過程中訪問到的對象才會加鎖。
優化 1:索引上的等值查詢,給唯一索引加鎖的時候,next-key lock 退化為行鎖。
優化 2:索引上的等值查詢,向右遍歷時且最后一個值不滿足等值條件的時候,next-key lock 退化為間隙鎖。
一個 bug:唯一索引上的范圍查詢會訪問到不滿足條件的第一個值為止。

案例一:等值查詢間隙鎖

由于表 t 中沒有 id=7 的記錄,加鎖單位是 next-key lock(),session A 加鎖范圍就是 (5,10];這是一個等值查詢 (id=7),而 id=10 不滿足查詢條件,根據優化2,next-key lock 退化成間隙鎖,因此最終加鎖的范圍是 (5,10)。

案例二:非唯一索引等值鎖

session A 要給索引 c 上 c=5 的這一行加上讀鎖。根據原則 1,加鎖單位是 next-key lock,因此會給 (0,5]加上 next-key lock。要注意 c 是普通索引,因此僅訪問 c=5 這一條記錄是不能馬上停下來的,需要向右遍歷,查到 c=10 才放棄。根據原則 2,訪問到的都要加鎖,因此要給 (5,10]加 next-key lock。但是同時這個符合優化 2:等值判斷,向右遍歷,最后一個值不滿足 c=5 這個等值條件,因此退化成間隙鎖 (5,10)。根據原則 2 ,只有訪問到的對象才會加鎖,這個查詢使用覆蓋索引,并不需要訪問主鍵索引,所以主鍵索引上沒有加任何鎖,這就是為什么 session B 的 update 語句可以執行完成。但 session C 要插入一個 (7,7,7) 的記錄,就會被 session A 的間隙鎖 (5,10) 鎖住。在這個例子中,lock in share mode 只鎖覆蓋索引,但是如果是 for update 就不一樣了。 執行 for update 時,系統會認為接下來要更新數據,因此會順便給主鍵索引上滿足條件的行加上行鎖。這個例子說明,鎖是加在索引上的。如果要用 lock in share mode 來給行加讀鎖避免數據被更新的話,就必須得繞過覆蓋索引的優化。

案例三:主鍵索引范圍鎖

開始執行的時候,要找到第一個 id=10 的行,因此本該是 next-key lock(5,10]。 根據優化 1, 主鍵 id 上的等值條件,退化成行鎖,只加了 id=10 這一行的行鎖。范圍查找就往后繼續找,找到 id=15 這一行停下來,因此需要加 next-key lock(10,15]。所以,session A 這時候鎖的范圍就是主鍵索引上,行鎖 id=10 和 next-key lock(10,15]。這樣,session B 和 session C 的結果就能理解了。

案例四:非唯一索引范圍鎖

session A 用字段 c 來判斷,加鎖規則跟案例三唯一的不同是:在第一次用 c=10 定位記錄的時候,索引 c 上加了 (5,10]這個 next-key lock 后,由于索引 c 是非唯一索引,沒有優化規則,也就是說不會蛻變為行鎖,因此最終 sesion A 加的鎖是,索引 c 上的 (5,10] 和 (10,15] 這兩個 next-key lock。

案例五:唯一索引范圍鎖bug

session A 是一個范圍查詢,按照原則 1 的話,應該是索引 id 上只加 (10,15]這個 next-key lock,并且因為 id 是唯一鍵,所以循環判斷到 id=15 這一行就應該停止了。但是實現上,InnoDB 會往前掃描到第一個不滿足條件的行為止,也就是 id=20。而且由于這是個范圍掃描,因此索引 id 上的 (15,20]這個 next-key lock 也會被鎖上。

案例六:非唯一索引上存在“等值”的例子

session A 在遍歷的時候,先訪問第一個 c=10 的記錄。同樣地,根據原則 1,這里加的是 (c=5,id=5) 到 (c=10,id=10) 這個 next-key lock。然后,session A 向右查找,直到碰到 (c=15,id=15) 這一行,循環才結束。根據優化 2,這是一個等值查詢,向右查找到了不滿足條件的行,所以會退化成 (c=10,id=10) 到 (c=15,id=15) 的間隙鎖。也就是說,這個 delete 語句在索引 c 上的加鎖范圍,就是下圖中藍色區域覆蓋的部分。

案例七:非唯一索引上存在“等值”的例子

session A 的 delete 語句加了 limit 2。表 t 里 c=10 的記錄其實只有兩條,因此加不加 limit 2,刪除的效果都是一樣的,但是加鎖的效果卻不同。可以看到,session B 的 insert 語句執行通過了,跟案例六的結果不同。這是因為,案例七里的 delete 語句明確加了 limit 2 的限制,因此在遍歷到 (c=10, id=30) 這一行之后,滿足條件的語句已經有兩條,循環就結束了。因此,索引 c 上的加鎖范圍就變成了從(c=5,id=5) 到(c=10,id=30) 這個前開后閉區間,如下圖所示:
這個例子的指導意義就是,在刪除數據的時候盡量加 limit。這樣不僅可以控制刪除數據的條數,讓操作更安全,還可以減小加鎖的范圍。

案例八:死鎖例子

session A 啟動事務后執行查詢語句加 lock in share mode,在索引 c 上加了 next-key lock(5,10] 和間隙鎖 (10,15);session B 的 update 語句也要在索引 c 上加 next-key lock(5,10] ,進入鎖等待;然后 session A 要再插入 (8,8,8) 這一行,被 session B 的間隙鎖鎖住。由于出現了死鎖,InnoDB 讓 session B 回滾。你可能會問,session B 的 next-key lock 不是還沒申請成功嗎? 其實是這樣的,session B 的“加 next-key lock(5,10] ”操作,實際上分成了兩步,先是加 (5,10) 的間隙鎖,加鎖成功;然后加 c=10 的行鎖,這時候才被鎖住的。

索引失效反面案例

條件字段函數操作

select count(*) from tradelog where month(t_modified)=7;
對索引字段做函數操作,可能會破壞索引值的有序性,因此優化器就決定放棄走樹搜索功能而遍歷整個索引的所有值。需要注意的是,即使是對于不改變有序性的函數,也不會考慮使用索引。比如,對于 select * from tradelog where id + 1 = 10000 這個 SQL 語句,這個加 1 操作并不會改變有序性,但是 MySQL 優化器還是不能用 id 索引快速定位到 9999 這一行。所以,需要在寫 SQL 語句的時候,手動改寫成 where id = 10000 -1 才可以。

隱式類型轉換

select * from tradelog where tradeid=110717;
tradeid 的字段類型是 varchar(32),而輸入的參數卻是整型,所以需要做類型轉換,導致走了全表掃描。MySQL默認是將字符串轉換成數字進行比較,所以上面的語句等價于select * from tradelog where CAST(tradid AS signed int) = 110717;觸發了上面說到的規則:對索引字段做函數操作,優化器會放棄走樹搜索功能。

隱式字符編碼轉換

select d.* from tradelog l, trade_detail d where d.tradeid=l.tradeid and l.id=2; 由于tradelog表的tradeid字符集編碼是utf8mb4,而trade_detail表的tradeid是utf8,utf8mb4是utf8的超集,所以發生了隱式轉換,該語句等價于select d.* from tradelog l, trade_detail d where d.tradeid=CONVERT(d.traideid USING utf8mb4) and l.id=2; 觸發了上面說到的規則:對被驅動表索引字段做函數操作,優化器會放棄走樹搜索功能。
思考題:不調整字段編碼的情況下怎么改才能走對索引?

慢查詢處理技巧

慢查詢導致性能問題的三種可能情況如下

索引沒有設計好

這種場景一般就是通過緊急創建索引來解決。MySQL 5.6 版本以后,創建索引都支持 Online DDL 了,對于那種高峰期數據庫已經被這個語句打掛了的情況,最高效的做法就是直接執行 alter table 語句。比較理想的是能夠在備庫先執行。假設你現在的服務是一主一備,主庫 A、備庫 B,這個方案的大致流程是這樣的:在備庫 B 上執行 set sql_log_bin=off,也就是不寫 binlog,然后執行 alter table 語句加上索引;執行主備切換;這時候主庫是 B,備庫是 A。在 A 上執行 set sql_log_bin=off,然后執行 alter table 語句加上索引。

語句沒寫好

這種就要通過分析explain命令分析語句的執行計劃,并進行優化,典型反面案例參考上一頁的索引失效反面案例。MySQL 5.7 提供了 query_rewrite 功能,可以把輸入的一種語句改寫成另外一種模式,當出現線上業務bug導致QPS暴漲,數據庫也面臨宕機風險的時候,可以用這個把壓力最大的SQL直接重寫為“select 1”返回,以解燃眉之急

MySQL選錯了索引

這種是低概率事件,具體解決方案參考上面的“MySQL為什么有時候會選錯索引”這節

慢查詢導致性能問題的三種可能情況,實際上出現最多的是前兩種,一般在測試環境可以通過以下手段盡量規避
上線前,在測試環境,把慢查詢日志(slow log)打開,并且把 long_query_time 設置成 0,確保每個語句都會被記錄入慢查詢日志;
在測試表里插入模擬線上的數據,做一遍回歸測試;觀察慢查詢日志里每類語句的輸出,特別留意 Rows_examined 字段是否與預期一致。

總結

以上是生活随笔為你收集整理的Mysql深入浅出学习的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。