高性能MySQL分析 Schema與數據類型優化 Scheme設計中的陷阱 創建高性能索引 查詢性能優化 分區表 MySQL優化服務器配置 文件 表 鎖

Schema與數據類型優化

選擇優化的數據類型

有幾個簡單的原則:

  • 更小的通常更好
    一般情況下使用可以正確存儲數據的最小數據類型。
  • 簡單的更好
    例如整型比字符操作代價更低。應當使用Mysql的日期類型而不是字符串,應當用整型存儲IP地址
  • 儘量避免NULL
    查詢中如果包含NULL的列,對於Mysql來說更難優化,這樣使得索引,索引統計,值都比較複雜。NULL的列會使用更多的存儲空間,在Mysql裏也需要特殊處理。當可爲NULL的列被索引時,每個索引記錄需要一個額外的字節

整數類型
無符號的數字上限可以提高一倍
爲整數類型指定寬度,如INT(11),不會限制值的合法範圍,只是規定了Mysql的一些交互工具(命令行或客戶端)用來顯示的字符個數。對於存儲和計算來講,int(1) 和 int(20)是相同的。

實數類型
浮點類型在存儲同樣範圍的值時,通常比Decimal使用更少的空間,Float使用4個字節,Double使用8個字節相比Float有更高的精度和更大的範圍。這裏能選擇的是存儲類型,Mysql內部使用Double作爲內部浮點計算的類型。

字符串類型
CHAR和VARCHAR
VARCHAR節省了存儲空間,如果行佔用存儲空間增長,並且在頁內沒有更多的空間存儲,MyISAM拆成不同的片段存儲,InnoDB則需要分裂頁來使行可以放進頁內。
下列情況使用Varchar是合適的:

  • 字符串最大長度比平均長度大很多;
  • 列的更新很少,所以碎片不是問題;
  • 使用了UTF-8字符集,每個字符都使用不同的字節數進行存儲。

InnoDB把過長的VARCHAR存儲爲BLOB
CHAR是定長的,會刪除末尾的空格。CHAR(1)需要一個字節,VARCHAR(1)需要2個字節,因爲還需要多一個字節存儲長度。
類似的還有BINNARY和VARBINARY,填充使用的\0(0字節)

BLOB和TEXT
都是爲了存儲很大的數據設計的字符串數據類型,分別採用二進制和字符方式存儲。不同在於BLOB存儲的是二進制數據,沒有排序規則或者字符集。
排序也只是對每個列的max_sort_length字節而不是整個字符串排序。
查詢如果涉及BLOB,服務器不能在內存臨時表中存儲BLOB,必須要使用磁盤臨時表,無論它多小。

日期和時間類型
DATETIME可以存儲1001到9999年,精度爲秒,與時區無關,使用8個字節的存儲空間。TIMESTAMP保存了1970年1月1日以來的秒數。只使用4個字節的存儲空間。從1970到2038年。

位數據類型
這些類型,不管底層存儲格式和處理方式如何,從技術上來說都是字符串類型。
BIT
5.0之前BIT是TINYINT的同義詞。之後則完全不同。MyISAM會打包所有的BIT列,InnoDB和Memory使用足夠存儲最小整數類型來存放BIT,所以不能節省存儲空間。Mysql把BIT當作字符串類型而不是數字,會造成一些混亂。例如 a bit(8),值爲b'00111001'二進制等於57(ascii顯示值等於9),a=9,a+0=57。應該謹慎使用,如果想存儲true/false,可以使用CHAR(0)

選擇標識符(identifier)
整數類型是最好的選擇,很快並且可以使用AUTO_INCREMENT。避免使用字符串作爲標識列,很耗空間,通常比數字類型慢,MyISAM默認對字符串使用壓縮索引,會導致查詢慢很多。

  • 隨機值如MD5,SHA1,UUID會導致INSERT和一些SELECT語句變慢,因爲可能導致隨機寫入索引不同位置,導致頁分裂,磁盤隨機訪問,對於聚簇存儲引擎產生聚簇索引碎片。
  • SELECT語句變慢因爲邏輯上相鄰的行會分佈在磁盤和內存的不同地方。
  • 隨機值導致緩存對所有類型的查詢語句效果都很差。

Scheme設計中的陷阱

  • 太多的列
    Mysql的存儲引擎API工作時需要在服務器層和存儲引擎層之間通過行緩衝格式拷貝數據,然後在服務器層將緩衝內容解碼成各個列。從行緩衝中將編碼過的列轉換成行數據結構的操作代價是非常高的。非常寬的表可能會使得CPU佔用非常高。
  • 太多的關聯
    “實體-屬性-值”(EAV)設計模式在Mysql下不能靠譜的工作,限制了每個關聯操作最多隻能有61張表。單個查詢最好在12個表內做關聯。
  • 全能的枚舉
    枚舉列表增加數據需要使用到ALTER TABLE,若不是加在最後可能會有影響
  • 變相的枚舉

