Sql Bad Case
- 條件字段函數操作
- 對索引字段做函數操作,可能會破壞索引值的有序性,因此優化器就決定放棄走樹搜索功能
- 栗子:month () 函數、where id + 1 = 10000 等
- 隱式類型轉換
- 在 MySQL 中,字符串和數字做比較的話,是將字符串轉換成數字
- 栗子:select “10” > 9(返回 1 代表做數字比較
- 隱式字符編碼轉換
- utf8mb4 是 utf8 的超集
- 栗子
- select * from trade_detail where tradeid=$L2.tradeid.value; (原SQL
- select * from trade_detail where CONVERT(traideid USING utf8mb4)=$L2.tradeid.value;
- CONVERT ( ) 函數:把輸入的字符串轉成 utf8mb4 字符集
- 連接過程中要求在被驅動表的索引字段上加函數操作(導致對被驅動表做全表掃描的原因
- 破局之道
- 統一字符集(若數據量較大且業務上暫時不允許做 DDL
- 修改SQL:…. where d.tradeid=CONVERT(l.tradeid USING utf8) and l.id=2;
Slow Situation
-
查詢長時間不返回
-
等 MDL 鎖
- 現在有一個線程正在表 t 上請求或者持有 MDL 寫鎖,把 select 語句堵住
-
查獲加表鎖的線程 id
-
-
等 FLUSH
-
Waiting for table flush 狀態示意圖
- MySQL 裏面對錶做 flush 操作的用法
# 只關閉表 t flush tables t with read lock; # 關閉 MySQL 裏所有打開的表 flush tables with read lock
- MySQL 裏面對錶做 flush 操作的用法
-
等行鎖
- 加鎖讀方式:select * from t where id=1 lock in share mode(for update
- 查看鎖等待信息:select * from t sys.innodb_lock_waits where locked_table=
xxx
-
-
查詢慢
- 掃描行數多
- 栗子:select * from t where c=50000 limit 1;
- 字段 c 上沒有索引,這個語句只能走 id 主鍵順序掃描,因此需要掃描 5 萬行
- 數據量與執行時間呈線性增漲
- 栗子:select * from t where c=50000 limit 1;
- 一致性讀
-
栗子
- select * from t where id=1;(掃描行數 1 ,執行時長 800 毫秒
- select * from t where id=1 lock in share mode;(掃描行數 1,執行時長 0.2 毫秒
-
id=1 的數據狀態
- session B 更新完 100 萬次,生成了 100 萬個回滾日誌 (undo log)
- 一致性讀需要從 1000001 開始依次執行 undo log,執行了 100 萬次後,纔將結果返回
-
- 掃描行數多
幻讀
-
特別說明
- 幻讀在 “當前讀” 下才會出現(普通查詢是快照讀,看不到其他事物插入的數據
- 當前讀的規則,就是要能讀到所有已經提交的記錄的最新值
- 幻讀僅專指 “新插入的行”(辯證觀點看待
- 幻讀在 “當前讀” 下才會出現(普通查詢是快照讀,看不到其他事物插入的數據
-
鎖的設計是爲了保證數據的一致性
- 不止是數據庫內部數據狀態在此刻的一致性,還包含了數據和日誌在邏輯上的一致性
-
爲了解決幻讀問題,InnoDB 只好引入新的鎖,也就是間隙鎖 (Gap Lock)
-
間隙鎖,鎖的就是兩個值之間的空隙
-
間隙鎖和行鎖合稱 next-key lock,每個 next-key lock 是前開後閉區間
-
-
間隙鎖和 next-key lock 的引入帶來了一些 “困擾”
-
間隙鎖導致的死鎖問題(間隙鎖與間隙鎖兼容、間隙鎖與插入意向鎖衝突
-
間隙鎖的引入,可能會導致同樣的語句鎖住更大的範圍,這其實是影響了併發度的
-
鎖規則
-
我總結的加鎖規則裏面,包含了兩個 “原則”、兩個 “優化” 和一個 “bug”
- 原則 1:加鎖的基本單位是 next-key lock。希望你還記得,next-key lock 是前開後閉區間
- 原則 2:查找過程中訪問到的對象纔會加鎖。
- 優化 1:索引上的等值查詢,給唯一索引加鎖的時候,next-key lock 退化爲行鎖
- 優化 2:索引上的等值查詢,向右遍歷時且最後一個值不滿足等值條件的時,next-key lock 退化爲間隙鎖
- 一個 bug:唯一索引上的範圍查詢會訪問到不滿足條件的第一個值爲止
-
關於覆蓋索引上的鎖
- 栗子:select id from t where c = 5 lock in share mode;
- lock in share mode 只鎖覆蓋索引, for update 就會順便給主鍵索引上滿足條件的行加上行鎖
-
主鍵索引範圍鎖
- session A 這時候鎖的範圍就是主鍵索引上,行鎖 id=10 和 next-key lock (10,15]
- 查找 id=10 行時是當做等值查詢來判斷的,而向右掃描到 id=15 的時候,用的是範圍查詢判斷
-
唯一索引範圍鎖 bug
- InnoDB 會往前掃描到第一個不滿足條件的行爲止,也就是 id=20
- 由於這是個範圍掃描,因此索引 id 上的 (15,20]
- InnoDB 會往前掃描到第一個不滿足條件的行爲止,也就是 id=20
-
limit 語句加鎖
- 栗子:delete from t where c = 10 / delete from t where c = 10 limit 2
- 前者加鎖範圍:(5,15)後置加鎖範圍:(5,10)
- 在刪除數據時儘量加 limit,不僅可以控制刪除數據的條數,讓操作更安全,還可以減小加鎖的範圍
- 栗子:delete from t where c = 10 / delete from t where c = 10 limit 2
-
next-key lock 加鎖時,要分成間隙鎖和行鎖兩段來執行的
-
讀提交隔離級別下的一個優化:語句執行過程中加上的行鎖,在語句執行完成後,就要把 “不滿足條件的行” 上的行鎖直接釋放了,不需要等到事務提交
show time
-
背景:業務高峯期生產環境的 MySQL 壓力太大,沒法正常響應,需要短期內、臨時性地提升一些性能
- 這就是爲什麼這章叫「show time」的原因 ( it’s your show time
- 這些處理手段中,既包括了粗暴地拒絕連接和斷開連接,也有通過重寫語句來繞過一些坑的方法
- 既有臨時的高危方案,也有未雨綢繆的、相對安全的預案
- 連接異常斷開是常有的事,你的代碼裏要有正確地重連並重試的機制
-
短連接風暴
-
如果使用的是短連接,在業務高峯期的時候,就可能出現連接數突然暴漲的情況
- 在機器負載比較高的時候,處理現有請求的時間變長,每個連接保持的時間也更長
-
MySQL 建立連接的過程,成本是很高的
- 除了正常的網絡連接三次握手外,還需要做登錄權限判斷和獲得這個連接的數據讀寫權限
-
max_connections 參數,用來控制一個 MySQL 實例同時存在的連接數的上限
- 系統就會拒絕接下來的連接請求,並報錯提示 “Too many connections”
- 設計 max_connections 這個參數的目的是想保護 MySQL(不要無腦調大數值
-
破局之道
-
先處理掉那些佔着連接但是不工作的線程
- max_connections 的計算,不是看誰在 running,是隻要連着就佔用一個計數位置
- 對於那些不需要保持的連接,我們可以通過 kill connection + id 主動踢掉
- 優先斷開事務外空閒太久的連接,如果還不夠,再考慮斷開事務內空閒太久的連接
- 服務端主動斷開後,客戶端會在發起下一個請求時收到「失去 MySQL 連接」報錯
-
減少連接過程的消耗,讓數據庫跳過權限驗證階段
-
跳過權限驗證的方法是:重啓數據庫,並使用–skip-grant-tables 參數啓動
- 跳過所有的權限驗證階段,包括連接過程和語句執行過程在內
-
不建議使用此方案,尤其你的庫外網可訪問的場景下
-
在 MySQL 8.0 版本里啓用–skip-grant-tables 參數後,默認把 --skip-networking 參數打開
- 表示這時候數據庫只能被本地的客戶端連接
-
-
-
-
慢查詢性能問題
- 引發性能問題的慢查詢,大體有以下三種可能
- 索引沒有設計好
- 通過緊急創建索引來解決(Online DDL、gh-ost
- SQL 語句沒寫好
- MySQL 5.7 提供了 query_rewrite 功能,可以把輸入的一種語句改寫成另外一種模式
- MySQL 選錯了索引
- force index + query_rewrite
- 索引沒有設計好
- 實際上出現最多的是前兩種,通過提前做好預防措施遠好於緊急救火
- 上線前回歸測試(通過 slow log 、Rows_examined 等
- 引發性能問題的慢查詢,大體有以下三種可能
-
QPS 突增問題
- 業務突然出現高峯或應用程序 bug,導致某個語句的 QPS 突然暴漲,MySQL 壓力過大影響服務
- 最理想的情況是讓業務把這個功能下掉,服務自然就會恢復
- 如果從數據庫端處理的話,針對不同的場景有對應的方法可以用
- 一種是由全新業務的 bug 導致的(可以從數據庫端直接把白名單去掉
- 這個新功能使用的是單獨的數據庫用戶(用管理員賬號把這個用戶刪掉,然後斷開現有連接
- 如果以上都不能則通過處理語句來限制(查詢重寫功能,將壓力最大的SQL改寫爲 select 1
- 風險極高,可能造成誤傷,而且會導致後面的業務邏輯一起失敗(優先級最低
- 其實方案 1 和 2 都要依賴於規範的運維體系:虛擬化、白名單機制、業務賬號分離
- 由此可見,更多的準備,往往意味着更穩定的系統
日誌完整性
- 前景概要:只要 redo log 和 binlog 保證持久化到磁盤,就能確保 MySQL 異常重啓後,數據可以恢復
- WAL 機制主要得益於兩個方面
- redo log 和 binlog 都是順序寫,磁盤的順序寫比隨機寫速度要快
- 組提交機制,可以大幅度降低磁盤的 IOPS 消耗
- MySQL 出現了性能瓶頸(IO上),可以考慮以下三種方法
- 組提交(參數 binlog_group_commit_sync_delay、binlog_group_commit_sync_no_delay_count
- 減少 binlog 的寫盤次數,可能會增加語句的響應時間,但沒有丟失數據的風險
- 將 sync_binlog 設置爲大於 1 的值(比較常見是 100~1000),但主機掉電時會丟 binlog 日誌
- 將 innodb_flush_log_at_trx_commit 設置爲 2,但主機掉電的時候會丟數據
- 不建議設置 0 ,因爲 MySQL 異常重啓就會丟數據 並且 寫到到 page cache 速度本來就很快
- 組提交(參數 binlog_group_commit_sync_delay、binlog_group_commit_sync_no_delay_count
- binlog 的寫入機制
-
事務執行過程中,先把日誌寫到 binlog cache,事務提交時再把 binlog cache 寫到 binlog 文件中
- 一個事務的 binlog 是不能被拆開的,因此不論這個事務多大,也要確保一次性寫入
-
每個線程一個,參數 binlog_cache_size 用於控制單個線程內 binlog cache 所佔內存的大小
- 超過了這個參數規定的大小,就要暫存到磁盤
-
binlog 寫盤狀態
-
每個線程有自己 binlog cache,但是共用同一份 binlog 文件
-
write:把日誌寫入到文件系統的 page cache(速度快
-
fsync:將數據持久化到磁盤的操作(佔磁盤的 IOPS
-
sync_binlog 控制 write 和 fsync 的時機
- sync_binlog=0 的時候,表示每次提交事務都只 write,不 fsync
- 考慮到丟失日誌量的可控性,一般不建議將這個參數設成 0
- sync_binlog=1 的時候,表示每次提交事務都會執行 fsync
- sync_binlog=N (N>1) 的時候,表示每次提交事務都 write,但累積 N 個事務後才 fsync
- 如果主機發生異常重啓,會丟失最近 N 個事務的 binlog 日誌
- sync_binlog=0 的時候,表示每次提交事務都只 write,不 fsync
-
-
- redo log 的寫入機制
-
事務在執行過程中,生成的 redo log 是要先寫到 redo log buffer 的,不需要直接持久化到磁盤
- 若異常重啓,這部分日誌就丟了,但由於事務並沒有提交,丟了也不會有損失
-
MySQL redo log 存儲狀態
-
存在 redo log buffer 中,物理上是在 MySQL 進程內存中,就是圖中的紅色部分(快
-
寫到磁盤 (write),但是沒有持久化(fsync),物理上是在文件系統的 page cache 裏(快
-
持久化到磁盤,對應的是 hard disk,也就是圖中的綠色部分(慢,同樣佔磁盤的 IOPS
-
innodb_flush_log_at_trx_commit 參數控制 redo log 的寫入策略
- 0:事務提交時都只是把 redo log 留在 redo log buffer 中
- 1:事務提交時都將 redo log 直接持久化到磁盤
- 2:每次事務提交時都只是把 redo log 寫到 page cache
-
可能將一個「沒有提交」的事務的 redo log 寫入到磁盤中的場景
- 後臺線程每秒一次的輪詢(把 redo log buffer 中的日誌寫入 page cache 並 fsync 持久化
- redo log buffer 佔用的空間即將達到 innodb_log_buffer_size 一半時後臺線程會主動寫盤
- 只 write,不 fsync
- 並行的事務提交的時候,順帶將這個事務的 redo log buffer 持久化到磁盤
-
“雙 1” 配置:sync_binlog 和 innodb_flush_log_at_trx_commit 都設置成 1
- 一個事務完整提交前,需要等待兩次刷盤,一次是 redo log(prepare 階段),一次是 binlog
-
-
組提交(group commit)機制
-
日誌邏輯序列號(log sequence number,LSN):
- LSN 是單調遞增的,用來對應 redo log 的一個個寫入點
- 每次寫入長度爲 length 的 redo log, LSN 的值就會加上 length
-
LSN 也會寫到 InnoDB 的數據頁中,來確保數據頁不會被多次執行重複的 redo log
-
栗子
-
trx1 是第一個到達的,會被選爲這組的 leader
-
等 trx1 要開始寫盤的時候,這個組裏面已經有了三個事務, LSN 變成了 160
-
trx1 去寫盤的時候,帶的就是 LSN=160(小於等於 160 的 redo log 均持久化
-
trx2 和 trx3 直接返回
-
一次組提交裏面,組員越多,節約磁盤 IOPS 的效果越好
-
-
利用組提交的 MySQL 優化:拖時間使 binlog 也可以組提交
- MySQL 爲了讓組提交的效果更好,把 redo log 做 fsync 的時間拖到 binlog write 之後
- binlog write:binlog 從 binlog cache 中寫到磁盤上的 binlog 文件
- binlog 的組提交的效果通常不如 redo log 組提交效果好(redo log fsync 執行很快
- 提升 binlog 組提交的效果的參數(兩者爲 或 關係,滿足一個則調用 fsync
- binlog_group_commit_sync_delay 參數:延遲多少微秒後才調用 fsync
- binlog_group_commit_sync_no_delay_count 參數:累積多少次以後才調用 fsync
- MySQL 爲了讓組提交的效果更好,把 redo log 做 fsync 的時間拖到 binlog write 之後
-
-
- 日誌相關問題
- 爲什麼 binlog cache 是每個線程自己維護的,而 redo log buffer 是全局共用的?
- 主要原因是 binlog 是不能 “被打斷的”,一個事務的 binlog 必須連續寫(等事務提交完寫入
- redo log 並沒有這個要求,中間生成的日誌可以寫到 redo log buffer,還可以搭便車持久化
- 事務執行期間還沒到提交階段時發生 crash 的話,redo log 丟失,這會不會導致主備不一致呢?
- 不會; binlog 還在 binlog cache 中,未發給備庫(crash 後從業務角度看事業也未提交
- 數據庫的 crash-safe 保證的是
- 如果客戶端收到事務成功的消息,事務就一定持久化了
- 如果客戶端收到事務失敗(比如主鍵衝突、回滾等)的消息,事務就一定失敗了
- 如果客戶端收到 “執行異常” 的消息,應用需要重連後通過查詢當前狀態來繼續後續的邏輯
- 此時數據庫只需要保證內部(數據和日誌之間,主庫和備庫之間)一致就可以了
- 爲什麼 binlog cache 是每個線程自己維護的,而 redo log buffer 是全局共用的?