對於 MySQL 數據庫而言,數據是存儲在文件裏的,而爲了可以快速定位到某張表裏的某條記錄進行查詢和修改,咱們須要將這些數據以必定的數據結構進行存儲,這個數據結構就是咱們說的索引。回憶一下咱們大學裏學過的算法與數據結構,可以支持快速查找的數據結構有:順序數組、哈希、搜索樹。算法
數組要求插入的時候保證有序,這樣查找的時候能夠利用二分查找法達到 O(log(N)) 的時間複雜度,對範圍查詢支持也很好,可是插入的時候若是不是在數組尾部,就須要摞動後面全部的數據,時間複雜度爲 O(N) 。因此有序數組只適合存儲靜態數據,例如幾乎不多變更的配置數據,或者是歷史數據。這裏應該會有人有疑問:我用另一種線性數據結構鏈表來替代數組不就能夠解決數組插入由於要移動數據致使太慢的問題了麼,要回答這個問題咱們須要瞭解操做系統讀取文件的流程,磁盤 IO 是一個相對很慢的操做,爲了提升讀取速度,咱們應該儘可能減小磁盤 IO 操做,而操做系統通常以 4kb 爲一個數據頁讀取數據,而 MySQL 通常爲 16kb 做爲一個數據塊,已經讀取的數據塊會在內存進行緩存,若是屢次數據讀取在同一個數據塊,則只須要一次磁盤 IO ,而若是順序一致的記錄在文件中也是順序存儲的,就能夠一次讀取多個數據塊,這樣範圍查詢的速度也能夠大大提高,顯然鏈表沒有這方面的優點。sql
相似於 jdk 中的 hashmap ,哈希表經過一個特定的哈希函數將 key 值轉換爲一個固定的地址,而後將對應的 value 放到這個位置,若是發生哈希碰撞就在這個位置拉出一個鏈表,因爲哈希函數的離散特性,因此通過哈希函數處理後的 key 將失去原有的順序,因此哈希結構的索引沒法知足範圍查詢,只適合等值查詢的狀況例如一些緩存的場景。數據庫
二叉樹在極端狀況下會變成線性結構,也就是每一個節點都只有左子節點或者只有右子節點,這樣就沒法利用二分查找只能從第一個節點開始向後遍歷了,因此爲了維持 O(log(N)) 的時間複雜度,咱們須要在插入節點的時候對節點進行調整以保證樹的平衡,因此平衡二叉樹插入的時間複雜度也是 O(log(N)) ,二叉樹只有兩個子節點,若是數據量很大則樹就很高,樹的每一層通常不在同一個數據塊中存儲,爲了儘可能的減小磁盤讀寫次數,咱們用N叉樹來代替二叉樹,在 MySQL 中這個N通常爲 1200 ,這樣樹高是 4 的話也能夠存儲億級別的數據,並且樹的前面兩層通常都在內存中, MySQL 中用到的 B+ 樹,通常用非葉子節點構建索引,而葉子節點用來存儲具體的值。數組
InnoDB 中,有聚簇索引和普通索引之分,聚簇索引根據主鍵來構建,葉子節點存放的是該主鍵對應的這一行記錄,而普通索引根據申明這個索引時候的列來構建,葉子節點存放的是這一行記錄對應的主鍵的值,而普通索引中還有惟一索引和聯合索引兩個特例,惟一索引在插入和修改的時候會校驗該索引對應的列的值是否已經存在,而聯合索引將兩個列的值按照申明時候的順序進行拼接後在構建索引。緩存
根據以上描述咱們能夠獲得如下信息:數據結構
數據是以行爲單位存儲在聚簇索引裏的,根據主鍵查詢能夠直接利用聚簇索引定位到所在記錄,根據普通索引查詢須要先在普通索引上找到對應的主鍵的值,而後根據主鍵值去聚簇索引上查找記錄,俗稱回表。併發
普通索引上存儲的值是主鍵的值,若是主鍵是一個很長的字符串而且建了不少普通索引,將形成普通索引佔有很大的物理空間,這也是爲何建議使用 自增ID 來替代訂單號做爲主鍵,另外一個緣由是 自增ID 在插入的時候能夠保證相鄰的兩條記錄可能在同一個數據塊,而訂單號的連續性在設計上可能沒有自增ID好,致使連續插入可能在多個數據塊,增長了磁盤讀寫次數。異步
若是咱們查詢一整行記錄的話,必定要去聚簇索引上查找,而若是咱們只須要根據普通索引查詢主鍵的值,因爲這些值在普通索引上已經存在,因此並不須要回表,這個稱爲索引覆蓋,在必定程度上能夠提升查詢效率,因爲聯合索引上經過多個列構建索引,有時候咱們能夠將須要頻繁查詢的字段加到聯合索引裏面,例如若是常常須要根據 name 查找 age 咱們能夠建一個 name 和 age 的聯合索引。函數
查詢的時候若是在索引上用了函數,將致使沒法用到根據以前列上的值構建的索引,索引遵循最左匹配原則,因此若是須要查詢某個列的值中間是否包含某個字符串,將沒法利用索引,若是有這種需求能夠利用全文索引,而若是查詢是否以某個字符串開頭就能夠,聯合索引根據第一個列查詢能夠用到索引,僅僅根據第二個列將沒法用到索引,查詢的時候用 IN 的效率高於 NOT = 。另外建議將索引的列設置爲非空,這個和 NULL 字段的存儲有關,下文在分析。高併發
有了以上的索引知識咱們在來分析數據是怎麼存儲的,InnoDB 存儲引擎的邏輯存儲結構從大到小依次能夠分爲:表空間、段、區、頁、行。
表空間做爲存儲結構的最高層,全部數據都存放在表空間中,默認狀況下用一個共享表空間 ibdata1 ,若是開啓了 innodb_file_per_table 則每張表的數據將存儲在單獨的表空間中,也就是每張表都會有一個文件,
表空間由各個段構成,InnoDB存儲引擎由索引組織的,而索引中的葉子節點用來記錄數據,存儲在數據段,而非葉子節點用來構建索引,存儲在索引段,而回滾段咱們在後面分析鎖的時候在聊。
區是由連續的頁組成,任何狀況下一個區都是 1MB ,
一個區中能夠有多個頁,每一個頁默認爲 16KB ,因此默認狀況下一個區中能夠包含64個連續的頁,頁的大小是能夠經過 innodb_page_size 設置,頁中存儲的是具體的行記錄。一行記錄最終以二進制的方式存儲在文件裏,咱們要可以解析出一行記錄中每一個列的值,存儲的時候就須要有固定的格式,至少須要知道每一個列佔多少空間,而 MySQL 中定義了一些固定長度的數據類型,例如 int、tinyint、bigint、char數組、float、double、date、datetime、timestamp 等,這些字段咱們只須要讀取對應長度的字節,而後根據類型進行解析便可,對於變長字段,例如 varchar、varbinary 等,須要有一個位置來單獨存儲字段實際用到的長度,固然還須要頭信息來存儲元數據,例如記錄類型,下一條記錄的位置等。下面咱們以 Compact 行格式分析一行數據在 InnoDB 中是怎麼存儲的。
變長字段長度列表,該位置用來存儲所申明的變長字段中非空字段實際佔有的長度列表,例若有3個非空字段,其中第一個字段長度爲3,第二個字段爲空,第三個字段長度爲1,則將用 01 03 表示,爲空字段將在下一個位置進行標記。變長字段長度不能超過 2 個字節,因此 varchar 的長度最大爲 65535。
NULL 標誌位,佔 1 個字節,若是對應的列爲空則在對應的位上置爲 1 ,不然爲 0 ,因爲該標誌位佔一個字節,因此列的數量不能超過 255。若是某字段爲空,在後面具體的列數據中將不會在記錄。這種方式也致使了在處理索引字段爲空的時候須要進行額外的操做。
記錄頭信息,固定佔 5 字節,包含下一條記錄的位置,該行記錄總長度,記錄類型,是否被刪除,對應的 slot 信息等
列數據 包含具體的列對應的值,加上兩個隱藏列,事務 ID 列和回滾指針列。若是沒有申明主鍵,還會增長一列記錄內部 ID。
下面咱們以《MySQL 技術內幕》第二版中的例子分析下一行記錄在表空間具體的存儲結構。
CREATE TABLE mytest( t1 varchar(10), t2 varchar(10), t3 char(10), t4 varchar(10) ) engine = innodb; insert into mytest VALUES('a','bb','bb','ccc'); insert into mytest VALUES('d',NULL,NULL,'fff');
該表定義了 3 個變長字段和 1 個定長字段,而後插入兩行記錄,第二行記錄包含空值,咱們打開表空間 mytest.ibd 文件,轉換爲 16 進制,並定位到以下內容:
//第一行記錄 03 02 01 爲變長字段長度列表,這裏是倒序存放的,分別對應 ccc、bb、a 的長度。 00 表示沒有爲空的字段 00 00 10 00 2c 爲記錄頭 00 00 00 2b 68 00 沒有申明主鍵,維護內部 ID 00 00 00 00 06 05 事務ID 80 00 00 00 32 01 10 回滾指針 61 第一列 a 的值 62 62 第二列 bb 的值 62 62 20 20 20 20 20 20 20 20 第三列 bb 的值,固定長度 char(10) 以20進行填充 63 63 63 第四列 ccc 的值 //第二行記錄 03 01 爲變長字段長度列表,這裏是倒序存放的,分別對應 fff、a 的長度,第二列位空。 06 轉換爲二進制爲 00000110 表示第二列和第三列爲空 00 00 20 ff 98 爲記錄頭 00 00 00 2b 68 01 沒有申明主鍵,維護內部 ID 00 00 00 00 06 06 事務ID 80 00 00 00 32 01 10 回滾指針 64 第一列 d 的值 65 65 65 第四列 fff 的值
到此,咱們瞭解了一個數據行是怎麼存儲的,然而數據行並非存儲引擎管理的最小存儲單位,索引只可以幫助咱們定位到某個數據頁,每一次磁盤讀寫的最小單位爲也是數據頁,而一個數據頁內存儲了多個數據行,咱們須要瞭解數據頁的內部結構才能知道存儲引擎怎麼定位到某一個數據行。InnoDB 的數據頁由如下 7 個部分組成:
頁目錄裏維護多個 slot ,一個 slot 包含多個行記錄。每一個 slot 佔 2 個字節,記錄這個 slot 裏的行記錄相對頁初始位置的偏移量。因爲索引只能定位到數據頁,而定位到數據頁內的行記錄還須要在內存中進行二分查找,而這個二分查找就須要藉助 slot 信息,先找到對應的 slot ,而後在 slot 內部經過數據行中記錄頭裏的下一個記錄地址進行遍歷。每個 slot 能夠包含 4 到 8 個數據行。若是沒有 slot 輔助,鏈表自己是沒法進行二分查找的。
排序有好多種算法來實現,在 MySQL 中常常會帶上一個 limit ,表示從排序後的結果集中取前 100 條,或者取第 n 條到第 m 條,要實現排序,咱們須要先根據查詢條件獲取結果集,而後在內存中對這個結果集進行排序,若是結果集數量特別大,還須要將結果集寫入到多個文件裏,而後單獨對每一個文件裏的數據進行排序,而後在文件之間進行歸併,排序完成後在進行 limit 操做。沒錯,這個就是 MySQL 實現排序的方式,前提是排序的字段沒有索引。
CREATE TABLE `person` ( `id` int(11) NOT NULL, `city` varchar(16) NOT NULL, `name` varchar(16) NOT NULL, `age` int(11) NOT NULL, `addr` varchar(128) DEFAULT NULL, PRIMARY KEY (`id`), KEY `city` (`city`) ) ENGINE=InnoDB; select city,name,age from person where city='武漢' order by name limit 100 ;
使用 explain 發現該語句會使用 city 索引,而且會有 filesort . 咱們分析下該語句的執行流程
這裏是查詢 city,name,age 3個字段,比較少,若是查詢的字段較多,則多個列若是都放入 sortbuffer 將佔有大量內存空間,另外一個方案是隻區出待排序的字段和主鍵放入 sortbuffer 這裏是 name 和 id ,排序完成後在根據 id 取出須要查詢的字段返回,其實就是時間換取空間的作法,這裏經過 max_length_for_sort_data 參數控制,是否採用後面的方案進行排序。
另外若是 sortbuffer 裏的條數不少,一樣會佔有大量的內存空間,能夠經過參數 sort_buffer_size 來控制是否須要藉助文件進行排序,這裏會把 sortbuffer 裏的數據放入多個文件裏,用歸併排序的思路最終輸出一個大的文件。
以上方案主要是 name 字段沒有加上索引,若是 name 字段上有索引,因爲索引在構建的時候已是有序的了,因此就不須要進行額外的排序流程只須要在查詢的時候查出指定的條數就能夠了,這將大大提高查詢速度。咱們如今加一個 city 和 name 的聯合索引。
alter table person add index city_user(city, name);
這樣查詢過程以下:
因爲聯合因此在構建索引的時候,在 city 等於武漢的索引節點中的數據已是根據 name 進行排序了的,因此這裏只須要直接查詢就可,另外這裏若是加上 city, name, age 的聯合索引,則能夠用到索引覆蓋,不行到主鍵索引上進行回表。
總結一下,咱們在有排序操做的時候,最好可以讓排序字段上建有索引,另外因爲查詢第一百萬條開始的一百條記錄,須要過濾掉前面一百萬條記錄,即便用到索引也很慢,因此能夠根據 ID 來進行區分,分頁遍歷的時候每次緩存上一次查詢結果最後一條記錄的 id , 下一次查詢加上 id > xxxx limit 0,1000 這樣能夠避免前期掃描到的結果被過濾掉的狀況。
InnoDB 經過一些列後臺線程將相關操做進行異步處理,以下圖所示,同時藉助緩衝池來減少 CPU 和磁盤速度上的差別。當查詢的時候會先經過索引定位到對應的數據頁,而後檢測數據頁是否在緩衝池內,若是在就直接返回,若是不在就去聚簇索引中經過磁盤 IO 讀取對應的數據頁並放入緩衝池。一個數據頁會包含多個數據行。緩存池經過 LRU 算法對數據頁進行管理,也就是最頻繁使用的數據頁排在列表前面,不常用的排在隊尾,當緩衝池滿了的時候會淘汰掉隊尾的數據頁。從磁盤新讀取到的數據頁並不會放在隊列頭部而是放在中間位置,這個中間位置能夠經過參數進行修。緩衝池也能夠設置多個實例,數據頁根據哈希算法決定放在哪一個緩衝池。
InnoDB 在更新數據的時候會採用 WAL 技術,也就是 Write Ahead Logging ,這個日誌就是 redolog 用來保證數據庫宕機後能夠經過該文件進行恢復。這個文件通常只會順序寫,只有在數據庫啓動的時候纔會讀取 redolog 文件看是否須要進行恢復。該文件記錄了對某個數據頁的物理操做,例如某個 sql 把某一行的某個列的值改成 10 ,對應的 redolog 文件格式可能爲:把第5個數據頁中偏移量爲99的位置寫入一個值 10 。redolog 不是無限大的,他的大小是能夠配置的,而且是循環使用的,例如配置大小爲 4G ,一共 4 個文件,每一個文件 1G 。 首先從第一個文件開始順序寫,寫到第四個文件後在從第一個文件開始寫,相似一個環,用一個後臺線程把 redolog 裏的數據同步到聚簇索引上的數據頁上。寫入 redolog 的時候不能將沒有同步到數據頁上的記錄覆蓋,若是碰到這種狀況會停下來先進行數據頁同步而後在繼續寫入 redolog 。另外執行更新操做的時候,會先更新緩衝池裏的數據頁,而後寫入 redolog , 這個時候真正存儲數據的地方尚未更新,也就是說這時候緩衝池中的數據頁和磁盤不一致,這種數據頁稱爲髒頁,當髒頁因爲內存不足或者其餘緣由須要丟棄的時候,必定要先將該髒頁對應的redolog 刷新到磁盤裏的真實數據頁,否則下次查詢的時候因爲 redolog 沒有同步到磁盤,而查詢直接經過索引定位到數據頁就會查詢出髒數據。
更新的時候先從磁盤或者緩衝池中讀取對應的數據頁,而後對數據頁裏的數據進行更改並生成 redolog 到對應的緩衝池(redolog buffer)進行緩存,當事務提交的時候將緩存寫入到 redolog 的物理磁盤文件上。這裏因爲操做系統的文件寫入 InnoDB 並無使用 O_DIRECT 直接寫入到文件,爲了保證性能而是先寫入操做系統的緩存,以後在進行 flush ,因此事務提交的時候 InnoDB 須要在調用一次 fsync 的系統調用來確保數據落盤。爲了提升性能 InnoDB 能夠經過參數 innodb_flush_log_at_trx_commit 來控制事務提交時是否強制刷盤。默認爲 1 ,事務每次提交都須要調用 fsync 進行刷盤,0 表示事務提交的時候不會調用 redolog 的文件寫入,經過後臺線程每秒同步一次,2 表示事務提交的時候會寫入文件可是隻保證寫入操做系統緩存,不進行 fsync 操做。 redolog 文件只會順序寫,因此磁盤操做性能不會太慢,因此建議生產環境都設置爲 1 ,以防止數據庫宕機致使數據丟失。
在執行更新邏輯的時候還會寫入另一個日誌:undolog 。這個文件存儲在共享表空間中,也就是即便打開了 innodb_file_per_table 參數,全部的表的 undolog 都存儲在同一個文件裏。該文件主要用來作事務回滾和 MVCC 。undolog 是邏輯日誌,也就是他不是記錄的將物理的數據頁恢復到以前的狀態,而是記錄的和原 sql 相反的 sql , 例如 insert 對應 delete , delete 對應 insert ,update 對應另一個 update 。事務回滾很好理解,執行相反的操做回滾到以前的狀態,而 MVCC 是指鏡像讀,當一個事務須要查詢某條記錄,而該記錄已經被其餘事務修改,但該事務還沒提交,而當前事務能夠經過 undolog 計算到以前的值。這裏咱們只須要知道和 redolog 同樣, undolog 也是須要在執行 update 語句的時候在事務提交前須要寫入到文件的。另外 undolog 的寫入也會有對應的 redolog ,由於 undolog 也須要持久化,經過 WAL 能夠提升效率。這裏能夠總結下,在事務提交的時候要保證 redolog 寫入到文件裏,而這個 redolog 包含 主鍵索引上的數據頁的修改,以及共享表空間的回滾段中 undolog 的插入。 另外 undolog 的清理經過一個後臺線程定時處理,清理的時候須要判斷該 undolog 是否全部的事務都不會用到。
熟悉 MySQL 的都知道,他經過 binlog 來進行高可用,也就是經過 binlog 來將數據同步到集羣內其餘的 MySQL 實例。binlog 和 redolog 的區別是,他是在存儲引擎上層 Server 層寫入的,他記錄的是邏輯操做,也就是對應的 sql ,而 redolog 記錄的底層某個數據頁的物理操做,redolog 是循環寫的,而binlog 是追加寫的,不會覆蓋之前寫的數據。而binlog 也須要在事務提交前寫入文件。binlog 的寫入頁須要經過 fsync 來保證落盤,爲了提升 tps ,MySQL 能夠經過參數 sync_binlog 來控制是否須要同步刷盤,該策略會影響當主庫宕機後備庫數據可能並無徹底同步到主庫數據。因爲事務的原子性,須要保證事務提交的時候 redolog 和 binlog 都寫入成功,因此 MySQL 執行層採用了兩階段提交來保證 redolog 和 binlog 都寫入成功後才 commit,若是一方失敗則會進行回滾。
下面咱們理一下一條 update 語句的執行過程:
update person set age = 30 where id = 1;
數據庫使用鎖是爲了對共享資源進行併發訪問控制,從而保證數據的完整性和一致性。InnoDB 中鎖的最小粒度爲行,和 jdk 中的 ReadWriteLock 同樣,InnoDB提供了共享鎖和排他鎖,分別用來讀和寫。共享鎖之間能夠兼容,其餘都互斥。根據加鎖的範圍,能夠分爲:全局鎖、表級鎖、行鎖。全局鎖會把整個數據庫實例加鎖,命令爲 flush tables withs read lock ,
將使數據庫處於只讀狀態,其餘數據寫入和修改表結構等語句會阻塞,通常在備庫上作全局備份使用。而表級鎖有兩種,一種是表鎖,命令爲 lock table with read/write ,和讀寫鎖同樣,另一種是元數據鎖,也叫意向鎖,不須要顯示申明,當執行修改表結構,加索引的時候會自動加元數據寫鎖,對錶進行增刪改查的時候會加元數據讀鎖。這樣當兩條修改語句的事務之間元數據鎖都是讀鎖不互斥,可是修改表結構的時候執行更新因爲互斥就須要阻塞。還有一種行級鎖稱爲間隙鎖,他鎖定的是兩條記錄之間的間隙,防止其餘事務往這個間隙插入數據,間隙鎖是隱式鎖,是存儲引擎本身加上的。
非鎖定讀
普通的 select 操做都是非鎖定讀,若是存在事務衝突,會利用 undolog 獲取新事務操做以前的鏡像返回,在讀已提交的隔離級別下,會獲取新事務修改前的最新的一份已經提交的數據,而在可重複讀的隔離級別下,會讀取該事務開始時的數據版本。當有多個事務併發操做同一行記錄時,該記錄會同時存在多個 undolog ,每一個 undolog 就是一個版本,這種模式稱爲多版本併發控制(MVCC) ,該模式可以極大的提升數據庫的性能,想想,若是基於鎖來控制的話,當對某個記錄進行修改的時候,另外一個事務將須要等待,無論他是要讀取仍是寫入,MVCC 容許寫入的時候還可以進行讀操做,這對大部分都是查詢操做的應用來講極大的提升了 tps 。
鎖定讀
有時候咱們在查詢的時候須要顯示的給記錄加鎖來保證一致性,select for update 將對掃描到的記錄加上排他鎖,而 select in share lock 將對掃描的記錄加上共享鎖。這兩個語句必須在一個事物內,也就是須要顯示開啓事物,begin transaction; 當事物提交的時候會釋放鎖。具體加鎖的邏輯咱們後面在分析。另外全部的鎖定讀都是當前讀,也就是讀取當前記錄的最新版本,不會利用 undolog 讀取鏡像。另外全部的 insert、update、delete 操做也是當前讀,update、delete 會在更新以前進行一次當前讀,而後加鎖,而 insert 由於會觸發惟一索引檢測,也會包含一個當前讀。
自增加鎖:
在主鍵設置爲自增加的狀況下,該表會維護一個計數器,每一個插入操做都會先獲取這個計數器的當前值,而後加 1 做爲新的主鍵,顯然這個計數器是一個共享變量須要加排他鎖,而這個鎖不須要等到事物提交後才釋放,他在 sql 語句插入完成後就會釋放,新版本的 innoDB 採用互斥量來實現提升了插入速度。
鎖的問題
髒讀是指事務A對某個數據頁進行了更改,可是並無提交,這個數據就成爲髒數據,這裏稍微和上面提到的髒頁作下區分,髒頁是指內存中已經更改可是尚未刷新到磁盤的數據,髒頁是正常的,而髒讀是指一個事物讀取了另一個事物沒有提交的數據,若是另一個數據對這個數據又進行了更改,則出現數據一致性,髒讀違背了數據庫的隔離性。髒讀目前只能出如今讀未提交這個隔離級別下,目前 MySQL 默認的隔離級別爲可重複讀。
不可重複讀是指一個事務前後兩次讀取同一條記錄的結果不同,由於第二次讀取的時候可能其餘事務已經進行更改並提交,不可重複讀只發生在隔離級別爲讀未提交和讀已提交裏。
丟失更新是指兩個事務同時更新某一條記錄,致使其中一個事務更新失效,理論上任何一個隔離級別都不會發生丟失更新,由於更新的時候會加上排他鎖,可是應用中卻常常發生,例如一個計數器應用,事務A查詢計數器的值 v=5,在內存中加 1 寫入到數據庫,在寫入以前另一個事務讀取到計數器的值 v=5 ,而後加 1 寫入數據庫,這樣原本應該爲 7 , 如今倒是 6 ,這是由於
咱們是先讀取在寫入,而讀取和寫入對數據庫而言是兩個操做,並非一個原子操做,這裏能夠經過把查詢的記錄加上排他鎖 select for update 來防止丟失更新現象。固然這裏直接將 sql 改成 v = v + 1 也能夠。
死鎖是指兩個或兩個以上事務因爭奪資源而互相等待的狀況,InnoDB 提供了死鎖檢測和超時機制來防止死鎖的影響,死鎖檢測是很是耗 CPU 的,當不少個事務同時競爭同一個資源的時候,例如搶購的時候扣商品份額,或者支付的時候全部的訂單都會用到一個公共帳戶,同一個資源競爭的事務越多,死鎖檢測越耗 CPU 。爲了減小這種狀況的影響,建議儘可能在業務層減小熱點的產生,例如將熱點帳戶拆分紅若個個一樣功能的帳戶,萬一發生高併發,建議在應用層作限流或者排隊,固然也能夠在數據庫層作排隊,這個須要修改數據庫源碼。
InnoDB的加鎖過程比較複雜,大體能夠記住一個原則是:將全部掃描到的記錄都加鎖,範圍查詢會加間隙鎖,而後加鎖過程按照兩階段鎖 2PL 來實現,也就是先加鎖,而後全部的鎖在事物提交的時候釋放。怎麼加鎖和數據庫的隔離級別有關,然而咱們通常不多更改 MySQL 的隔離級別,因此下面咱們均按照可重複讀的隔離級別進行分析,另一個因素是查詢條件中是否包含索引,是主鍵索引仍是普通索引,是不是惟一索引等。咱們如下面這條 sql 語句來分析加鎖過程。
select * from trade_order where order_no = '201912102322' for update;
order_no = '201912102322' 這條記錄不存在的狀況下,若是order_no 是主鍵索引,則會加一個間隙鎖,而這個間隙是主鍵索引中 order_no 小於 201912102322 的第一條記錄到大於 201912102322 的第一條記錄。試想一下若是不加間隙鎖,若是其餘事物插入了一條 order_no = '201912102322' 的記錄,因爲 select for update 是當前讀,即便上面那個事物沒有提交,若是在該事物中從新查詢一次就會發生幻讀。
若是沒有索引,則對掃描到的全部記錄和間隙都加鎖,若是不匹配行鎖將會釋放只剩下間隙鎖。回憶一下上面講的數據頁的結果中又一個最大記錄和最小記錄,Infimum 和 Supremum Record,這兩個記錄在加間隙鎖的時候就會用到。
InnoDB 存儲引擎的事務需徹底符合 ACID 特性。下面咱們一塊兒看下 InnoDB 作了哪些事情。
原子性 : 是指一個事務內的全部操做要麼所有成功要麼所有失敗,數據庫中將 redolog 和 binlog 的寫入採用兩階段提交就是爲了保證事務的原子性。另外因爲 InnodDB 是按頁進行存儲的,每一個頁大小爲 16kb 而操做系統的通常以 4KB 爲一頁進行讀取,因此可能出現一個 InnoDB 的數據頁只寫了一部分的狀況。而 InnoDB 爲了防止這種狀況的發生採用雙寫機制,除了寫入磁盤上的數據頁還會在共享空間中寫入。而 redolog 按塊存儲,每一個塊 512 字節,正好和扇區大小同樣因此,能夠保證原子性,不須要進行雙寫。
一致性 :保證磁盤和緩存的數據一致,binlog 數據和 主庫中的數據一致。
隔離性 : 默認爲可重複讀,採用 undolog 來實現。
持久性 : 事務一旦提交,其結果就是永久的,redolog 須要在事務提交前進行刷盤,磁盤採用 RAID 等。