「筆記」MySQL 實戰 45 講 - 實踐篇(二)

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 鎖

      tu1

      • 現在有一個線程正在表 t 上請求或者持有 MDL 寫鎖,把 select 語句堵住
    • 查獲加表鎖的線程 id

      tu2

  • 等 FLUSH

    • Waiting for table flush 狀態示意圖

      tu3

      • MySQL 裏面對錶做 flush 操作的用法
         # 只關閉表 t
         flush tables t with read lock;
         # 關閉 MySQL 裏所有打開的表
         flush tables with read lock
        
    • 等行鎖

      • 加鎖讀方式: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 id=1;(掃描行數 1 ,執行時長 800 毫秒
        • select * from t where id=1 lock in share mode;(掃描行數 1,執行時長 0.2 毫秒
      • id=1 的數據狀態

        tu1

        • session B 更新完 100 萬次,生成了 100 萬個回滾日誌 (undo log)
        • 一致性讀需要從 1000001 開始依次執行 undo log,執行了 100 萬次後,纔將結果返回

幻讀

  • 特別說明

    • 幻讀在 “當前讀” 下才會出現(普通查詢是快照讀,看不到其他事物插入的數據
      • 當前讀的規則,就是要能讀到所有已經提交的記錄的最新值
    • 幻讀僅專指 “新插入的行”(辯證觀點看待
  • 鎖的設計是爲了保證數據的一致性

    • 不止是數據庫內部數據狀態在此刻的一致性,還包含了數據和日誌在邏輯上的一致性
  • 爲了解決幻讀問題,InnoDB 只好引入新的鎖,也就是間隙鎖 (Gap Lock)

    • 間隙鎖,鎖的就是兩個值之間的空隙

      tu1

    • 間隙鎖和行鎖合稱 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 就會順便給主鍵索引上滿足條件的行加上行鎖
  • 主鍵索引範圍鎖

    tu1

    • session A 這時候鎖的範圍就是主鍵索引上,行鎖 id=10 和 next-key lock (10,15]
    • 查找 id=10 行時是當做等值查詢來判斷的,而向右掃描到 id=15 的時候,用的是範圍查詢判斷
  • 唯一索引範圍鎖 bug

    tu1

    • InnoDB 會往前掃描到第一個不滿足條件的行爲止,也就是 id=20
      • 由於這是個範圍掃描,因此索引 id 上的 (15,20]
  • limit 語句加鎖

    • 栗子:delete from t where c = 10 / delete from t where c = 10 limit 2
      • 前者加鎖範圍:(5,15)後置加鎖範圍:(5,10)
    • 在刪除數據時儘量加 limit,不僅可以控制刪除數據的條數,讓操作更安全,還可以減小加鎖的範圍
  • 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 的寫入機制
    • 事務執行過程中,先把日誌寫到 binlog cache,事務提交時再把 binlog cache 寫到 binlog 文件中

      • 一個事務的 binlog 是不能被拆開的,因此不論這個事務多大,也要確保一次性寫入
    • 每個線程一個,參數 binlog_cache_size 用於控制單個線程內 binlog cache 所佔內存的大小

      • 超過了這個參數規定的大小,就要暫存到磁盤
    • binlog 寫盤狀態

      tu1

      • 每個線程有自己 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 日誌
  • redo log 的寫入機制
    • 事務在執行過程中,生成的 redo log 是要先寫到 redo log buffer 的,不需要直接持久化到磁盤

      • 若異常重啓,這部分日誌就丟了,但由於事務並沒有提交,丟了也不會有損失
    • MySQL redo log 存儲狀態

      tu1

      • 存在 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

      • 栗子

        tu1

        • trx1 是第一個到達的,會被選爲這組的 leader

        • 等 trx1 要開始寫盤的時候,這個組裏面已經有了三個事務, LSN 變成了 160

        • trx1 去寫盤的時候,帶的就是 LSN=160(小於等於 160 的 redo log 均持久化

        • trx2 和 trx3 直接返回

        • 一次組提交裏面,組員越多,節約磁盤 IOPS 的效果越好

      • 利用組提交的 MySQL 優化:拖時間使 binlog 也可以組提交

        tu1

        • 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
  • 日誌相關問題
    • 爲什麼 binlog cache 是每個線程自己維護的,而 redo log buffer 是全局共用的?
      • 主要原因是 binlog 是不能 “被打斷的”,一個事務的 binlog 必須連續寫(等事務提交完寫入
      • redo log 並沒有這個要求,中間生成的日誌可以寫到 redo log buffer,還可以搭便車持久化
    • 事務執行期間還沒到提交階段時發生 crash 的話,redo log 丟失,這會不會導致主備不一致呢?
      • 不會; binlog 還在 binlog cache 中,未發給備庫(crash 後從業務角度看事業也未提交
    • 數據庫的 crash-safe 保證的是
      • 如果客戶端收到事務成功的消息,事務就一定持久化了
      • 如果客戶端收到事務失敗(比如主鍵衝突、回滾等)的消息,事務就一定失敗了
      • 如果客戶端收到 “執行異常” 的消息,應用需要重連後通過查詢當前狀態來繼續後續的邏輯
        • 此時數據庫只需要保證內部(數據和日誌之間,主庫和備庫之間)一致就可以了
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章