MySQL深刻學習總結

MySQL簡介

  • 關於MySQL發音的官方答案:
    The official way to pronounce 「MySQL」 is 「My Ess Que Ell」 (not 「my sequel」), but we do not mind if you pronounce it as 「my sequel」 or in some other localized way.

        MySQL 能夠分爲 Server 層和存儲引擎層兩部分。mysql

        Server層包括鏈接器、查詢緩存、分析器、優化器、執行器等,涵蓋MySQL的大多數核心服務功能,以及全部的內置函數(如日期、時間、數學和加密函數等),全部跨存儲引擎的功能都在這一層實現,好比存儲過程、觸發器、視圖等。算法

        存儲引擎層負責數據的存儲和提取。其架構模式是插件式的,支持 InnoDB、MyISAM、Memory 等多個存儲引擎。如今最經常使用的存儲引擎是InnoDB,它從MySQL 5.5.5版本開始成爲了默認存儲引擎。create table 語句中使用 engine=memory,來指定使用內存引擎建立表。sql

        如今最經常使用的存儲引擎是InnoDB,它從MySQL 5.5.5版本開始成爲了默認存儲引擎。create table 語句中使用 engine=memory, 來指定使用內存引擎建立表。數據庫

查詢語句執行過程

clipboard.png

鏈接器

        第一步,鏈接器鏈接到數據庫,鏈接器負責跟客戶端創建鏈接、獲取權限、維持和管理鏈接。後端

鏈接命令通常是這麼寫的:mysql -h$ip -P$port -u$user -p$password

帳號密碼錯誤會報錯:Access denied for user數組

        鏈接完成後,若是沒有後續的動做,這個鏈接就處於空閒狀態,能夠在show processlist命令中看到它。文本中這個圖是show processlist的結果,其中的Command列顯示爲"Sleep"的這一行,就表示如今系統裏面有一個空閒鏈接。緩存

clipboard.png

        客戶端若是太長時間沒動靜,鏈接器就會自動將它斷開。這個時間是由參數wait timeout控制的,默認值是8小時。安全

斷開後再執行sql會報錯:Lost connection to MySQL server during query

        創建鏈接的過程一般是比較複雜的,因此建議在使用中要儘可能減小創建鏈接的動做,也就是儘可能使用長鏈接。性能優化

        可是 MySQL 在執行過程當中臨時使用的內存是管理在鏈接對象裏面的。這些資源會在鏈接斷開的時候才釋放。因此若是長鏈接累積下來,可能致使內存佔用太大,被系統強行殺掉(OOM),從現象看就是 MySQL 異常重啓。網絡

怎麼解決這個問題呢?能夠考慮如下兩種方案。

  1. 按期斷開長鏈接。使用一段時間,或者程序裏面判斷執行過一個佔用內存的大查詢後,斷開鏈接,以後要查詢再重連。
  2. 若是用的是MySQL 5.7或更新版本,能夠在每次執行一個比較大的操做後,經過執行mysql reset connection來從新初始化鏈接資源。這個過程不須要重連和從新作權限驗證可是會將鏈接恢復到剛剛建立完時的狀態。

查詢緩存

        第二步,查詢語句會先查詢緩存,以前執行過的語句及其結果可能會以 key-value 對的形式,被直接緩存在內存中。key 是查詢的語句,value 是查詢的結果。

        可是查詢緩存利大於弊,由於查詢緩存的失效很是頻繁,只要有對一個表的更新,這個表上全部的查詢緩存都會被清空。

        除非是靜態配置表才適合用查詢緩存。能夠將參數 query_cache_type 設置成DEMAND,這樣對於默認的 SQL 語句都不使用查詢緩存。SQL_CACHE 顯式指定使用查詢緩存。

select SQL_CACHE * from T where ID=10;

        可是,MySQL 8.0版本完全刪除了查詢緩存功能。

分析器

        第三步,分析語句,先是詞法分析,找出select,表名,列名等關鍵字;而後是語法分析,判斷語法是否正確。表名列名不對的sql,會在語法分析時報錯。

語法錯誤:ERROR 1064 (42000): You have an error in your SQL syntax;

優化器

        第四步,決定使用哪一個索引,join的時候決定各個表的鏈接順序。

執行器

        第五步,先判斷對當前表是否有權限(若是命中查詢緩存,會在返回結果時驗證權限)。

ERROR 1142 (42000): SELECT command denied to user 'b'@'localhost' for table 'T'

        如:select * from T where ID=10; 執行過程

  1. 調用 InnoDB 引擎接口取這個表的第一行,判斷 ID 值是否是 10,若是不是則跳過,若是是則將這行存在結果集中;
  2. 調用引擎接口取「下一行」,重複相同的判斷邏輯,直到取到這個表的最後一行。
  3. 執行器將上述遍歷過程當中全部知足條件的行組成的記錄集做爲結果集返回給客戶端。

        慢查詢日誌中有一行 rows_examined 字段,表示這個語句執行過程當中掃描了多少行。這個值就是在執行器每次調用引擎獲取數據行的時候累加的。可是引擎掃描行數跟 rows_examined 並非徹底相同的。

查詢的數據如何返回

  • 對一個200G的大表作全表掃描,而內存只有16G,會不會把數據庫主機的內存用光了?

    實際上,MySQL不是取到所有數據再返回客戶端。取數據和發數據的流程是這樣的:

    1. 獲取一行,寫到 net_buffer 中。這塊內存的大小是由參數 net_buffer_length 定義的,默認是 16k。
    2. 重複獲取行,直到 net_buffer 寫滿,調用網絡接口發出去。
    3. 若是發送成功,就清空 net_buffer,而後繼續取下一行,並寫入 net_buffer。
    4. 若是發送函數返回 EAGAIN 或 WSAEWOULDBLOCK,就表示本地網絡棧(socket send buffer)寫滿了,進入等待。直到網絡棧從新可寫,再繼續發送。
  • MySQL 客戶端發送請求後,接收服務端返回結果的方式有兩種:

    1. 一種是本地緩存,也就是在本地開一片內存,先把結果存起來。若是用 API 開發,對應的就是 mysql_store_result 方法。
    2. 另外一種是不緩存,讀一個處理一個。若是用 API 開發,對應的就是 mysql_use_result 方法。

      MySQL 客戶端默認採用第一種方式,而若是加上–quick 參數,就會使用第二種不緩存的方式。

      採用不緩存的方式時,若是本地處理得慢,就會致使服務端發送結果被阻塞,所以會讓服務端變慢。

        MySQL 是「邊讀邊發的」。這就意味着,若是客戶端接收得慢,會致使 MySQL 服務端因爲結果發不出去,這個事務的執行時間變長。

        對於正常的線上業務來講,若是一個查詢的返回結果不會不少的話,都建議使用 mysql_store_result 這個接口,直接把查詢結果保存到本地內存。

更新語句執行過程

        更新語句一樣會走鏈接器,查詢緩存(清空該表緩存),分析器,優化器這一套流程,與查詢流程不同的是,更新流程還涉及兩個重要的日誌模塊,redo log(重作日誌)和 binlog(歸檔日誌)。

重作日誌:redo log

        若是每一次的更新操做都須要寫進磁盤,而後磁盤也要找到對應的那條記錄,而後再更新,整個過程 IO 成本、查找成本都很高。

        MySQL採用了WAL技術,全稱是 Write-Ahead Logging,的關鍵點就是先寫日誌,再寫磁盤。

        具體來講,當有一條記錄須要更新的時候,InnoDB 引擎就會先把記錄寫到 redo log裏面,並更新內存,這個時候更新就算完成了。同時,InnoDB 引擎會在適當的時候,將這個操做記錄更新到磁盤裏面,而這個更新每每是在系統比較空閒的時候作。

        可是若是 InnoDB 的 redo log 寫滿了。這時候系統會中止全部更新操做,把 checkpoint 往前推動(對應的全部髒頁都 flush 到磁盤上),redo log 留出空間能夠繼續寫。

        一旦一個查詢請求須要在執行過程當中先 flush 掉一個髒頁時,這個查詢就可能要比平時慢了。因爲刷髒頁的邏輯會佔用 IO 資源並可能影響到了更新語句,要儘可能避免這種狀況,就要合理地設置 innodb_io_capacity 的值,而且平時要多關注髒頁比例,不要讓它常常接近 75%。髒頁比例是經過 Innodb_buffer_pool_pages_dirty/Innodb_buffer_pool_pages_total 獲得的,具體的命令參考下面代碼:

mysql> select VARIABLE_VALUE into @a from global_status where VARIABLE_NAME = 'Innodb_buffer_pool_pages_dirty';
select VARIABLE_VALUE into @b from global_status where VARIABLE_NAME = 'Innodb_buffer_pool_pages_total';
select @a/@b;