範式和反範式
在範式化的數據庫中,每個事實數據會出現並且只出現一次,相反,在反範式化的數據庫中,信息是冗餘的。

  1. 第一範式
    確保數據表中每列(字段)的原子性。
    如果數據表中每個字段都是不可再分的最小數據單元,則滿足第一範式。
    例如:user用戶表,包含字段id,username,password

  2. 第二範式
    在第一範式的基礎上更進一步,目標是確保表中的每列都和主鍵相關。
    如果一個關係滿足第一範式,並且除了主鍵之外的其他列,都依賴於該主鍵,則滿足第二範式。
    例如:一個用戶只有一種角色,而一個角色對應多個用戶。則可以按如下方式建立數據表關係,使其滿足第二範式。
    user用戶表,字段id,username,password,role_id
    role角色表,字段id,name
    用戶表通過角色id(role_id)來關聯角色表

  3. 第三範式
    在第二範式的基礎上更進一步,目標是確保表中的列都和主鍵直接相關,而不是間接相關。
    例如:一個用戶可以對應多個角色,一個角色也可以對應多個用戶。則可以按如下方式建立數據表關係,使其滿足第三範式。
    user用戶表,字段id,username,password
    role角色表,字段id,name
    user_role用戶-角色中間表,id,user_id,role_id
    像這樣,通過第三張表(中間表)來建立用戶表和角色表之間的關係,同時又符合範式化的原則,就可以稱爲第三範式。

  4. 反範式化
    反範式化指的是通過增加冗餘或重複的數據來提高數據庫的讀性能。
    例如:在上例中的user_role用戶-角色中間表增加字段role_name。
    反範式化可以減少關聯查詢時,join表的次數。

範式的優點

  • 範式化的更新操作更快
  • 更新需要變更的數據更少
  • 表比較小,可以更好放在內存裏

缺點是通常需要關聯,代價相對昂貴,也可能使得一些索引策略無效。

反範式的優點
避免關聯
查詢相對高效(當索引合理)

創建高性能索引

索引可以包含一個或多個列,如果索引包含多個列,那列的順序也十分重要,因爲Mysql只能最高效的使用索引的最左前綴列。

B-Tree的索引列是順序組織存儲的,很適合查找範圍數據。適用於全鍵值、鍵值範圍或鍵前綴查找。

紅黑樹是一種含有紅黑結點並能自平衡的二叉查找樹。它必須滿足下面性質:
性質1:每個節點要麼是黑色,要麼是紅色。
性質2:根節點是黑色。
性質3:每個葉子節點(NIL)是黑色。
性質4:每個紅色結點的兩個子結點一定都是黑色。
性質5:任意一結點到每個葉子結點的路徑都包含數量相同的黑結點。
從性質5又可以推出:
性質5.1:如果一個結點存在黑子結點,那麼該結點肯定有兩個子結點

哈希索引(hash index)只有精確匹配索引所有列的查詢纔有效。只包含哈希值和行指針,不存儲字段值,所以不能避免讀取行。
並不是按照索引值順序存儲,所以無法用於排序。
也不支持部分索引列匹配查找。只支持等值查詢,不支持範圍查詢。

高性能的索引策略

獨立的列才能使用到索引,列不能使用操作符或者表達式
多列索引,當使用到多個單列索引時,會進行多個索引的聯合操作(索引合併)

選擇合適的索引列順序
正確的順序依賴於使用該索引的查詢,並且同時需要考慮如何更好地滿足排序和分組的需要。
在一個多列B-Tree索引中,索引列的順序意味着索引首先按照最左列進行排序,其次是第二列。

聚簇索引
並非一種單獨的索引類型,而是一種數據存儲方式。InnoDB在同一個結構中保存了B-Tree索引和數據行。
InnoDB使用主鍵聚集數據,如果沒有定義主鍵,會選擇一個唯一的非空索引代替,如果沒有這樣的索引,會隱式定義一個主鍵作爲聚簇索引。InnoDB只聚集同一個頁面的記錄。
優點:

  • 把相關數據保存再一起。
  • 數據訪問更快
  • 使用覆蓋索引掃描的查詢可以直接使用節點中的主鍵值。

缺點:

  • 插入速度依賴於插入順序,如果不是按照主鍵加載數據,加載完成後最好使用OPTIMIZE TABLE重新組織表
  • 更新聚簇索引的代價很高,因爲會將被更新的行移動到新位置
  • 插入新航或者主鍵更新需要移動行時,可能面臨“頁分裂(Page Split)”問題
  • 可能導致全表掃描變慢,尤其是行比較稀疏
  • 二級索引(非聚簇索引)可能比想象的要更大,因爲葉子節點包含了引用行的主鍵列。
  • 二級索引需要兩次索引查找,而不是一次

覆蓋索引
如果索引的葉子節點中已經包含要查詢的數據,那麼還有什麼必要再回表查詢呢?所以一個索引包含(或者覆蓋)所有需要查詢的字段的值,我們就稱之爲覆蓋索引。

索引排序
只有索引的列順序和orderby的順序完全一致,並且列的正序,逆序都一樣時,才能使用索引對結果進行排序。如果查詢需要關聯多張表,則只有當orderby的引用字段全部爲第一個表時,才能使用索引進行排序。