在 InnoDB 中,innodb_flush_neighbors 參數就是用來控制這個行爲的,值爲 1 的時候會有「連坐」機制,值爲 0 時表示不找鄰居,本身刷本身的。固態硬盤建議設置爲0。

        InnoDB 的 redo log 是能夠配置的固定大小,好比能夠配置爲一組 4 個文件,每一個文件的大小是 1GB,總共就能夠記錄 4GB 的操做。從頭開始寫,寫到末尾就又回到開頭循環寫,以下面這個圖所示。若是redo log 設置的過小,磁盤壓力很小,可是數據庫出現間歇性的性能下跌。
clipboard.png

        write pos 是當前記錄的位置,一邊寫一邊後移,寫到第 3 號文件末尾後就回到 0 號文件開頭。checkpoint 是當前要擦除的位置,也是日後推移而且循環的,擦除記錄前要把記錄更新到數據文件。

        write pos 和 checkpoint 之間的是還空着的部分,能夠用來記錄新的操做。若是 write pos 追上 checkpoint,這時候就得停下來先擦掉一些記錄,把 checkpoint 推動一下。

        有了 redo log,InnoDB 就能夠保證即便數據庫發生異常重啓,以前提交的記錄都不會丟失,這個能力稱爲crash-safe。

        redo log buffer :插入數據的過程當中,生成的日誌都得先保存起來,但又不能在還沒 commit 的時候就直接寫到 redo log 文件裏。因此,redo log buffer 就是一塊內存,用來先存 redo 日誌的。也就是說,在執行第一個 insert 的時候,數據的內存被修改了,在執行 commit 的時候 redo log buffer 才寫入了日誌。

爲了控制 redo log 的寫入策略,innodb_flush_log_at_trx_commit 參數,它有三種可能取值:

  1. 設置爲 0 的時候,表示每次事務提交時都只是把 redo log 留在 redo log buffer 中 ;
  2. 設置爲 1 的時候,表示每次事務提交時都將 redo log 直接持久化到磁盤;
  3. 設置爲 2 的時候,表示每次事務提交時都只是把 redo log 寫到 page cache。

        InnoDB 有一個後臺線程,每隔 1 秒,就會把 redo log buffer 中的日誌,調用 write 寫到文件系統的 page cache,而後調用 fsync 持久化到磁盤。也就是說,一個沒有提交的事務的 redo log,也是可能已經持久化到磁盤的。

還有兩種場景也會把沒有提交的redo log 寫到硬盤。

  1. redo log buffer 佔用的空間即將達到 innodb_log_buffer_size 一半的時候,後臺線程會主動寫盤。注意,因爲這個事務並無提交,因此這個寫盤動做只是 write,而沒有調用 fsync,也就是隻留在了文件系統的 page cache。
  2. 並行的事務提交的時候,順帶將這個事務的 redo log buffer 持久化到磁盤。假設一個事務 A 執行到一半,另外一個事務B提交,事務B要把 redo log buffer 裏的日誌所有持久化到磁盤。

歸檔日誌:binlog

redo log 是 InnoDB 引擎特有的日誌,而 Server 層也有本身的日誌,稱爲 binlog(歸檔日誌)。

binlog 的三種格式對比:
        statement:記錄到 binlog 裏的是語句原文,最後會有 COMMIT;可能會致使主備不一致,由於limit 、等sql 執行時可能主備優化器選擇的索引不同,排序也不同。now()執行的結果也不同。
        row :記錄了操做的事件每一條數據的變化狀況,最後會有一個 XID event。缺點是太佔空間。
        mixed:同時使用兩種格式,由數據庫判斷具體某條sql使用哪一種格式。可是有選擇錯誤的狀況。

這兩種日誌有如下三點不一樣。

  1. redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 層實現的,全部引擎均可以使用。
  2. redo log 是物理日誌,記錄的是「在某個數據頁上作了什麼修改」;binlog 是邏輯日誌,記錄的是這個語句的原始邏輯,好比「給 ID=2 這一行的 c 字段加 1 」。
  3. redo log 是循環寫的,空間固定會用完;binlog 是能夠追加寫入的。追加寫」是指 binlog 文件寫到必定大小後會切換到下一個,並不會覆蓋之前的日誌。

redo log 和 binlog 是怎麼關聯起來的?
        它們有一個共同的數據字段,叫 XID。崩潰恢復的時候,會按順序掃描 redo log:

  • 若是碰到既有 prepare、又有 commit 的 redo log,就直接提交;
  • 若是碰到只有 parepare、而沒有 commit 的 redo log,就拿着 XID 去 binlog 找對應的事務。

        處於 prepare 階段的 redo log 加上完整 binlog,重啓也能恢復,由於 binlog 完整了,那麼從庫就同步過去了,爲了保證主從一致,有完整的 binlog 就算成功。

        事務執行過程當中,先把日誌寫到 binlog cache,事務提交的時候,再把 binlog cache 寫到 binlog 文件中。

write 和 fsync 的時機,是由參數 sync_binlog 控制的:

  1. sync_binlog=0 的時候,表示每次提交事務都只 write,不 fsync;
  2. sync_binlog=1 的時候,表示每次提交務都會執行 fsync;
  3. sync_binlog=N(N>1) 的時候,表示每次提交事務都 write,但累積 N 個事務後才 fsync。
比較常見的是將其設置爲 100~1000 中的某個數值。對應的風險是:若是主機發生異常重啓,會丟失最近 N 個事務的 binlog 日誌。

更新語句執行過程

好比:update T set c=c+1 where ID=2;

  1. 執行器先找引擎取 ID=2 這一行。若是 ID=2 這一行所在的數據頁原本就在內存中,就直接返回給執行器;不然,須要先從磁盤讀入內存,而後再返回。
  2. 執行器拿到引擎給的行數據,把這個值加上 1,獲得新的一行數據,再調用引擎接口寫入這行新數據。
  3. 引擎將這行新數據更新到內存中,同時將這個更新操做記錄到 redo log 裏面,此時 redo log 處於 prepare 狀態。而後告知執行器執行完成了,隨時能夠提交事務。
  4. 執行器生成這個操做的 binlog,並把 binlog 寫入磁盤。
  5. 執行器調用引擎的提交事務接口,引擎把剛剛寫入的 redo log 改爲提交(commit)狀態,更新完成。

        這裏給出這個 update 語句的執行流程圖,圖中淺色框表示是在 InnoDB 內部執行的,深色框表示是在執行器中執行的。其實就是把redo log 和binlog 作兩階段提交,爲了讓兩份日誌之間的邏輯一致。
clipboard.png

備份恢復

保存必定時間的binlog,同時系統會按期作整庫備份。

當須要恢復到指定的某一秒時,

  1. 首先,找到最近的一次全量備份,若是運氣好,可能就是昨天晚上的一個備份,從這個備份恢復到臨時庫
  2. 而後,從備份的時間點開始,將備份的 binlog 依次取出來,重放到指定的那個時刻。

        redo log 用於保證 crash-safe 能力。innodb_flush_log_at_trx_commit 這個參數設置成 1 的時候,表示每次事務的 redo log 都直接持久化到磁盤。這個參數建議設置成 1,這樣能夠保證 MySQL 異常重啓以後數據不丟失。

        binlog用於備份恢復和從庫同步。sync_binlog 這個參數設置成 1 的時候,表示每次事務的 binlog 都持久化到磁盤。這個參數也建議設置成 1,這樣能夠保證 MySQL 異常重啓以後 binlog 不丟失。

主備同步

  1. 在備庫 B 上經過 change master 命令,設置主庫 A 的 IP、端口、用戶名、密碼,以及要從哪一個位置開始請求 binlog,這個位置包含文件名和日誌偏移量。
  2. 在備庫 B 上執行 start slave 命令,這時候備庫會啓動兩個線程,就是圖中的 io_thread 和 sql_thread。其中 io_thread 負責與主庫創建鏈接。
  3. 主庫 A 校驗完用戶名、密碼後,開始按照備庫 B 傳過來的位置,從本地讀取 binlog,發給 B。
  4. 備庫 B 拿到 binlog 後,寫到本地文件,稱爲中轉日誌(relay log)。
  5. sql_thread 讀取中轉日誌,解析出日誌裏的命令,並執行。

clipboard.png
        一主一備結構,須要注意主備切換,備庫設置只讀,避免切換bug形成雙寫不一致問題(設置 readonly 對超級用戶是無效的,同步更新的線程有超級權限,因此還能寫入同步數據)。

        雙主結構,要避免循環更新問題,由於MySQL 在 binlog 中記錄了這個命令第一次執行時所在實例的 server id。因此能夠規定兩個庫的 server id 必須不一樣,每一個庫在收到從本身的主庫發過來的日誌後,先判斷 server id,若是跟本身的相同,表示這個日誌是本身生成的,就直接丟棄這個日誌。