索引和數據的碎片化
B-Tree索引可能會碎片化。
表的數據存儲也可能碎片化:
行碎片
這種碎片指的時數據行被存儲到多個地方的多個片段中。即使只查詢一行記錄,也會導致性能下降。
行間碎片
邏輯上順序的頁,或者行再磁盤上不是順序存儲的。行間碎片對諸如全表掃描和聚簇索引掃描之類的操作有很大影響。
剩餘空間碎片
指數據頁中有大量的空餘空間,會導致服務器讀取大量不需要的數據造成浪費。

查詢性能優化

查詢的聲明週期大致按照順序:
從客戶端,到服務器,然後在服務器上進行解析,生成執行計劃,執行,並返回結果給客戶端。執行時最重要的階段,包含了大量爲檢索數據到存儲引擎的調用以及調用後的數據處理,包括排序,分組等。

慢查詢基礎:優化數據訪問

是否請求了不需要的數據

  • 查詢不需要的記錄
  • 查詢不需要的列 (多表關聯 * )
  • 總是取出全部列(select * )
  • 重複查詢相同的數據

是否在掃描額外的記錄

衡量查詢開銷的三個指標如下:

  • 響應時間
  • 掃描的行數
  • 返回的行數

響應時間是 服務時間排隊時間 之和。
掃描的行數和返回的行數理想情況下應該是相同的,一般在1:1到10:1之間
掃描的行數和訪問類型:在EXPAIN語句中的type列反應了訪問類型。訪問類型有很多中,包括全表掃描,索引掃描,範圍掃描,唯一索引查詢,常數引用等。這些速度是從慢到快,掃描行數也是從多到少。

重構查詢的方式

一個複雜查詢還是多個簡單查詢

Mysql支持多個簡單查詢,一個通用服務器上可以支持每秒10萬的查詢,一個千兆網卡滿足每秒2000次的查詢。Mysql內部每秒能掃描內存中上百萬行數據,相比之下響應數據給客戶端就慢得多了

切分查詢

將一個大查詢分而治之,例如一個刪除大量數據的語句,拆分爲多個小的刪除。

分解關聯查詢

有很多好處:

  • 讓緩存的效率更高。無論是應用程序的緩存和Mysql的緩存,都會在單表的情況下更容易命中。
  • 查詢分解後減少了鎖的競爭
  • 應用層關聯,更容易對數據庫進行拆分,做到高性能和可擴展
  • 減少冗餘記錄的查詢
  • 在應用中實現的哈希關聯,而不是使用Mysql的嵌套查詢。

執行查詢的基礎

執行查詢的過程:

  1. 客戶端發送一條查詢給服務器
  2. 服務器先檢查緩存,如果命中了緩存,則立刻返回存儲在緩存中的結果。否則進入下一個階段。
  3. 服務器進行SQL解析,預處理,再由優化器生成對應的執行計劃。
  4. Mysql根據優化器生成的執行計劃,調用存儲引擎的API執行查詢。
  5. 將結果返回給客戶端

Mysql客戶端/服務器通信協議

通信協議是“半雙工”的,意味着任何一個時刻,要麼是服務端向客戶端發送數據,要麼是客戶端向服務端發送數據。這種協議讓MySQL通信簡單快速。但是也意味着沒法進行流量控制,一旦一端開始發送消息,另一端要完整接收完整個消息才能響應它。客戶端用一個單獨的數據包將查詢傳給服務器,所以查詢語句特別長的時候,參數max_allowed_packet特別重要。

查詢狀態

最簡單使用SHOW FULL PROCESSLIST查看當前狀態,狀態值有如下幾種:

  • Sleep:線程正在等待客戶端發送新的請求。
  • Query:線程正在執行查詢或者將查詢結果返回客戶端。
  • Locked:服務器層線程等待表鎖。在存儲引擎基本實現的鎖,例如InnoDB的行所,不會體現在線程狀態中。
  • Analyzing and statistics:線程收集存儲引擎的統計信息,並生成查詢的執行計劃。
  • Copying to tmp table [on disk]:線程執行查詢,並將其結果集複製到一個臨時表中,這種狀態一般要麼是做GROUP BY操作,或者文件排序操作,或者UNION操作。如果後面有“on disk”標記表示MySQL將內存臨時表放到磁盤上。
  • Sorting result:線程在對結果集排序。
  • Sending data:線程可能在多個狀態之間傳送數據,或者在生成結果集,或者在向客戶端返回數據。

查詢緩存

檢查緩存是通過一個對大小寫敏感的哈希查找實現的。查詢和緩存中的查詢即使只有一個字節不同頁不會匹配,如果命中在返回結果集之前MySQL會檢查一次用戶權限,這是無需解析SQL的,因爲查詢緩存中有保存當前查詢需要的表信息。

查詢優化處理

語法解析器和預處理

MySQL通過關鍵字將SQL語句解析,生成語法解析樹,使用MySQL語法規則驗證和解析查詢。例如是否使用了錯誤的關鍵字,關鍵字順序是否正確,引號前後是否正確匹配。
預處理根據MySQL規則進一步檢查解析樹是否合法。例如數據表、列是否存在,名字和別名是否有歧義。
下一步預處理器會驗證權限。

查詢優化器

語法樹已經合法,優化器將其轉爲了執行計劃。優化器作用就是找到最好的執行計劃。
可以通過查詢當前回話的Last_query_cost的值來得知MySQL計算當前查詢成本。
根據一系列統計信息計算得來:每個表或者索引的頁面個數,索引的基數(索引中不同值的數量),索引和數據行的長度,索引分佈的情況。
優化器在評估成本的時候不考慮任何緩存,假設讀取任何數據都需要一次磁盤IO

MySQL的查詢優化器是一個複雜部件,使用了很多優化的執行策略。優化策略簡單分爲兩種:靜態優化和動態優化。
靜態優化直接對解析樹進行優化,靜態優化在第一次萬能充後就一直有效,使用不同參數執行查詢頁不會發生變化,可以認爲是一種“編譯時優化”。
動態優化和查詢的上下文有關,例如WHERE條件中的取值、索引中條目對應的數據行數等。可以認爲時“運行時優化”。
MySQL能夠處理的優化類型:

  • 重新定義關聯表的順序:數據表的關聯並不總是按照查詢中指定的順序執行
  • 將外連接轉爲內連接:MySQL識別並重寫查詢,讓其可以調整關聯順序。
  • 使用等價變化規則:通過等價變換來簡化並規範表達式。合併減少一些比較,一定一些恆等或者恆不等的判斷。
  • 優化Count() Max() Min():min和max可以直接查詢b-tree的最左或者最右端。
  • 預估並轉化位常數表達式:
  • 覆蓋索引掃描
  • 子查詢優化;某些情況下可以將子查詢轉換爲效率更高的形式
  • 提前終止查詢:在發現已經滿足查詢需求的時候,MySQL總是能夠立刻終止查詢。
  • 等值傳播:兩個列的值通過等值關聯,MySQL能夠傳遞where條件。
  • 列表in()的比較:MySQL將in()列表中的數據先進行排序,然後通過二分查找的方式來確定列表中的值是否滿足條件。這是一個O(log n)的操作。等價轉換爲Or的複雜度時O(n)。

MySQL執行關聯查詢

MySQL先從一個表中循環取出單條數據,在嵌套循環到下一個表中尋找匹配的行,依次直到找到所有表中匹配的行,然後根據各個表匹配的行返回查詢中需要的各個列。MySQL會嘗試在最後一個關聯表中找到所有匹配的行,如果不行就返回上一層次關聯表。
MySQL多表關聯的指令樹時一顆左側深度優先的樹。

關聯查詢優化器

MySQL的最優執行計劃中的關聯表的順序,通過預估需要讀取的數據頁來選擇,讀取的數據頁越少越好。
關聯順序的調整,可能會讓查詢進行更少的嵌套循環和回溯操作。
可以使用STRAIGHT_JOIN關鍵字重寫查詢,讓優化器按照查詢順序執行。

排序優化

排序時成本很高的操作,從性能角度考慮,應該儘量避免排序,或者避免對大量數據進行排序。
當不能用索引生成排序結果時,MySQL需要字節進行排序,如果數據量小使用內存,數據量大使用磁盤。不過統一都稱爲文件排序(filesort)。
MySQL有兩種排序算法:

  • 兩次傳輸排序(舊版本):讀取指針和需要排序的字段,排序之後,再根據排序結果讀取所需要的數據行。第二次讀取數據的時候可能產生大量隨機IOS,成本很高,不過在排序時加載的數據較少,所以在內存中就可以讀取更多的行數進行排序。
  • 單次傳輸排序(新版本):查詢所有需要列,根據給定列進行排序直接返回結果。在MySQL4.1之後引入。

查詢執行引擎

查詢執行階段就根據執行計劃,調用存儲引擎的實現接口來完成。
查詢結果返回時,即使不需要返回結果集給客戶端,MySQL返回查詢信息,例如影響到的行數。

查詢優化的侷限性

關聯子查詢(in+子查詢)
使用join,或者使用函數GROUP_CONCAT()在in中構造一個由分好分隔的列表,有時候比關聯更快,in加子查詢性能糟糕,一般建議使用exists等效改寫。

優化特定類型的查詢

優化count查詢

MyISAM的count函數非常快,只有在沒有條件的前提下。
近似值:某些不需要精確值的情況下,可以使用EXPLAIN出來的優化器估算行數。

優化關聯查詢

  • 確保on或者using子句中的列上有索引。
  • 確保任何的group by和order by中的表達式只設計一個表中的列,這樣MySQL纔有可能使用索引來優化過程

優化子查詢

在5.6之前儘量轉換使用join,5.6之後沒有太多差別

優化group by和distinct

groupby 使用主鍵列效率更高。

優化limit

“延遲關聯”,首先使用索引覆蓋來選取範圍內的主鍵,接下來根據這些主鍵獲取對應數據。

分區表

分區表限制:

  • 一個表最多隻能有1024個分區
  • 5.1中分區表達式必須是整數,或者是返回整數的表達式。5.5中某些場景可以直接使用列進行分區。
  • 如果分區字段中有主鍵或者唯一索引列,那麼所有的主鍵列和唯一索引列都必須包含進來。
  • 分區表中無法使用外鍵約束。