主備延遲

        能夠在備庫上執行 show slave status 命令,它的返回結果裏面會顯示 seconds_behind_master,用於表示當前備庫延遲了多少秒。每一個事務的 binlog 裏面都有一個時間字段,用於記錄主庫上寫入的時間; 備庫取出當前正在執行的事務的時間字段的值,計算它與當前系統時間的差值,獲得 seconds_behind_master。

主備延遲最直接的表現是,備庫消費中轉日誌(relay log)的速度,比主庫生產 binlog 的速度要慢。

主備延遲的來源

  1. 有些部署條件下,備庫所在機器的性能要比主庫所在的機器性能差。
  2. 考慮到主備切換,主備機器通常都同樣了,可是還可能備庫讀的壓力太大,

    一主多從,或者經過binlog輸出到外部系統(好比Hadoop),讓外部系統提供部分統計查詢能力。
  3. 大事務,若是事務執行十分鐘,那就會致使主從延遲十分鐘。

主備複製策略

        在官方的 5.6 版本以前,MySQL 只支持單線程複製,由此在主庫併發高、TPS 高時就會出現嚴重的主備延遲問題。

        並行複製策略有按表並行分發策略,按行並行分發策略,可是按行分發在決定線程分發的時候,須要消耗更多的計算資源。這兩個方案其實都有一些約束條件:

  1. 要可以從 binlog 裏面解析出表名、主鍵值和惟一索引的值。也就是說,主庫的 binlog 格式必須是 row;
  2. 表必須有主鍵;
  3. 不能有外鍵。表上若是有外鍵,級聯更新的行不會記錄在 binlog 中,這樣衝突檢測就不許確。

        官方 MySQL5.6 版本,支持了並行複製,只是支持的粒度是按庫並行。相比於按表和按行分發,這個策略有兩個優點:

  1. 構造 hash 值的時候很快,只須要庫名;並且一個實例上 DB 數也不會不少,不會出現須要構造 100 萬個項這種狀況。
  2. 不要求 binlog 的格式。由於 statement 格式的 binlog 也能夠很容易拿到庫名。

        MariaDB 的並行複製策略,僞模擬主庫併發度,主庫 redo log 組提交 (group commit) 優化,同一組提交會記錄commit_id,備庫把同一個commit_id分發到多個worker執行。

官方的 MySQL5.7 版本,由參數 slave-parallel-type 來控制並行複製策略:

  1. 配置爲 DATABASE,表示使用 MySQL 5.6 版本的按庫並行策略;
  2. 配置爲 LOGICAL_CLOCK,表示的就是相似 MariaDB 的策略。不過,MySQL 5.7 這個策略,針對並行度作了優化。

        MySQL 5.7.22 版本里,MySQL 增長了一個新的並行複製策略,基於 WRITESET 的並行複製。對於事務涉及更新的每一行,計算出這一行的 hash 值,組成集合 writeset。若是兩個事務沒有操做相同的行,也就是說它們的 writeset 沒有交集,就能夠並行。

讀寫分離

讀寫分離有兩種方案:

  1. 客戶端直連方案,由於少了一層 proxy 轉發,因此查詢性能稍微好一點兒,而且總體架構簡單,排查問題更方便。可是這種方案,因爲要了解後端部署細節,因此在出現主備切換、庫遷移等做的時候,客戶端都會感知到,而且須要調整數據庫鏈接信息。 可能會以爲這樣客戶端也太麻煩了,信息大量冗餘,架構很醜。其實也未必,通常採用這樣的架構,必定會伴隨一個負責管理後端的組件,好比 Zookeeper,儘可能讓業務端只專一於業務邏輯開發。
  2. 帶 proxy 的架構,對客戶端比較友好。客戶端不須要關注後端細節,鏈接維護、後端信息維護等工做,都是由 proxy 完成的。但這樣的話,對後端維護團隊的要求會更高。並且,proxy 也須要有高可用架構。所以,帶 proxy 架構的總體就相對比較複雜。

主從延遲的狀況下怎麼辦?

  1. 強制走主庫方案;對於必需要拿到最新結果的請求,強制將其發到主庫上。
  2. sleep 方案;主庫更新後,讀從庫以前先 sleep 一下。由於大多數狀況下主備延遲在 1 秒以內。
  3. 判斷主備無延遲方案; 每次從庫執行查詢請求前,先判斷 seconds_behind_master 是否已經等於 0。若是還不等於 0 ,那就必須等到這個參數變爲 0 才能執行查詢請求。
  4. 配合 semi-sync 方案;半同步複製:

    1. 事務提交的時候,主庫把 binlog 發給從庫;
    2. 從庫收到 binlog 之後,發回給主庫一個 ack,表示收到了;
    3. 主庫收到這個 ack 之後,才能給客戶端客戶端返回「事務完成」的確認。
  5. 等主庫位點方案;
  6. 等 GTID 方案。

隔離級別

數據庫特性

ACID(Atomicity、Consistency、Isolation、Durability,即原子性、一致性、、隔離性、持久性)。

        當數據庫上有多個事務同時執行的時候,就可能出現髒讀(dirty read)、不可重複讀(non-repeatable read)、幻讀(phantom read)的問題,爲了解決這些問題,就有了「隔離級別」的概念。

  • 髒讀:指的是一個事務的讀操做讀到了另外一個未提交的事務修改的值。
  • 不可重複讀:指的是一個事務讀了同一個值兩次,可是兩次的值不一樣,由於中間另外一個事務修改了這個值。
  • 幻讀:仍然指的是一個事務中讀了兩次,結果不一樣,可是與不可重複讀不一樣的是,這裏不一樣是由於別的事物作了插入操做,而是讀的條件是一個範圍的條件,這樣第二次會多讀到一條數據。

    不可重複讀重點在於update和delete,而幻讀的重點在於insert。

幻讀問題——間隙鎖

        即便把全部的記錄都加上鎖,仍是阻止不了新插入的記錄,也就是說行鎖解決不了幻讀問題,行鎖只能鎖住行,可是新插入記錄這個動做,要更新的是記錄之間的「間隙」。所以,爲了解決幻讀問題,InnoDB 只好引入新的鎖,也就是間隙鎖 (Gap Lock)。

        當執行 select * from t where d=5 for update 的時候,就不止是給數據庫中已有的 6 個記錄加上了行鎖,還同時加了 7 個間隙鎖。這樣就確保了沒法再插入新的記錄。

        間隙鎖和行鎖合稱 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]。

        間隙鎖和 next-key lock 的引入,解決了幻讀的問題,但同時也帶來了一些「困擾」。間隙鎖的引入,可能會致使一樣的語句鎖住更大的範圍,這實際上是影響了併發度的。

隔離級別

        SQL 標準的事務隔離級別包括:讀未提交read uncommitted)、讀提交(read committed)、可重複讀(repeatable read)和串行化(serializable )。隔離級別越高,效率越低。

  • 讀未提交是指,一個事務還沒提交時,它作的變動就能被別的事務看到。
  • 讀提交是指,一個事務提交以後,它作的變動纔會被其餘事務看到。
  • 可重複讀是指,一個事務執行過程當中看到的數據,老是跟這個事務在啓動時看到的數據是一致的。固然在可重複讀隔離級別下,未提交變動對其餘事務也是不可見的。
  • 串行化,顧名思義是對於同一行記錄,「寫」會加「寫鎖」,「讀」 會加「讀鎖」。當出現讀寫鎖衝突的時候,後訪問的事務必須等前一個事務執行完成,才能繼續執行。

在實現上,數據庫裏面會建立一個視圖,訪問的時候以視圖的邏輯結果爲準。

  1. 「可重複讀」隔離級別下:這個視圖是在事務啓動時建立的,整個事務存在期間都用這個視圖。
  2. 「讀提交」隔離級別下:這個視圖是在每一個 SQL 語句開始執行的時候建立的。
  3. 「讀未提交」隔離級別下:直接返回記錄上的最新值,沒有視圖概念
  4. 「串行化」隔離級別下:直接用加鎖的方式來避免並行訪問。

在 MySQL 裏,有兩個「視圖」的概念:

  • 一個是 view。它是一個用查詢語句定義的虛擬表,在調用的時候執行查詢語句並生成結果。建立視圖的語法是 create view … ,而它的查詢方法與表同樣。
  • 另外一個是 InnoDB 在實現 MVCC 時用到的一致性讀視圖,即 consistent read view,用於支持 RC(Read Committed,讀提交)和 RR(Repeatable Read,可重複讀)隔離級別的實現。

MySQL 默認隔離級別是可重複讀,Oracle 默認隔離級別是「讀提交」。

        將啓動參數 transaction-isolation 的值設置成 READ-UNCOMMITTED、READ-COMMITTED、REPEATABLE-READ 、SERIALIZABLE。

        能夠用 show variables 來查看當前的值。

事務隔離的實現——undo log

        每條記錄在更新的時候都會同時記錄一條回滾操做。同一條記錄在系統中能夠存在多個版本,這就是數據庫的(MVCC)。

        MVCC的全稱是「多版本併發控制」。爲了查詢一些正在被另外一個事務更新的行,而且能夠看到它們被更新以前的值,不用等待另外一個事務釋放鎖。

        InnoDB會給數據庫中的每一行增長三個字段,它們分別是DB_TRX_ID(事務版本號)、DB_ROLL_PTR(建立時間)、DB_ROW_ID(惟一id)。

        InnoDB 裏面每一個事務有一個惟一的事務 ID,叫做 transaction id。它是在事務開始的時候向 InnoDB 的事務系統申請的,是按申請順序嚴格遞增的。

        InnoDB 利用了「全部數據都有多個版本」的這個特性,實現了「秒級建立快照」的能力。

        B+Tree葉結點上,始終存儲的是最新的數據(多是還未提交的數據)。而舊版本數據,經過UNDO記錄存儲在回滾段(Rollback Segment)裏。每一條記錄都會維護一個ROW HEADER元信息,存儲有建立這條記錄的事務ID,一個指向UNDO記錄的指針。經過最新記錄和UNDO信息,能夠還原出舊版本的記錄。

假設一個值從 1 被按順序改爲了 二、三、4,在回滾日誌裏面就會有相似下面的記錄。
clipboard.png
        當前值是 4,可是在查詢這條記錄的時候,不一樣時刻啓動的事務會有不一樣的 read-view。同一條記錄在系統中能夠存在多個版本,就是數據庫的多版本併發控制(MVCC)。對於 read-view A,要獲得 1,就必須將當前值依次執行圖中全部的回滾操做獲得。這些回滾信息記錄在undo log 裏。

        當系統裏沒有比這個回滾日誌更早的 read-view 的時候會刪除老的undo log。

避免長事務

        儘可能不要使用長事務,長事務意味着系統裏面會存在很老的事務視圖。會有很大的undo log日誌佔用空間。並且長事務還會佔據鎖資源,也可能拖垮整個庫。

        能夠在 information_schema 庫的innodb_trx 這個表中查詢長事務,好比下面這個語句,用於查找持續時間超過 60s 的事務。能夠監控這個表,設置長事務閾值報警或者直接kill。

select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60

        能夠經過 SET MAX_EXECUTION_TIME 命令來控制每一個語句執行的最長時間,避免單個語句意外執行太長時間。

        確認是否有沒必要要的只讀事務。

        若是使用的是 MySQL 5.6 或者更新版本,把 innodb_undo_tablespaces 設置成 2或更大的值)。若是真的出現大事務致使undo log過大,這樣設置後清理起來更方便。

索引

常見索引模型

        Hash表 + 鏈表,查詢新增都很快,可是隻適用於只有等值查詢的場景,不能區間查詢, Memcached 及其餘一些 NoSQL 引擎在用。

        有序數組,等值查詢和範圍查詢場景中的性能就都很是優秀,二分查找O(log(N)),可是更新的效率很低,因此只適用於靜態存儲引擎。

        平衡二叉樹,更新和查詢都比較快。
        還有跳躍表,LSM樹等。

B+ 樹

        爲了讓一個查詢儘可能少地讀磁盤,就須要使用多叉樹。MySQL採用的是B+樹,因爲索引不止存在內存中,還要寫到磁盤上。二叉樹的樹高過高,100萬數據,就有20層,在機械硬盤時代,從磁盤隨機讀一個數據塊須要 10 ms 左右的尋址時間。就要花費200ms的尋址時間,就太慢了。MySQL B+樹 的一層節點數量在1200左右,只須要1-3次磁盤IO就能夠了,由於InnoDB存儲引擎的最小儲存單元頁(Page),一個頁的大小是16K。通常來講主鍵id爲bigint類型,長度8字節,指針6字節,那麼16284/14 = 1170。因此一次IO最多讀取1170個節點。

        相對於B樹,B+樹把全部的數據都放在了葉子節點上,這樣雖然每次都須要查詢葉子節點,但也不過兩三層,若是幹節點也放數據,那幹節點就變大了,一次就讀取不了1200節點了,層高會變大不少。

        而且MySQL把B+樹的全部葉子節點的數據用指針連起來了,這樣作區間查詢是很是快的。

主鍵索引和非主鍵索引

        主鍵索引的葉子節點存的是整行數據。在 InnoDB 裏,主鍵索引也被稱爲聚簇索引(clustered index)。

        非主鍵索引的葉子節點內容是主鍵的值。在 InnoDB 裏,非主鍵索引也被稱爲二級索引(secondary index)。

        查詢語句,若是走主鍵索引,會直接獲得數據,若是走非主鍵索引,查到主鍵後,還須要回主鍵索引再查一次數據。這個過程稱爲回表。(覆蓋索引不須要回表)
clipboard.png

        分爲聚簇索引和非聚簇索引的緣由:更新數據的時候,因爲數據的地址變了,須要更改索引,可是因爲數據只跟主鍵索引綁定,索引只須要更新聚簇索引,固然還有被更新列涉及到的索引也要更新。若是全部全部都跟數據綁定,雖然省掉了回表的過程,可是每次更新,須要更新全部的索引,得不償失。

索引維護

        B+ 樹爲了維護索引有序性,在插入新值的時候須要作必要的維護。

        好比按順序插入1-499,501-1000,索引都在一頁,再插入一個500,根據 B+ 樹的算法,這時候須要申請一個新的數據頁,而後挪動部分數據(501到1000的數據)過去。這個過程稱爲頁分裂。在這種狀況下,性能天然會受影響。

        除了影響性能外,頁分裂操做還影響數據頁的利用率。本來放在一個頁的數據,如今分到兩個頁中,總體空間利用率下降大約 50%。

        固然有分裂就有合併。當相鄰兩個頁因爲刪除了數據,利用率很低以後,會將數據頁作合併。合併的過程,能夠認爲是分裂過程的逆過程。

        因此通常建表規範都要求用自增主鍵,避免頁分裂,固然也有特殊狀況,使用別的字段當作主鍵。

        而且索引可能由於刪除,或者頁分裂等緣由,致使數據頁有空洞,重建索引的過程會建立一個新的索引,把數據按順序插入,這樣頁面的利用率最高,也就是索引更緊湊、更省空間。

alter table T drop index k;
alter table T add index(k);

        可是不能重建主鍵索引,不管是刪除主鍵仍是建立主鍵,都會將整個表重建。可使用 alter table T engine=InnoDB 重建表。

覆蓋索引

        若是執行的語句是 select ID from T where k between 3 and 5,這時只須要查 ID 的值,而 ID 的值已經在 k 索引樹上了,所以能夠直接提供查詢結果,不須要回表。也就是說,在這個查詢裏面,索引 k 已經「覆蓋了」查詢需求,稱爲覆蓋索引。

        因爲覆蓋索引能夠減小樹的搜索次數,顯著提高查詢性能,因此使用覆蓋索引是一個經常使用的性能優化手段。

        若是有根據身份證號查詢市民信息的需求,只要在身份證號字段上創建索引就夠了。若是如今有一個高頻請求,要根據市民的身份證號查詢他的姓名,再創建一個(身份證號、姓名)的聯合索引就是覆蓋索引,省去了回表環節。

最左前綴原則

        若是爲每一種查詢都設計一個索引,索引是否是太多了。

        B+ 樹這種索引結構,能夠利用索引的「最左前綴」,來定位記錄。

        爲了直觀地說明這個概念,用(name,age)這個聯合索引來分析。
clipboard.png
        能夠看到,索引項是按照索引定義裏面出現的字段順序排序的。

        當邏輯需求是查到全部名字是「張三」的人時,能夠快速定位到 ID4,而後向後遍歷獲得全部須要的結果。

        若是要查的是全部名字第一個字是「張」的人,SQL 語句的條件是"where name like ‘張 %’"。這時,也可以用上這個索引,查找到第一個符合條件的記錄是 ID3,而後向後遍歷,直到不知足條件爲止。

        能夠看到,不僅是索引的所有定義,只要知足最左前綴,就能夠利用索引來加速檢索。這個最左前綴能夠是聯合索引的最左 N 個字段,也能夠是字符串索引的最左 M 個字符。