在數據量超大的時候B-Tree就無法起作用了,除非是索引覆蓋查詢,否則數據庫服務器需要根據索引掃描的結果回表,查詢所有符合條件的記錄。如果數據量巨大,這將產生大量隨機IO,數據庫的響應時間將大到不可接受的程度。

MySQL優化服務器配置

MySQL配置的工作原理

MySQL從 命令行參數配置文件中獲取配置信息。配置文件一般是在 /etc/my.cnf/etc/mysql/my.cnf
確認配置文件路徑,可以使用下列命令

$ which mysql
/bin/mysql
$/bin/mysql --verbose --help|grep -A 1 'Default options'
Default options are read from the following files in the given order:
/etc/my.cnf /etc/mysql/my.cnf /usr/etc/my.cnf ~/.my.cnf 

配置文件分爲多個部分,每個部分的開頭是用方括號括起來的分段名稱。客戶端會讀取client部分,服務器通常讀取mysqld部分。

配置項都使用小鞋,單次之間用下劃線或者橫線隔開。

常用變量及其效果

  • key_buffer_size
    一次性爲鍵緩衝區(key buffer)分配所有的指定空間。操作系統會在使用時才真正分配。
  • table_cache_size
    這個變量會等到下次有線程打開表纔有效果,會變更緩存中表的數量。
  • thread_cache_size
    MySQL只有再關閉連接時纔在緩存中增加線程,只在創建新連接時才從緩存中刪除線程。
  • query_cache_size
    修改這個變量會立刻刪除所有緩存的查詢,重新分配這片緩存到指定大小,並且重新初始化內存。
  • read_buffer_size
    MySQL只會在查詢需要使用時纔會爲該緩存分配內存,並且一次性分配該參數指定大小的全部內存。
  • read_rnd_buffer_size
    MySQL只會在查詢需要使用時纔會爲該緩存分配內存,並且只會分配該參數需要大小的內存。
  • sort_buffer_size
    MySQL只會在查詢排序需要使用時纔會爲該緩存分配內存,並且一次性分配該參數指定大小的全部內存,不管排序是否需要這想·麼大的內存。

InnoDB事務日誌

InnoDB使用日誌來減少提交事務時的開銷。因爲日誌中已經記錄了事務,無需在每個事務提交時把緩衝池的髒塊刷新到磁盤中。
InnoDB用日誌把隨機IO變成順序IO,一旦日誌寫入磁盤,事務就持久化了,即使變更還沒有寫到數據文件。
InnoDB最後是要把變更寫入數據文件,日誌有固定大小。InnoDB的日誌是環形方式寫的:當寫到日誌的尾部,會重新跳轉到開頭繼續寫,但不會覆蓋到還沒應用到數據文件的日誌記錄,因爲這樣會清掉已經提交事務的唯一持久化記錄。
InnoDB使用一個後臺線程只能地刷新這些變更到數據文件。這個線程可以批量組合寫入,是的數據寫入更順序,以提高效率。事務日誌把數據文件的隨機IO轉換爲幾乎順序的日誌文件和數據文件IO,把刷新操作轉移到後臺使得查詢可以更快完成,並且緩和查詢高峯時IO的壓力。

InnoDB表空間

InnoDB把數據保存在表空間內,本質上是一個由一或多個磁盤文件組成的虛擬文件系統。InnoDB用表空間實現很多功能,不只是存儲表和索引。它還保存了回滾日誌(舊版本號),插入緩衝(Insert Buffer)、雙寫緩衝(Doublewrite Buffer),以及其他內部數據結構。
InnoDB使用雙寫緩衝來避免頁沒寫完整鎖導致的數據損壞。這是一個特殊的保留區域,再一些連續的塊中足夠保存100個頁。本質上是一個最近寫回的頁面的備份拷貝。當InnoDB從緩衝池刷新頁面到磁盤時,首先把他們寫到雙寫緩衝,然後再把他們寫到其所屬的數據區域中,可以保證每個頁面的寫入都是原子並且持久化的。頁面在末尾都有校驗值(Checksum)來確認是否損壞。

InnoDB的多線程

  • Master Thread
    非常核心的後臺線程,主要負責將緩衝池中的數據異步刷新到磁盤,保證數據的一致性,包括髒頁的刷新、合併插入緩衝(INSERT BUFFER)、UNDO頁的回收等。
  • IO Thread
    InnoDB中大量使用了AIO(Async IO)來處理IO請求,可以極大提高數據庫性能,IO Thread主要是負責這些IO請求的回調(call back)處理。InnoDB1.0之前工有4個IO Thread,分別是write、read、insert buffer、log IO thread。
  • Purge Thread
    事務提交後,其使用的undolog可能不再需要,因此需要PurgeThread來回收已經使用並分配的undo頁。

InnoDB的內存

  • 緩衝池
    InnoDB基於磁盤存儲,記錄按照頁的方式進行管理。在數據庫中進行讀取頁的操作,首先將磁盤讀到的頁存放在緩衝池中,下次讀取先判斷頁是否在緩衝池則直接讀取,否則讀取磁盤上的頁。對頁的修改首先修改緩衝池,然後再以一定的頻率刷新到磁盤(通過checkpoint機制)。緩衝池配置通過innodb_buffer_pool_size來設置。
    緩衝池中緩存的數據頁類型有:索引頁、數據頁、undo頁、插入緩衝(insert buffer)、自適應哈希索引(adaptive hash index)、InnoDB存儲的鎖信息(lock info)、數據字典信息(data dictionary)等


  • LRU List、Free List和Flush List
    InnoDB的LRU添加了midpoint位置,新讀取的頁不是放到首部,而是放到midpoint位置。默認是放在LRU列表長度的5/8處。有些操作可能會全表掃描加載大量的頁,如果直接加載到首部則可能刷出有效頁。數據庫開始時,LRU是空的,頁都在FreeList中,查找時從Free列表中查找是否有可用空閒頁,若有則從Free列表中刪除放入LRU。當頁從LRU的old部分假如到new時,稱之爲page made young,因爲innodb_old_blocks_time設置導致頁沒有從old部分移動到new部分稱爲page not made young。

  • 重做日誌緩衝(redo log buffer)
    三種情況會講redo log buffer中的內容刷新到日誌文件

  1. Master Thread每秒刷新一次
  2. 每個事務提交時會刷新
  3. redo log buffer剩餘空間小於1/2時
  • 額外的內存池
    在對數據庫結構本身的內存進行分配的時候,需要從額外的內存池進行申請。

Checkpoint技術

InnoDB存儲引擎內部有兩種:

  • Sharp Checkpoint
    數據庫關閉時將所有髒頁刷回磁盤,默認工作方式,參數innodb_fast_shuthown=1
  • Fuzzy Checkpoint
    刷新一部分髒頁。(Master Thread Checkpoint,FLUSH_LRU_LIST Checkpoint,Async/Sync Flush Checkpoint,Dirty Page too much Checkpoint)

InnoDB關鍵特性

插入緩衝

  • Insert Buffer
    對於非聚集索引的插入或者更新操作,不是每一次直接插入到索引頁,而是先判斷插入的非聚集索引是否在緩衝池中,若在則插入,若不在則放入到一個Insert Buffer中。以一定的頻率進行Insert Buffer和非聚集索引子節點的合併操作。需要滿足兩個條件:1.索引是輔助索引。2.索引不是唯一的。
  • Change Buffer
    InnoDB 1.0.x開始可以對DML操作進行緩衝 (Insert,Delete,Update)分別是:Insert Buffer,Delete Buffer,Purge Buffer。

Insert Buffer是一顆B+樹,全局唯一,負責對所有表的輔助索引進行Insert Buffer。
Merge Insert Buffer是合併到真正的輔助索引中的操作,在下面幾種情況時發生:

  • 輔助索引頁被讀取到緩衝池中
  • Insert Buffer Bitmap 頁追蹤到該輔助索引頁已經沒有空間可用
  • Master Thread 觸發

自適應Hash索引(Adaptive Hash Index)

InnoDB 會監控各種索引列的查詢,如果判斷建立哈希索引可以提高訪問速度,則會自動建立。AHI是通過緩衝池的B+樹構建而來,不需要對整張表結構建立哈希索引。有如下要求:

  • 以相同模式訪問了100次
  • 頁通過該模式訪問了N次:N=頁中記錄*1/16

異步IO

異步IO(Asychronous IO,AIO)

文件

  • 參數文件:
    初始化參數文件
  • 日誌文件:
    例如錯誤日誌文件(error log),二進制日誌文件(binlog),慢查詢日誌文件(slow query log),查詢日誌文件(log)
  • socket文件:
    UNIX域套接字方式進行連接是需要的文件。
  • pid文件:
    MySQL實例的進程ID文件
  • MySQL表結構文件:
    用來存放MySQL表結構定義的文件
  • 存儲引擎文件:

二進制日誌(binlog)

記錄了對MySQL數據庫執行更改的所有操作,不包括SELECT和SHOW。

mysql> mysqlmaster status;

File Position Binlog_Do_DB Binlog_Ignore_DB Executed_Gtid_Set
binlog.001663 5924141

mysql> show binlog events in 'binlog.001663' limit 5;

binlog文件名(Log_name) 日誌開始位置(Pos) 事件類型(Event_type) 服務器編號(Server_id) 日誌結束位置(End_log_pos) 信息
binlog.001663 5878887 Anonymous_Gtid 1 5878966 SET @@SESSION.GTID_NEXT= 'ANONYMOUS'
binlog.001663 5878966 Query 1 5879057 BEGIN
binlog.001663 5879057 Table_map 1 5879148 table_id: 8291 (vv_oaxxljob.XXL_JOB_QRTZ_SCHEDULER_STATE)
binlog.001663 5879148 Update_rows 1 5879340 table_id: 8291 flags: STMT_END_F
binlog.001663 5879340 Xid 1 5879371 COMMIT /* xid=4800934 */
binlog.001663 5879371 Anonymous_Gtid 1 5879450 SET @@SESSION.GTID_NEXT= 'ANONYMOUS'
binlog.001663 5879450 Query 1 5879541 BEGIN
binlog.001663 5879541 Table_map 1 5879632 table_id: 8291 (vv_oaxxljob.XXL_JOB_QRTZ_SCHEDULER_STATE)
binlog.001663 5879632 Update_rows 1 5879824 table_id: 8291 flags: STMT_END_F
binlog.001663 5879824 Xid 1 5879855 COMMIT /* xid=4800956 */
binlog.001663 5879855 Anonymous_Gtid 1 5879934 SET @@SESSION.GTID_NEXT= 'ANONYMOUS'
binlog.001663 5879934 Query 1 5880025 BEGIN
binlog.001663 5880025 Table_map 1 5880116 table_id: 8291 (vv_oaxxljob.XXL_JOB_QRTZ_SCHEDULER_STATE)
binlog.001663 5880116 Update_rows 1 5880308 table_id: 8291 flags: STMT_END_F
binlog.001663 5880308 Xid 1 5880339 COMMIT /* xid=4800988 */