前綴索引

        使用前綴索引,定義好長度,就能夠作到既節省空間,又不用額外增長太多的查詢成本。

        在創建索引時關注的是區分度,區分度越高越好。由於區分度越高,意味着重複的鍵值越少。所以,能夠經過統計索引上有多少個不一樣的值來判斷要使用多長的前綴。

        可使用下面這個語句,算出這個列上有多少個不一樣的值:

select count(distinct email) as L from SUser;

        使用前綴索引就用不上覆蓋索引對查詢性能的優化了,這是在選擇是否使用前綴索引時須要考慮的一個因素。

        那麼對於身份證號,一共 18 位,其中前 6 位是地址碼,因此同一個縣的人的身份證號前 6 位通常會是相同的。該怎麼存儲,怎麼設計索引呢?

    1. 第一種方式是使用倒序存儲。身份證號的最後 6 位沒有地址碼這樣的重複邏輯。

      select field_list from t where id_card = reverse('input_id_card_string');

      select field_list from t where id_card = reverse('input_id_card_string');

    1. 第二種方式是使用 hash 字段。在表上再建立一個整數字段,來保存身份證的校驗碼,同時在這個字段上建立索引。

      alter table t add id_card_crc int unsigned, add index(id_card_crc);

      而後每次插入新記錄的時候,都同時用 crc32() 這個函數獲得校驗碼填到這個新字段。因爲校驗碼可能存在衝突,因此查詢語句 where 部分要判斷 id_card 的值是否精確相同。

      select field_list from t where id_card_crc=crc32('input_id_card_string') and id_card='input_id_card_string'

    索引下推

            最左前綴的時候,那些不符合最左前綴的部分,會怎麼樣呢?

            若是如今有一個需求:檢索出表中「名字第一個字是張,並且年齡是 10 歲的全部男孩」。那麼,SQL 語句是這麼寫的:

    mysql> select * from tuser where name like '張 %' and age=10 and ismale=1;

            這個語句在搜索索引樹的時候,只能用 「張」,找到第一個知足條件的記錄 ID3。

            而後須要判斷其餘條件是否知足。

            在 MySQL 5.6 以前,只能從 ID3 開始一個個回表。到主鍵索引上找出數據行,再對比字段值。

            而 MySQL 5.6 引入的索引下推優化(index condition pushdown),能夠在索引遍歷過程當中,對索引中包含的字段先作判斷,直接過濾掉不知足條件的記錄,減小回表次數。
    clipboard.png

    change buffer

            當須要更新一個數據頁時,若是數據頁在內存中就直接更新,而若是這個數據頁尚未在內存中的話,在不影響數據一致性的前提下,InooDB 會將這些更新操做緩存在 change buffer 中,這樣就不須要從磁盤中讀入這個數據頁了。在下次查詢須要訪問這個數據頁的時候,將數據頁讀入內存,而後執行 change buffer 中與這個頁有關的操做。經過這種方式就能保證這個數據邏輯的正確性。雖然是隻更新內存,可是在事務提交的時候,把 change buffer 的操做也記錄到 redo log 裏了,因此崩潰恢復的時候,change buffer 也能找回來。

            須要說明的是,雖然名字叫做 change buffer,實際上它是能夠持久化的數據。也就是說,change buffer 在內存中有拷貝,也會被寫入到磁盤上。

            將 change buffer 中的操做應用到原數據頁,獲得最新結果的過程稱爲 merge。除了訪問這個數據頁會觸發 merge 外,系統有後臺線程會按期 merge。在數據庫正常關閉(shutdown)的過程當中,也會執行 merge 操做。

    ​        顯然,若是可以將更新操做先記錄在 change buffer,減小讀磁盤,語句的執行速度會獲得明顯的提高。並且,數據讀入內存是須要佔用 buffer pool 的,因此這種方式可以避免佔用內存,提升內存利用率。

            惟一索引的更新就不能使用 change buffer,實際上也只有普通索引可使用。

            change buffer 用的是 buffer pool 裏的內存,所以不能無限增大。change buffer 的大小,能夠經過參數 innodb_change_buffer_max_size 來動態設置。這個參數設置爲 50 的時候,表示 change buffer 的大小最多隻能佔用 buffer pool 的 50%。

    ​        若是要在這張表中插入一個新記錄 (4,400) 的話,InnoDB 的處理流程是怎樣的。

    第一種狀況是,這個記錄要更新的目標頁在內存中。這時,InnoDB 的處理流程以下:

    • 對於惟一索引來講,找到 3 和 5 之間的位置,判斷到沒有衝突,插入這個值,語句執行結束;
    • 對於普通索引來講,找到 3 和 5 之間的位置,插入這個值,語句執行結束。

      這個判斷只會耗費微小的 CPU 時間。不是重點

    第二種狀況是,這個記錄要更新的目標頁不在內存中。這時,InnoDB 的處理流程以下:

    • 對於惟一索引來講,須要將數據頁讀入內存,判斷到沒有衝突,插入這個值,語句執行結束;
    • 對於普通索引來講,則是將更新記錄在 change buffer,語句執行就結束了。

            將數據從磁盤讀入內存涉及隨機 IO 的訪問,是數據庫裏面成本最高的操做之一。change buffer 由於減小了隨機磁盤訪問,因此對更新性能的提高是會很明顯的。

            change buffer 適用於寫多讀少的業務,好比帳單類、日誌類的系統。由於會記錄不少change buffer(寫的時候) 纔會merge(讀的時候)

            反過來,讀多寫少的業務,幾乎每次把更新記錄在change buffer 後,就會當即出發merge,這樣隨機訪問 IO 的次數不會減小,反而增長了change buffer 的維護代價。

            因此,對於身份證號這類字段,若是業務已經保證不會寫入重複數據,不須要數據庫作約束,加普通索引比加主鍵索引要好,若是全部的更新後面,都立刻伴隨着對這個記錄的查詢,那麼應該關閉 change buffer。而在其餘狀況下,change buffer 都能提高更新性能。

            在實際使用中,能夠發現,普通索引和 change buffer 的配合使用,對於數據量大的表的更新優化仍是很明顯的,特別是在使用機械硬盤時。

    change buffer 和 redo log 對比

    insert into t(id,k) values(id1,k1),(id2,k2);

    這條更新語句作了以下操做:

    1. Page 在內存中,直接更新內存;
    2. Page 沒有在內存中,就在內存的 change buffer 區域,記錄下「要往 Page 插入一行」這個信。
    3. 將上述兩個動做記入 redo log 中。

    後續的更新操做

    1. Page 在內存中,會直接從內存返回。
    2. Page 不在內容中,須要把 Page 從磁盤讀入內存中,而後應用 change buffer 裏面的操做日誌,生成一個正確的版本並返回結果。

            因此,若是要簡單地對比這兩個機制在提高更新性能上的收益的話,redo log 主要節省的是隨機寫磁盤的 IO 消耗(轉成順序寫),而 change buffer 主要節省的則是隨機讀磁盤的 IO 消耗。

    優化器如何選擇索引

            優化器結合是否掃描行數、是否使用臨時表、是否排序等因素進行綜合判斷。

            MySQL 在真正開始執行語句以前,並不能精確地知道知足條件的記錄有多少條,而只能根據統計信息來估算記錄數。

            這個統計信息就是索引的「區分度」。顯然,一個索引上不一樣的值越多,這個索引的區分度就越好。而一個索引上不一樣的值的個數,稱之爲「基數」(cardinality)。也就是說,這個基數越大,索引的區分度越好。

            可使用 show index 方法,看到一個索引的基數。
    clipboard.png

            MySQL 採樣統計的方法得到基數,InnoDB 默認會選擇 N 個數據頁,統計這些頁面上的不一樣值,獲得一個平均值,而後乘以這個索引的頁面數,就獲得了這個索引的基數。當變動的數據行數超過 1/M 的時候,會自動觸發從新作一次索引統計。analyze table t 命令,能夠用來從新統計索引信息。

    在 MySQL 中,有兩種存儲索引統計的方式,能夠經過設置參數 innodb_stats_persistent 的值來選擇:

    • 設置爲 on 的時候,表示統計信息會持久化存儲。這時,默認的 N 是 20,M 是 10。
    • 設置爲 off 的時候,表示統計信息只存儲在內存中。這時,默認的 N 是 8,M 是 16。

    其實索引統計只是一個輸入,對於一個具體的語句來講,優化器還要判斷,執行這個語句自己要掃描多少行。

    clipboard.png
    rows 這個字段表示的是預計掃描行數。

            少數狀況下優化器會選錯索引,第一種方法能夠採用 force index 強行選擇一個索引。

            但其實使用 force index 最主要的問題仍是變動的及時性。由於選錯索引的狀況仍是比較少出現的,因此開發的時候一般不會先寫上 force index。而是等到線上出現問題的時候,纔會再去修改 SQL 語句、加上 force index。可是修改以後還要測試和發佈,對於生產系統來講,這個過程不夠敏捷。

            因此,數據庫的問題最好仍是在數據庫內部來解決。既然優化器放棄了使用索引 a,說明 a 還不夠合適,因此第二種方法就是,能夠考慮修改語句,引導 MySQL 使用指望的索引。好比,在這個例子裏,顯然把「order by b limit 1」 改爲 「order by b,a limit 1」 ,語義的邏輯是相同的。

            以前優化器選擇使用索引 b,是由於它認爲使用索引 b 能夠避免排序(b 自己是索引,已是有序的了,若是選擇索引 b 的話,不須要再作排序,只須要遍歷),因此即便掃描行數多,也斷定爲代價更小。

            如今 order by b,a 這種寫法,要求按照 b,a 排序,就意味着使用這兩個索引都須要排序。所以,掃描行數成了影響決策的主要條件,因而此時優化器選了只須要掃描 1000 行的索引 a。

            固然,這種修改並非通用的優化手段,可能修改語義這件事兒不太好,能夠用 limit 100 讓優化器意識到,使用 b 索引代價是很高的。實際上是根據數據特徵誘導了一下優化器,也不具有通用性。

    select from (select from t where (a between 1 and 1000) and (b between 50000 and 100000) order by b limit 100)alias limit 1;

            第三種方法是:在有些場景下,能夠新建一個更合適的索引,來提供給優化器作選擇,或刪掉誤用的索引。

            對索引字段作函數操做,可能會破壞索引值的有序性,所以優化器就決定放棄走樹搜索功能。

    1. 條件字段函數操做

      select count(*) from tradelog where month(t_modified)=7;

      同理 where id+1=1000 也不會用索引,改爲 where id =1000 - 1 會用索引。

    2. 隱式類型轉換

      select * from tradelog where tradeid=110717; (tradeid 是varchar)

      等同於 select * from tradelog where CAST(tradid AS signed int) = 110717;

    3. 隱式字符編碼轉換

      select * from trade_detail where tradeid=$L2.tradeid.value;

      $L2.tradeid.value 的字符集是 utf8mb4。字符集 utf8mb4 是 utf8 的超集,因此當這兩個類型的字符串在作比較的時候,MySQL 內部的操做是,先把 utf8 字符串轉成 utf8mb4 字符集,再作比較。

      至關於 select * from trade_detail where CONVERT(traideid USING utf8mb4)=$L2.tradeid.value;

    全局鎖和表鎖

    全局鎖

            顧名思義,全局鎖就是對整個數據庫實例加鎖。MySQL 提供了一個加全局讀鎖的方法,命令是 Flush tables with read lock (FTWRL)。當須要讓整個庫處於只讀狀態的時候,可使用可使用這個命令,以後其餘線程的如下語句會被阻塞:數據更新語句(數據的增刪改)、數據定義語句(包括建表、修改表結構等)和更新類事務的提交語句。

            全局鎖的典型使用場景是,作全庫邏輯備份。也就是把整庫每一個表都 select 出來存成文本。

            經過 FTWRL 確保不會有其餘線程對數據庫作更新,而後對整個庫作備份。在備份過程當中整個庫徹底處於只讀狀,這是很危險的。可是不加鎖,備份的數據會有不一致的問題。

            能夠拿到一個一致性視圖來備份,官方自帶的邏輯備份工具是 mysqldump。當 mysqldump 使用參數–single-transaction 的時候,導數據以前就會啓動一個事務,來確保拿到一致性視圖。而因爲 MVCC 的支持,這個過程當中數據是能夠正常更新的。

            那爲何還須要FTWRL呢,由於一致性讀是好,但前提是引擎要支持這個隔離級別。對於 MyISAM 這種不支持事務的引擎,就須要使用 FTWRL 命令了。

            既然要全庫只讀,爲何不使用 set global readonly=true 的方式呢?確實 readonly 方式也可讓全庫進入只讀狀態,但仍是建議用 FTWRL 方式,主要有兩個緣由:

    • 在有些系統中,readonly 的值會被用來作其餘邏輯,好比用來判斷一個庫是主庫仍是備庫。所以,修改 global 變量的方式影響面更大,不建議使用。
    • 在異常處理機制上有差別。若是執行 FTWRL 命令以後因爲客戶端發生異常斷開,那麼 MySQL 會自動釋放這個全局鎖,整個庫回到能夠正常更新的狀態。而將整個庫設置爲 readonly 以後,若是客戶端發生異常,則數據庫就會一直保持 readonly 狀態,這樣會致使整個庫長時間處於不可寫狀態,風險較高。

    表級鎖

            MySQL 裏面表級別的鎖有兩種:一種是表鎖,一種是元數據鎖(meta data lock,MDL)。

            表鎖的語法是 lock tables … read/write。與 FTWRL 相似,能夠用 unlock tables 主動釋放鎖,也能夠在客戶端斷開的時候自動釋放。須要注意,lock tables 語法除了會限制別的線程的讀寫外,也限定了本線程接下來的操做對象。

            對於 InnoDB 這種支持行鎖的引擎,通常不使用 lock tables 命令來控制併發,畢竟鎖住整個表的影響面仍是太大。

            另外一類表級的鎖是 MDL(metadata lock)。MDL 不須要顯式使用,在訪問一個表的時候會被自動加上。MDL 的做用是,保證讀寫的正確性。能夠想象一下,若是一個查詢正在遍歷一個表中的數據,而執行期間另外一個線程對這個表結構作變動,刪了一列,那麼查詢線程拿到的結果跟表結構對不上,確定是不行的。

    ​        所以,在 MySQL 5.5 版本中引入了 MDL,當對一個表作增刪改查操做的時候,加 MDL 讀鎖;當要對錶作結構變動操做的時候,加 MDL 寫鎖。

    • 讀鎖之間不互斥,所以能夠有多個線程同時對一張表增刪改查。
    • 讀寫鎖之間、寫鎖之間是互斥的,用來保證變動表結構操做的安全性。

    安全的給表增長字段

    ​        有幾個請求在讀寫表,會加上MDL讀鎖,而後修改表字段的請求會被blocked,請求MDL寫鎖,這個時候,後面的所有讀寫請求都會被MDL寫鎖 blocked,若是查詢語句頻繁,並且客戶端有重試機制,也就是說超時後會再起一個新 session 再請求的話,這個庫的線程很快就會爆滿。

    那麼如何安全的給表加字段呢?

            首先要解決長事務,事務不提交,就會一直佔着 MDL 鎖。在 MySQL 的 information_schema 庫的 innodb_trx 表中,能夠查到當前執行中的事務。若是要作 DDL 變動的表恰好有長事務在執行,要考慮先暫停 DDL,或者 kill 掉這個長事務。

            其次,在 alter table 語句裏面設定等待時間,若是在這個指定的等待時間裏面可以拿到 MDL 寫鎖最好,拿不到也不要阻塞後面的業務語句,先放棄。以後開發人員或者 DBA 再經過重試命令重複這個過程。

    ALTER TABLE tbl_name NOWAIT add column ...
    ALTER TABLE tbl_name WAIT N add column ...

    行鎖

            MyISAM 引擎就不支持行鎖。不支持行鎖意味着併發控制只能使用表鎖,對於這種引擎的表,同一張表上任什麼時候刻只能有一個更新在執行,這就會影響到業務併發度。InnoDB 是支持行鎖的,這也是 MyISAM 被 InnoDB 替代的重要緣由之一。

            在 InnoDB 事務中,行鎖是在須要的時候才加上的,但並非不須要了就馬上釋放,而是要等到事務結束時才釋放。這個就是兩階段鎖協議。

    ​        若是事務中須要鎖多個行,要把最可能形成鎖衝突、最可能影響併發度的鎖儘可能日後放。

    死鎖

            當併發系統中不一樣線程出現循環資源依賴,涉及的線程都在等待別的線程釋放資源時,就會致使這幾個線程都進入無限等待的狀態,稱爲死鎖。這裏用數據庫中的行鎖舉個例子。
    clipboard.png
            這時候,事務 A 在等待事務 B 釋放 id=2 的行鎖,而事務 B 在等待事務 A 釋放 id=1 的行鎖。 事務 A 和事務 B 在互相等待對方的資源釋放,就是進入了死鎖狀態。當出現死鎖之後,有兩種策略:

    • 一種策略是,直接進入等待,直到超時。這個超時時間能夠經過參數 innodb_lock_wait_timeout 來設置。

      設置時間長,等待時間太長;設置時間短,有的長事務,不是死鎖的也會結束。
    • 另外一種策略是,發起死鎖檢測,發現死鎖後,主動回滾死鎖鏈條中的某一個事務,讓其餘事務得以繼續執行。將參數 innodb_deadlock_detect 設置爲 on,表示開啓這個邏輯。

      每一個新來的被堵住的線程,都要判斷會不會因爲本身的加入致使了死鎖,這是一個時間複雜度是 O(n) 的操做。會耗費大量的CPU資源。

    慢SQL問題排查

    使用 show processlist 命令查看 Waiting for table metadata lock 的示意圖。
    clipboard.png
    這個狀態表示的是,如今有一個線程正在表 t 上請求或者持有 MDL 寫鎖,把 select 語句堵住了。

            經過查詢 sys.schema_table_lock_waits 這張表,就能夠直接找出形成阻塞的 process id,把這個鏈接用 kill 命令斷開便可。

    經過 sys.innodb_lock_waits 查行鎖

    select * from t sys.innodb_lock_waits where locked_table= 'test'.'t'G
    clipboard.png
    這個信息很全,4 號線程是形成堵塞的罪魁禍首。而幹掉這個罪魁禍首的方式,就是 KILL QUERY 4 或 KILL 4。實際上,這裏 KILL 4 纔有效。

    其餘

    count(*) 語句分析

            MyISAM 引擎把一個表的總行數存在了磁盤上,所以執行 count(*) 的時候會直接返回這個數,效率很高;

            InnoDB 引擎就麻煩了,執行 count(*) 的時候,須要把數據一行一行地從引擎裏面讀出來,而後累積計數。由於多版本併發控制(MVCC)的緣由,InnoDB 表「應該返回多少行」也是不肯定的。

            count() 是一個聚合函數,對於返回的結果集,一行行地判斷,若是 count 函數的參數不是 NULL,累計值就加 1,不然不加。最後返回累計值。
            因此,count(*)、count(主鍵 id) 和 count(1) 都表示返回知足條件的結果集的總行數;而 count(字段),則表示返回知足條件的數據行裏面,參數「字段」不爲 NULL 的總個數。

            按照效率排序的話,count(字段) < count(主鍵id) < count(1) < count(*),因此建議,儘可能使用count(*)。

    order by 語句分析

    ​        MySQL 會給每一個線程分配一塊內存用於快速排序,稱爲 sort_buffer

    ​        explain 結果裏的 Extra 這個字段中的「Using filesort」表示的就是須要排序。

    ​        sort_buffer_size,就是 MySQL 爲排序開闢的內存(sort_buffer)的大小。若是要排序的數據量小於 sort_buffer_size,排序就在內存中完成。但若是排序數據量太大,內存放不下,則不得不利用磁盤臨時文件輔助排序。

            創建聯合索引,甚至覆蓋索引,能夠避免排序過程。

    join 語句分析

    ​        直接使用 join 語句,MySQL 優化器可能會選擇表 t1 或 t2 做爲驅動表,改用 straight_join 讓 MySQL 使用固定的鏈接方式執行查詢,這樣優化器只會按照指定的方式去 join。

    select * from t1 straight_join t2 on (t1.a=t2.a);

    clipboard.png
            在這條語句裏,被驅動表 t2 的字段 a 上有索引,join 過程用上了這個索引,所以效率是很高的。稱之爲「Index Nested-Loop Join」,簡稱 NLJ。

    ​        若是被驅動表 t2 的字段 a 上沒有索引,那每次到 t2 去匹配的時候,就要作一次全表掃描。這個效率很低。這個算法叫作「Simple Nested-Loop Join」的算法,簡稱 BNL。

    ​        因此在判斷要不要使用 join 語句時,就是看 explain 結果裏面,Extra 字段裏面有沒有出現「Block Nested Loop」字樣。

            在決定哪一個表作驅動表的時候,應該是兩個表按照各自的條件過濾,過濾完成以後,計算參與 join 的各個字段的總數據量,數據量小的那個表,就是「小表」,應該做爲驅動表。

            Multi-Range Read 優化,這個優化的主要目的是儘可能使用順序讀盤。由於大多數的數據都是按照主鍵遞增順序插入獲得的,因此能夠認爲,若是按照主鍵的遞增順序查詢的話,對磁盤的讀比較接近順序讀,可以提高讀性能。

    select * from t1 where a>=1 and a<=100;

    clipboard.png
            Batched Key Access(BKA) 算法。這個 BKA 算法,其實就是對 NLJ 算法的優化。

            NLJ 算法執行的邏輯是:從驅動表 t1,一行行地取出 a 的值,再到被驅動表 t2 去作 join。也就是說,對於表 t2 來講,每次都是匹配一個值。這時,MRR 的優點就用不上了。

    ​        既然如此,就把表 t1 的數據取出來一部分,先放到一個臨時內存。這個臨時內存就是 join_buffer。

    自增主鍵

    clipboard.png
            表定義裏面出現了一個 AUTO_INCREMENT=2,表示下一次插入數據時,若是須要自動生成自增值,會生成 id=2。

    實際上,表的結構定義存放在後綴名爲.frm 的文件中,可是並不會保存自增值。

    • MyISAM 引擎的自增值保存在數據文件中。
    • InnoDB 引擎的自增值,實際上是保存在了內存裏,MySQL 8.0 版本後,纔有了「自增值持久化」的能力。

      • MySQL 5.7 及以前的版本,自增值保存在內存裏,並無持久化。每次重啓後,第一次打開表的時候,都會去找自增值的最大值 max(id),而後將 max(id)+1 做爲這個表當前的自增值。
      • MySQL 8.0 版本,將自增值的變動記錄在了 redo log 中,重啓的時候依靠 redo log 恢復重啓以前的值。

    自增值修改機制

    • 若是插入數據時 id 字段指定爲 0、null 或未指定值,那麼就把這個表當前的 AUTO_INCREMENT 值填到自增字段;
    • 若是插入數據時 id 字段指定了具體的值 X ,就直接使用語句裏指定的值 Y。

      • 若是 X < Y,那麼這個表的自增值不變;
      • 若是 X≥Y,就須要把當前自增值修改成新的自增值。

            新的自增值生成算法是:從 auto_increment_offset 開始,以 auto_increment_increment 爲步長,持續疊加,直到找到第一個大於 X 的值,做爲新的自增值。

    自增值的修改時機

    1. 執行器調用 InnoDB 引擎接口寫入一行,傳入的這一行的值(0,1,1);
    2. InnoDB 發現用戶沒有指定自增 id 的值,獲取表 t 當前的自增值 2;
    3. 將傳入的行的值改爲 (2,1,1);
    4. 將表的自增值改爲 3;
    5. 繼續執行插入數據操做,因爲已經存在 c=1 的記錄,因此報 Duplicate key error,語句返回。

            因此,sql執行報錯了,自增值已經改變了,惟一鍵衝突是致使自增主鍵 id 不連續的第一種緣由。一樣地,事務回滾也會產生相似的現象,這就是第二種緣由。

            批量插入的時候,因爲系統預先不知道要申請多少個自增 id,因此就先申請一個,而後兩個,而後四個,直到夠用。這是主鍵 id 出現自增 id 不連續的第三種緣由。

    自增id用完怎麼辦

    一、主鍵id

            再申請下一個 id 時,獲得的值保持不變。因此到最大值以後,再申請id,因爲id不變,因此插入會報主鍵衝突,若是數據量比較大,主鍵id應該用 bigint unsigned。默認是無符號整型 (unsigned int) ,4 個字節232-1(4294967295)。

    二、系統row_id

            若是建立的 InnoDB 表沒有指定主鍵,那麼 InnoDB 會建立一個不可見的,長度爲 6 個字節的 row_id。InnoDB 維護了一個全局的 dict_sys.row_id 值,全部無主鍵的 InnoDB 表,每插入一行數據,都把當前的 dict_sys.row_id 值做爲要插入數據的 row_id,而後把 dict_sys.row_id 的值加 1。

            實際上,在代碼實現時 row_id 是一個長度爲 8 字節的無符號長整型 (bigint unsigned)。可是,InnoDB 在設計時,給 row_id 留的只是 6 個節的長度,這樣寫到數據表中時只放了最後 6 個字節,因此 row_id 能寫到數據表中的值,就有兩個特徵:

            248-1到 264 之間,row_id 會是0,264 以後會從0開始。

            在 InnoDB 邏輯裏,申請到 row_id=N 後,就將這行數據寫入表中;若是表中已經存在 row_id=N 的行,新寫入的行就會覆蓋原有的行。

            覆蓋數據,就意味着數據丟失,影響的是數據可靠性;報主鍵衝突,是插入失敗,影響的是可用性。而通常狀況下,可靠性優先於可用性。

    三、Xid

            redo log 和 binlog 相配合的時候,提到了有一個共同的字段叫做 Xid。它在 MySQL 中是用來對應事務的。

            MySQL 內部維護了一個全局變量 global_query_id,每次執行語句的時候將它賦值給 Query_id,而後給這個變量加 1。若是當前語句是這個事務執行的第一條語句,那麼 MySQL 還會同時把 Query_id 賦值給這個事務的 Xid。

            而 global_query_id 是一個純內存變量,重啓以後就清零了。因此就知道了,在同一個數據庫實例中,不一樣事務的 Xid 也是有可能相同的。

            可是 MySQL 重啓以後會從新生成新的 binlog 文件,這就保證了,同一個 binlog 文件裏,Xid 必定是唯一的。

            可是 global_query_id 定義的長度是 8 個字節,這個自增值的上限是 264-1。理論上也是可能重複的。

    四、trx_id

            Xid 是由 server 層維護的。InnoDB 內部使用 Xid,就是爲了可以在 InnoDB 事務和 server 之間作關聯。可是,InnoDB 本身的 trx_id,是另外維護的。

            InnoDB 內部維護了一個 max_trx_id 全局變量,每次須要申請一個新的 trx_id 時,就得到 max_trx_id 的當前值,而後並將 max_trx_id 加 1。

            InnoDB 數據可見性的核心思想是:每一行數據都記錄了更新它的 trx_id,當一個事務讀到一行數據的時候,判斷這個數據是否可見的方法,就是經過事務的一致性視圖與這行數據的 trx_id 作對比。

            對於正在執行的事務,能夠從 information_schema.innodb_trx 表中看到事務的 trx_id。

    ​ update 和 delete 語句除了事務自己,還涉及到標記刪除舊數據,也就是要把數據放到 purge 隊列裏等待後續物理刪除,這個操做也會把 max_trx_id+1, 所以在一個事務中至少加 2;

    ​ InnoDB 的後臺操做,好比表的索引信息統計這類操做,也是會啓動內部事務的,所以你可能看到,trx_id 值並非按照加 1 遞增的。

            只讀事務會分配一個特殊的,比較大的id,把當前事務的 trx 變量的指針地址轉成整數,再加上 248,使用這個算法,就能夠保證如下兩點:

    1. 由於同一個只讀事務在執行期間,它的指針地址是不會變的,因此不管是在 innodb_trx 仍是在 innodb_locks 表裏,同一個只讀事務查出來的 trx_id 就會是同樣的。
    2. 若是有並行的多個只讀事務,每一個事務的 trx 變量的指針地址確定不一樣。這樣,不一樣的併發只讀事務,查出來的 trx_id 就是不一樣的。

            加上248是爲了保證只讀事務顯示的 trx_id 值比較大,正常狀況下就會區別於讀寫事務的 id。理論狀況下也可能只讀事務與讀寫事務相等,可是沒有影響。

            max_trx_id 會持久化存儲,重啓也不會重置爲 0,那麼從理論上講,只要一個 MySQL 服務跑得足夠久,就可能出現 max_trx_id 達到 248-1 的上限,而後從 0 開始的狀況。當達到這個狀態後,MySQL 就會持續出現一個髒讀的 bug。由於後續的trx_id確定比末尾那些trx_id大,能看到這些數據。

    五、thread_id

            系統保存了一個全局變量 thread_id_counter,每新建一個鏈接,就將 thread_id_counter 賦值給這個新鏈接的線程變量。定義的大小是 4 個字節,所以達到 232-1 後,它就會重置爲 0,而後繼續增長。可是,在 show processlist 裏不會看到兩個相同的 thread_id。由於 MySQL 設計了一個惟一數組的邏輯,給新線程分配 thread_id 的時候,邏輯代碼是這樣的:

    do {
            new_id= thread_id_counter++;
    } while (!thread_ids.insert_unique(new_id).second);

    誤刪數據怎麼辦

    1. delete 語句誤刪數據行:Flashback工具過閃回把數據恢復回來。 原理是修改 binlog 的內容,拿回原庫重放。而可以使用這個方案的前提是,須要確保 binlog_format=row 和 binlog_row_image=FULL。

      如何預防:把 sql_safe_updates 參數設置爲 on。,delete 或者 update 語句必須有where條件,不然執行會報錯。
    1. 誤刪庫 / 表:全量備份,加增量日誌,在應用日誌的時候,須要跳過 12 點誤操做的那個語句的 binlog:

      1. 若是原實例沒有使用 GTID 模式,只能在應用到包含 12 點的 binlog 文件的時候,先用–stop-position 參數執行到誤操做以前的日誌,而後再用–start-position 從誤操做以後的日誌繼續執行;
      2. 若是實例使用了 GTID 模式,就方便多了。假設誤操做命令的 GTID 是 gtid1,那麼只須要執行 set gtid_next=gtid1;begin;commit; 先把這個 GTID 加到臨時實例的 GTID 集合,以後按順序執行 binlog 的時候,就會自動跳過誤操做的語句。

        如何加速恢復:使用 mysqlbinlog 命令時,加上一個–database 參數,用來指定誤刪表所在的庫。
        在 start slave 以前,先經過執行 change replication filter replicate_do_table = (tbl_name) 命令,就可讓臨時庫只同步誤操做的表;

            延遲複製備庫,通常的主備複製結構存在的問題是,若是主庫上有個表被誤刪了,這個命令很快也會被髮給全部從庫,進而致使全部從庫的數據表也都一塊兒被誤刪了。延遲複製的備庫是一種特殊的備庫,經過 CHANGE MASTER TO MASTER_DELAY = N 命令,能夠指定這個備庫持續保持跟主庫有 N 秒的延遲。

            好比把 N 設置爲 3600,這就表明了若是主庫上有數據被誤刪了,而且在 1 小時內發現了這個誤操做命令,這個命令就尚未在這個延遲複製的備庫執行。這時候到這個備庫上執行 stop slave,再經過以前介紹的方法,跳過誤操做命令,就能夠恢復出須要的數據。

    預防誤刪庫 / 表的方法,制定操做規範。這樣作的目的,是避免寫錯要刪除的表名。

    1. 在刪除數據表以前,必須先對錶作更名操做。而後,觀察一段時間,確保對業務無影響之後再刪除這張表。
    2. 改表名的時候,要求給表名加固定的後綴(好比加_to_be_deleted),而後刪除表的動做必須經過管理系統執行。而且,管理系刪除表的時候,只能刪除固定後綴的表。

    刪除數據,爲什麼表文件大小不變

            delete 命令其實只是把記錄的位置,或者數據頁標記爲了「可複用」,但磁盤文件的大小是不會變的。也就是說,經過 delete 命令是不能回收表空間的。這些能夠複用,而沒有被使用的空間,看起來就像是「空洞」。

            實際上,不止是刪除數據會形成空洞,插入數據也會。若是數據是隨機插入的,就可能形成索引的數據頁分裂。更新索引上的值,能夠理解爲刪除一箇舊的值,再插入一個新值。不難理解,這也是會形成空洞的。

            也就是說,通過大量增刪改的表,都是多是存在空洞的。因此,若是可以把這些空洞去掉,就能達到收縮表空間的目的。而重建表,就能夠達到這樣的目的。

            使用 alter table A engine=InnoDB 命令來重建表。MySQL 會自動完成轉存數據、交換表名、刪除舊錶的操做。

            重建表的時候,InnoDB 不會把整張表佔滿,每一個頁留了 1/16 給後續的更新用。也就是說,其實重建表以後不是「最」緊湊的。

    怎麼複製一張表

    一、mysqldump 方法

    使用 mysqldump 命令將數據導出成一組 INSERT 語句。你可使用下面的命令:

    mysqldump -h$host -P$port -u$user --add-locks=0 --no-create-info --single-transaction --set-gtid-purged=OFF db1 t --where="a>900" --result-file=/client_tmp/t.sql

    而後能夠經過下面這條命令,將這些 INSERT 語句放到 db2 庫裏去執行。

    mysql -h127.0.0.1 -P13000 -uroot db2 -e "source /client_tmp/t.sql"

    二、導出 CSV 文件

    直接將結果導出成.csv 文件。MySQL 提供了下面的語法,用來將查詢結果導出到服務端本地目錄:

    select * from db1.t where a>900 into outfile '/server_tmp/t.csv';

    而後用下面的 load data 命令將數據導入到目標表 db2.t 中。

    load data infile '/server_tmp/t.csv' into table db2.t;

    三、物理拷貝方法

    直接拷貝文件是不行的,須要在數據字典中註冊。

    MySQL 5.6 版本引入了可傳輸表空間(transportable tablespace) 的方法,,能夠經過導出 + 導入表空間的方式,實現物理拷貝表的功能。

    相關文章
    相關標籤/搜索