MySQL5.1引入了binlog_format參數,參數有STATEMENT、ROW、MIXED三種。

  • STATEMENT
    和之前的MySQL版本一樣,二進制日誌文件記錄的是日誌的邏輯SQL語句。
  • ROW
    記錄的是表的行更改情況。如果設置爲ROW,可以將InnoDB事務隔離設置爲READ COMMITTED獲取更好的併發性。
  • MIXED
    默認使用STATEMENT,某些情況下使用MIXED。
  1. 表的存儲引擎爲NDB,對錶的DML操作以ROW格式記錄。
  2. 使用了UUID() USER() CURRENT_USER() FOUND_ROWS() ROW_COUNT()
  3. 使用了INSERT DELAY語句
  4. 使用了用戶自定義函數
  5. 使用了臨時表

要查看binlog日誌文件的內容,必須使用MySQL提供的工具mysqlbinlog。

表結構定義文件

MySQL定義了frm爲後綴名的文件,記錄了表結構(視圖)定義。

InnoDB存儲引擎文件

表空間文件(tablespace file)

默認有一個初始大小爲10MB,名爲ibdata1的文件

重做日誌文件(redo log file)

默認情況下會有 ib_logfile0和ib_logfile1作爲 redo log file 。每個InnoDB至少有一個重做日誌文件組(group),文件組下有兩個重做日誌文件,用戶可以設置多個鏡像日誌組(mirrored log groups)

索引組織表(index organized table)

MySQL默認創建一個6字節大小的指針(_rowid)

InnoDB邏輯存儲結構

所有的數據都被邏輯地存放在一個空間內,稱之爲表空間(tablespace),表空間又由段(segment),區(extent)、頁(page)組成,頁在某些文檔中也成爲塊(block)


表空間

如果啓用了 innodb_file_per_table的參數,每張表的數據可以單獨放到一個表空間內 ,其中存放的是數據、索引、和插入緩衝Bitmap頁,其他類的數據如回滾(undo)信息、插入緩衝索引頁、系統事務信息、二次寫緩衝還是放在原本的共享表空間。

表空間是由各個段組成的,包括數據段、索引段、回滾段等。數據段就是B+樹的葉子節點(Leaf node segment),索引段即B+樹的非索引節點(Non-leaf node segment)。

區是連續頁組成的空間,任何情況下每個區的大小都爲1MB,爲了保證區中頁的連續性,InnoDB一次從磁盤申請4-5個區,默認情況頁大小爲16KB,一個區中一共有64個連續的頁。InnoDB1.0.x引入壓縮頁,每個頁的大小可以通過key_block_size設置爲2k、4k、8k。1.2.x版本新增了參數innodb_page_size,通過該參數可以將默認頁的大小設置爲4k、8k。

InnoDB中常見的頁類型有:

  • 數據頁(B-tree Node)
  • Undo頁(Undo Log Page)
  • 系統頁(System Page)
  • 事務數據頁(Transaction system Page)
  • 插入緩衝頁位圖(Insert Buffer Bitmap)
  • 插入緩衝空閒列表頁(Insert Buffer Free List)
  • 未壓縮的二進制大對象頁(Uncompressed BLOB Page)

MySQL的存儲是面向列的(row-oriented),數據是按行存儲的。頁存放的記錄有硬性定義最多存放16KB/2 - 200行,即7992行。

InnoDB數據頁結構

數據頁由下面7個部分組成:

  • File Header(文件頭)固定
  • Page Header(頁頭)固定
  • Infimun 和 Supremun Record 固定
    頁中兩個虛擬的行記錄,Infimun是指比頁中任何主鍵更小的值,Supremun指比任何值都大的值,這兩個值在頁創建的時候創建,在任何時候情況下都不會刪除。
  • User Record(用戶記錄,即行記錄)
    存儲實際記錄,B+樹索引組織。
  • Free Space(空閒空間)
    空閒空間,鏈表數據結構。一條記錄被刪除後會放到空閒空間。
  • Page Directory(頁目錄)
    存放了記錄的相對位置,這些記錄指針稱之爲槽(slots)或者目錄槽(dictionary slots),稀疏目錄,可能包含多條記錄。
    B+樹索引不能找到實際的記錄,而是找到記錄的頁。
  • File Trailer(文件結尾信息)
    檢測頁是否完整寫入了磁盤,checksum值。


行溢出數據
InnoDB會將一條記錄中的某些列存儲在真正的數據列之外,BLOB,LOB字段可能不一定會將字段放在溢出頁面,VARCHAR也有可能會放進溢出頁面。
Oracle VarCHAR2最多存放4000字節,MSSQL最多8000字節,MySQL最多65535(存在其他開銷,最長65532)。當發生行溢出時,數據存放在頁類型Uncompress BLOB頁面。數據頁只保存數據的前768字節。

lock與latch
latch一般稱爲閂鎖,輕量級,要求鎖定的時間非常短。在InnoDB中,分爲mutex(互斥量)與rwlock(讀寫鎖)。用來保證併發線程操作臨界資源的正確性,並且通常沒有死鎖檢測的機制。
lock的對象是事務,用來鎖定的是數據庫中的對象,如表、頁、行。在commit或者rollback之後釋放,有死鎖檢測機制。

鎖的類型

  • 共享鎖(S Lock):允許事務讀一行數據
  • 排他鎖(X Lock):允許事務更新或刪除一行數據

上述兩種都是悲觀鎖,樂觀鎖就是CAS(Compare and Swap)

一致性非鎖定讀(consistent nonlocking read)
是指InnoDB通過MVCC(Multi Version Concurrency Control)讀取數據庫當前行的方式。如果讀取的行正在進行update或者delete操作,則讀取一個快照。在Read Committed和Repeatedable Read中使用。前者讀取最新的快照,後者使用事務開始時的快照。
一致性鎖定讀(locking read)
也可以顯式的對讀取加鎖,有兩種操作:

  • select ... for update(加一個X鎖)
  • select ... lock in share mode(加一個S鎖)

行鎖的3種算法

  • Record Lock:單個行記錄的鎖
  • Gap Lock:鎖定一個範圍,不包括記錄本身
  • Next-Key Lock:Gap+Record,鎖定範圍以及記錄本身。用來解決幻影相關問題(Phantom)
    針對的是索引的區間,但是當查詢條件指定唯一索引值(只針對主鍵索引/聚集索引)時,會降級爲Record Lock,若是二級索引則不會。而且InnoDB還會對二級索引的下一個鍵值加上Gap Lock。
    例如,二級索引b列有1,3,6,9。當使用X鎖鎖定3時(where b<=3 for update),會NKL鎖定了範圍(1-3),同時會使用GL鎖定下一個鍵值(3-6)。
    利用這個機制可以用一個事務,首先select id from t where col=xxx lock in share mode,接下來insert t (col) values (xxx),能夠保證一定插入不存在的值。

死鎖

兩個事務執行時,因爭奪鎖資源互相等待的場景。
解決死鎖最簡單的就是超時,通過innodb_lock_wait_timeout控制超時時間。
當前普遍使用的是wait-for graph(主動檢測的方式),這要求數據庫保存兩種信息:

  • 鎖的信息鏈表
  • 事務的等待列表

通過上述信息,可以在事務請求鎖併發生等待時都進行判斷,在上述兩個信息構造的圖中是否存在迴路,如果存在就表示存在死鎖。
採用深度優先算法實現,InnoDB1.2之前採用遞歸方式,之後採用非遞歸提高了性能。

事務的實現(ACID)

事務的隔離性由鎖來實現,redo log(重做日誌)保證事務的原子性和持久性,undo log()保證事務的一致性。
redo恢復提交事務修改的頁操作,是物理日誌,記錄的是頁的物理修改操作。
undo回滾某個行記錄到特定版本,是邏輯日誌,記錄的是行的修改記錄。

redo

存在 redo log buffer和redo log file,buffer寫入file時需要調用fsync操作,此操作取決於磁盤性能,決定了事務提交的性能也就是數據庫的性能。

UNIX的寫操作
一般情況下,對硬盤(或者其他持久存儲設備)文件的write操作,更新的只是內存中的頁緩存(page cache),而髒頁面不會立即更新到硬盤中,而是由操作系統統一調度,如由專門的flusher內核線程在滿足一定條件時(如一定時間間隔、內存中的髒頁達到一定比例)內將髒頁面同步到硬盤上(放入設備的IO請求隊列)。
因爲write調用不會等到硬盤IO完成之後才返回,因此如果OS在write調用之後、硬盤同步之前崩潰,則數據可能丟失。雖然這樣的時間窗口很小,但是對於需要保證事務的持久化(durability)和一致性(consistency)的數據庫程序來說,write()所提供的“鬆散的異步語義”是不夠的,通常需要OS提供的同步IO(synchronized-IO)原語來保證
fsync的功能是確保文件fd所有已修改的內容已經正確同步到硬盤上,該調用會阻塞等待直到設備報告IO完成。除了同步文件的修改內容(髒頁),fsync還會同步文件的描述信息(metadata,包括size、訪問時間st_atime & st_mtime等等),因爲文件的數據和metadata通常存在硬盤的不同地方,因此fsync至少需要兩次IO寫操作

undo

delete和update操作產生的刪除語句並不是馬上執行,而是將delete_flag標記爲1,最後有purge操作來統一完成。用undo log來執行,執行之後的空間不會回收,只會用於重用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章