MySQL面试准备——64页pdf
本筆記為以前整理的零碎的關于Mysql的知識點,有深入源碼的也有淺層的八股。已經被我整理成了一個pdf。
實習崗位正好也是和數據庫內核有關的,之后應該還會更新。做個整理,方便秋招的時候快速回顧吧。
鏈接:鏈接
提取碼:1234
目錄
- 《DBNotes:Buffer Pool刷臟頁細節以及改進》
- 獲取一個空閑頁的源碼邏輯
- Page_Cleaner_Thread
- LRU_Manager_Thread
- Hazard Pointer作為驅逐算法改進
- 參考
- 《DBNotes:Join算法的前世今生》
- NestLoopJoin算法
- Simple Nested-Loop Join
- Index Nested-Loop Join
- Block Nested-Loop Join
- Batched Key Access
- Hash Join算法
- In-Memory Join(CHJ)
- On-Disk Hash Join
- 參考鏈接
- 《DBNotes_ Buffer Pool對于緩沖頁的鏈表式管理》
- Buffer Pool回顧
- Buffer Pool內部組成
- freelist
- flushlist
- LRU鏈表管理以及改進
- 《DBNotes_single_table訪問方法、MRR多范圍讀取優化、索引合并》
- single_table訪問方法
- const
- ref
- ref_or_null
- range
- index
- all
- MRR多范圍讀取優化
- 索引合并
- intersection
- union
- sort-union
- 《MySQL8.0.22:Lock(鎖)知識總結以及源碼分析》
- 1、關于鎖的一些零碎知識,需要熟知
- 事務加鎖方式:
- Innodb事務隔離
- MVCC多版本并發控制
- 常用語句 與 鎖的關系
- 意向鎖
- 行級鎖
- 2、鎖的內存結構以及一些解釋
- 3、InnoDB的鎖代碼實現
- 鎖系統結構lock_sys_t
- lock_t 、lock_rec_t 、lock_table_t
- bitmap
- 鎖的基本模式的兼容關系和強弱關系
- 行鎖類別代碼
- 記錄鎖的alloc函數
- 記錄鎖的add函數
- 記錄鎖的create函數
- 4、鎖的流程
- 表鎖加鎖流程
- 行鎖加鎖流程
- 插入加鎖流程
- 刪除加鎖流程帶來的死鎖
- 釋放鎖流程
- 死鎖流程
- 5、參考
- count()用法
- 《MySQL——join語句優化tips》
- 要不要用join
- Join驅動表選擇
- Multi-Range Read優化
- Batched Key Access (BKA)對NLJ進行優化
- BNL算法性能問題
- BNL轉BKA
- 《MySQL——redo log 與 binlog 寫入機制》
- binlog寫入機制
- redo log寫入機制
- 組提交機制實現大量的TPS
- 理解WAL機制
- 如何提升IO性能瓶頸
- 《MySQL——備庫多線程復制策略》
- 備庫并行復制能力
- MySQL5.6版本 并行復制策略
- MariaDB 并行復制策略
- MySQL5.7版本 并行復制策略
- MySQL5.7.22版本 并行復制策略
- 總結
- 《MySQL——查詢長時間不返回的三種原因與查詢慢的原因》
- 查詢長時間不返回
- 等MDL鎖
- 等flush
- 等行鎖
- 查詢慢
- 幻讀現象
- 幻讀帶來的問題
- 如何解決幻讀
- next-key lock
- 臨時表的應用
- 臨時表可以重名的原因
- 臨時表的主備同步
- 覆蓋索引優化查詢
- 思考
- 事務
- 事務的必要性
- MySQL中如何控制事務
- 手動開啟事務
- 事務的四大特征
- 事務的四大特征
- 事務開啟方式
- 事務手動提交與手動回滾
- 事務的隔離性
- 臟讀現象
- 不可重復讀現象
- 幻讀現象
- 串行化
- 一些補充
- 使用長事務的弊病
- `commit work and chain`的語法是做什么用的?
- 怎么查詢各個表中的長事務?
- 如何避免長事務的出現?
- 事務隔離是怎么通過read-view(讀視圖)實現的?
- 參考
- 索引
- 回表
- 覆蓋索引
- 最左前綴原則
- 聯合索引的時候,如何安排索引內的字段順序?
- 索引下推
- 重建索引問題
- 聯合主鍵索引和 InnoDB 索引組織表問題
- in與between的區別
- 表鎖是什么?表鎖有什么用?表鎖怎么用?
- 行鎖是什么?行鎖有什么用?行鎖怎么用?
- 死鎖與死鎖檢測
- 何時會死鎖檢測
- 如何避免高量級的死鎖檢測
- 練習
- 主備一致性
- 備庫為什么要設置為只讀模式?
- 備庫設置為只讀,如何與主庫保持同步更新?
- A到B的內部流程如何?
- binlog內容是什么?
- `row`格式對于恢復數據有何好處
- M-M結構的循環復制問題以及解決方案
- 關于查詢能力
- 關于change buffer
- 關于寫能力(基于change buffer)
- MySQL索引底層原理理解以及常見問題總結
- 二叉查找樹為索引
- 紅黑樹為索引
- B樹作為索引
- B+樹作為索引
- MyISAM存儲引擎索引實現
- InnoDB存儲引擎索引實現
- 常見問題
- 聚集索引與非聚集索引
- InnoDB基于主鍵索引和普通索引的查詢有什么區別?
- InnoDB主鍵索引為何是整型的自增主鍵
- 何時使用業務字段作為主鍵呢?
- 哈希與B樹
- “N叉樹”的N值在MySQL中是可以被人工調整的么?
《DBNotes:Buffer Pool刷臟頁細節以及改進》
本筆記知識沿用之前DBNotes: Buffer Pool對于緩沖頁的鏈表式管理的部分知識
獲取一個空閑頁的源碼邏輯
任何一個讀寫請求都需要從Buffer pool來獲取所需頁面。如果需要的頁面已經存在于Buffer pool,那么直接利用當前頁面進行操作就行。但是如果所需頁面不在Buffer pool,比如UPDATE操作,那么就需要從Buffer pool中新申請空閑頁面,將需要讀取的數據放到Buffer pool中進行操作。
如何從buffer pool中獲取一個頁面呢?這依賴于buf_LRU_get_free_block函數,該函數會循環嘗試去淘汰LRU list上的頁面。每次循環都會訪問freelist,查看是否有足夠的空閑頁面,如果沒有,就繼續從LRUlist去淘汰。這樣的循環在負載較高的時候會加劇對freelist以及LRUlist的mutex的競爭。可以設置buf_pool->try_LRU_scan是做了一個優化,如果當前用戶線程掃描的時候 發現沒有空閑頁面,那么其他用戶線程就不需要進行同樣的掃描。
MySQL的free頁面的獲取依賴于Page_Cleaner_Thread的刷新能力,如果刷新不及時,那么系統就會使用上面所說的循環邏輯來為用戶線程申請空閑頁面,可以看出是十分耗時間的。而如果刷新過快,也會導致性能問題,因為刷新是需要io操作的。
所以引入獨立的線程負責LRU list的刷臟。目的是為了讓獨立線程根據系統負載動態調整LRU的刷臟能力。由于LRU list的刷臟從page cleaner線程中脫離出來,調整LRU list的刷臟能力不再會影響到page cleaner。
同時由于單線程LRUlist刷臟存在問題,設計者進行了改進。繼續將LRU list獨立于page cleaner threads并將LRU list單線程刷臟增加為多線程刷臟。page cleaner只負責flush list的刷臟,lru_manager_thread只負責LRU List刷臟。這樣的分離,可以使得LRU list刷臟和Flush List刷臟并行執行。
Page_Cleaner_Thread
主要負責flushlist的刷臟,避免用戶線程同步刷臟頁。
也是每隔一定時間刷一次臟頁,sleep time是自適應的,依賴于當前的lsn,flushlist中的oldest_modification以及當前的同步刷臟點。
與LRU_Manager_Thread不同,該線程每次執行刷的臟頁數量也是自適應的,依賴于當前系統中臟頁的比率,日志產生的速度以及幾個參數。
LRU_Manager_Thread
一個系統線程,隨著InnoDB啟動而work,作用是定期清理出空閑的數據頁(數量為innodb_LRU_scan_depth)并加入到Freelist中,防止用戶線程去做同步刷臟影響效率。
該線程每隔一段時間就去FLUSH。先嘗試從LRU中驅逐部分數據頁,如果數量不夠就從Flushlist中驅逐。
線程執行頻率是自適應的:
設定max_free_len = innodb_LRU_scan_depth * innodb_buf_pool_instances。
如果Freelist中的數量小于max_free_len 的1%,則sleep time = 0,表示這時候空閑頁太少了,需要一直執行buf_flush_LRU_tail操作,從而騰出空閑的數據頁。
如果Free List中的數量介于max_free_len的1%-5%,則sleep time減少50ms(默認為1000ms),如果Free List中的數量介于max_free_len的5%-20%,則sleep time不變,如果Free List中的數量大于max_free_len的20%,則sleep time增加50ms,但是最大值不超過rds_cleaner_max_lru_time。
Hazard Pointer作為驅逐算法改進
在學術上,Hazard Pointer是一個指針,如果這個指針被一個線程所占有,在它釋放之前,其他線程不能對他進行修改,但是在InnoDB里面,概念剛好相反,一個線程可以隨時訪問Hazard Pointer,但是在訪問后,他需要調整指針到一個有效的值,便于其他線程使用。我們用Hazard Pointer來加速逆向的邏輯鏈表遍歷。 先來說一下這個問題的背景,我們知道InnoDB中可能有多個線程同時作用在Flush List上進行刷臟,例如LRU_Manager_Thread和Page_Cleaner_Thread。同時,為了減少鎖占用的時間,InnoDB在進行寫盤的時候都會把之前占用的鎖給釋放掉。這兩個因素疊加在一起導致同一個刷臟線程刷完一個數據頁A,就需要回到Flush List末尾(因為A之前的臟頁可能被其他線程給刷走了,之前的臟頁可能已經不在Flush list中了),重新掃描新的可刷盤的臟頁。另一方面,數據頁刷盤是異步操作,在刷盤的過程中,我們會把對應的數據頁IO_FIX住,防止其他線程對這個數據頁進行操作。我們假設某臺機器使用了非常緩慢的機械硬盤,當前Flush List中所有頁面都可以被刷盤(buf_flush_ready_for_replace返回true)。我們的某一個刷臟線程拿到隊尾最后一個數據頁,IO fixed,發送給IO線程,最后再從隊尾掃描尋找可刷盤的臟頁。在這次掃描中,它發現最后一個數據頁(也就是剛剛發送到IO線程中的數據頁)狀態為IO fixed(磁盤很慢,還沒處理完)所以不能刷,跳過,開始刷倒數第二個數據頁,同樣IO fixed,發送給IO線程,然后再次重新掃描Flush List。它又發現尾部的兩個數據頁都不能刷新(因為磁盤很慢,可能還沒刷完),直到掃描到倒數第三個數據頁。所以,存在一種極端的情況,如果磁盤比較緩慢,刷臟算法性能會從O(N)退化成O(N*N)。 要解決這個問題,最本質的方法就是當刷完一個臟頁的時候不要每次都從隊尾重新掃描。我們可以使用Hazard Pointer來解決,方法如下:遍歷找到一個可刷盤的數據頁,在鎖釋放之前,調整Hazard Pointer使之指向Flush List中下一個節點,注意一定要在持有鎖的情況下修改。然后釋放鎖,進行刷盤,刷完盤后,重新獲取鎖,讀取Hazard Pointer并設置下一個節點,然后釋放鎖,進行刷盤,如此重復。當這個線程在刷盤的時候,另外一個線程需要刷盤,也是通過Hazard Pointer來獲取可靠的節點,并重置下一個有效的節點。通過這種機制,保證每次讀到的Hazard Pointer是一個有效的Flush List節點,即使磁盤再慢,刷臟算法效率依然是O(N)。 這個解法同樣可以用到LRU List驅逐算法上,提高驅逐的效率。
參考
MySQL · 源碼分析 · Innodb緩沖池刷臟的多線程實現
MySQL · 源碼分析 · InnoDB LRU List刷臟改進之路
MySQL · 引擎特性 · InnoDB Buffer Pool
《DBNotes:Join算法的前世今生》
在8.0.18之前,MySQL只支持NestLoopJoin算法,最簡單的就是Simple NestLoop Join,MySQL針對這個算法做了若干優化,實現了Block NestLoop Join,Index NestLoop Join和Batched Key Access等,有了這些優化,在一定程度上能緩解對HashJoin的迫切程度。但是HashJoin的支持使得MySQL優化器有更多選擇,SQL的執行路徑也能做到更優,尤其是對于等值join的場景。
NestLoopJoin算法
長期以來,在MySQL中執行聯接的唯一算法是嵌套循環算法的變體。
Simple Nested-Loop Join
如果我們執行這樣一條等值查詢語句:
select * from t1 straight_join t2 on (t1.a=t2.b);由于表 t2 的字段 b 上沒有索引,每次到 t2 去匹配的時候,就要做一次全表掃描。就相當于是雙for循環。如果 t1 和 t2 都是 10 萬行的表(當然了,這也還是屬于小表的范圍),就要掃描 100 億行。
SimpleNestLoopJoin顯然是很低效的,對內表需要進行N次全表掃描,實際復雜度是N*M,N是外表的記錄數目,M是記錄數,代表一次掃描內表的代價。為此,MySQL針對SimpleNestLoopJoin做了若干優化。
Index Nested-Loop Join
如果我們能對內表的join條件建立索引,那么對于外表的每條記錄,無需再進行全表掃描內表,只需要一次Btree-Lookup即可,整體時間復雜度降低為N*O(logM)。
再來看看這一句
在這條語句里,被驅動表 t2 的字段 a 上有索引,join 過程用上了這個索引,因此這個語句的執行流程是這樣的:
執行流程示意圖如下:
對比HashJoin,對于外表每條記錄,HashJoin是一次HashTable的search,當然HashTable也有build時間,還需要處理內存不足的情況,不一定比INLJ好。
Block Nested-Loop Join
MySQL采用了批量技術,即一次利用join_buffer_size緩存足夠多的記錄,每次遍歷內表時,每條內表記錄與這一批數據進行條件判斷,這樣就減少了掃描內表的次數,如果內表比較大,間接就緩解了IO的讀壓力。
Simple Nested-Loop Join 與 Block Nested-Loop Join從時間復雜度上來說,這兩個算法是一樣的。但是,Block Nested-Loop Join是內存操作,速度上會快很多,性能也更好。
示意圖如下:
Batched Key Access
IndexNestLoopJoin利用join條件的索引,通過Btree-Lookup去匹配減少了遍歷內表的代價。如果join條件是非主鍵列,那么意味著大量的回表和隨機IO。BKA優化的做法是,將滿足條件的一批數據按主鍵排序,這樣回表時,從主鍵的角度來說就相對有序,緩解隨機IO的代價。BKA實際上是利用了MRR特性(MultiRangeRead),訪問數據之前,先將主鍵排序,然后再訪問。主鍵排序的緩存大小通過參數read_rnd_buffer_size控制。
Hash Join算法
NestLoopJoin算法簡單來說,就是雙重循環,遍歷外表(驅動表),對于外表的每一行記錄,然后遍歷內表,然后判斷join條件是否符合,進而確定是否將記錄吐出給上一個執行節點。從算法角度來說,這是一個M*N的復雜度。HashJoin是針對equal-join場景的優化,基本思想是,將外表數據load到內存,并建立hash表,這樣只需要遍歷一遍內表,就可以完成join操作,輸出匹配的記錄。如果數據能全部load到內存當然好,邏輯也簡單,一般稱這種join為CHJ(Classic Hash Join),之前MariaDB就已經實現了這種HashJoin算法。如果數據不能全部load到內存,就需要分批load進內存,然后分批join,下面具體介紹這幾種join算法的實現。
In-Memory Join(CHJ)
HashJoin一般包括兩個過程,創建hash表的build過程和探測hash表的probe過程。
1).build phase
遍歷外表,以join條件為key,查詢需要的列作為value創建hash表。這里涉及到一個選擇外表的依據,主要是評估參與join的兩個表(結果集)的大小來判斷,誰小就選擇誰,這樣有限的內存更容易放下hash表。
2).probe phase
hash表build完成后,然后逐行遍歷內表,對于內表的每個記錄,對join條件計算hash值,并在hash表中查找,如果匹配,則輸出,否則跳過。所有內表記錄遍歷完,則整個過程就結束了
On-Disk Hash Join
CHJ的限制條件在于,要求內存能裝下整個外表。在MySQL中,Join可以使用的內存通過參數join_buffer_size控制。如果join需要的內存超出了join_buffer_size,那么CHJ將無能為力,只能對外表分成若干段,每個分段逐一進行build過程,然后遍歷內表對每個分段再進行一次probe過程。假設外表分成了N片,那么將掃描內表N次。這種方式當然是比較弱的。
在MySQL8.0中,如果join需要內存超過了join_buffer_size,build階段會首先利用hash算將外表進行分區,并產生臨時分片寫到磁盤上;然后在probe階段,對于內表使用同樣的hash算法進行分區。由于使用分片hash函數相同,那么key相同(join條件相同)必然在同一個分片編號中。接下來,再對外表和內表中相同分片編號的數據進行CHJ的過程,所有分片的CHJ做完,整個join過程就結束了。這種算法的代價是,對外表和內表分別進行了兩次讀IO,一次寫IO。相對于之之前需要N次掃描內表IO,現在的處理方式更好。
順序為:外表的分片、內表分片、哈希連接
參考鏈接
join語句怎么優化?
MySQL8.0 新特性 Hash Join
哈希加入MySQL 8
MySQL · 新特征 · MySQL 哈希連接實現介紹
《DBNotes_ Buffer Pool對于緩沖頁的鏈表式管理》
Buffer Pool回顧
我們知道針對數據庫的增刪改刪操作都是在Buffer Pool中完成的,一條sql的執行步驟可以認為是這樣的:
1、innodb存儲引擎首先在緩沖池中查詢有沒有對應的數據,有就直接返回
2、如果不存在,則去磁盤進行加載,并加入緩沖池
3、同時該記錄會被加上獨占鎖,防止多人修改,出現數據不一致
而且我們知道,可以通過設置my.cnf配置中的innodb_buffer_pool_size來修改緩沖池大小,加快sql查詢速度,當然也需要注意設置過大會造成系統swap空間被占用,導致系統變慢降低查詢性能。
Buffer Pool內部組成
緩沖池對應一片連續內存,我們將其劃分為大小為16kb的頁(與innodb對應),這些頁稱為緩沖頁。
為了很好的管理這些頁,設計者為每個緩沖頁都創建了一些控制信息:表空間編號、頁號、緩沖頁在緩沖池中的地址、鏈表節點信息等。將每個頁對應的控制信息占用的一塊內存稱為一個控制塊。控制塊與緩沖頁一一對應,都存放在緩沖池中。
在Mysql啟動時,會自己完成對緩沖池的初始化:向操作系統申請內存,自己劃分成若干對控制塊和緩沖頁。
freelist
當我們從磁盤中load一個數據頁到緩沖池中,我們應該放到哪個緩沖頁中呢?
很顯然我們應該把數據頁放到“空閑”的緩沖頁中。
設計者將所有空閑的緩沖頁對應的控制塊作為一個節點放到一個鏈表中,稱為freelist。每次從freelist中取出一個空閑的緩沖頁中,并且將該緩沖頁對應的控制塊信息填上,然后將該節點移除,表示緩沖頁已經被使用了
flushlist
當一個控制塊節點被從freelist中移除,說明該頁已經被使用了。如果這種“使用操作”是對數據進行修改的話,那么必定需要將該頁數據flush到磁盤上。但是每次修改一頁就將那一頁flush的話,磁盤IO占用率高。所以每次修改緩沖頁后,將這些臟頁控制塊放入一個fulshlist上。當flush時機到了,就把flushlist節點對應的緩沖頁刷新搭配磁盤上。
LRU鏈表管理以及改進
緩沖池內存有限,當freelist中沒有多余的空閑緩沖頁,就需要把某些舊的緩沖頁從緩沖池中移除,然后把新的數據頁放進來。為了提高內存命中率,使用LRU。
但是普通的LRU不能解決下面的問題;
1、加載到緩沖池的頁不一定被用到(針對于預讀)
2、如果有非常多的使用頻率低的頁被同時加載到緩沖池中,則可能會把那些使用頻率非常高的頁從緩沖池中淘汰。(針對全表掃描)
關于innodb對于LRU的改進見如鏈接:
MySQL——Innodb改進LRU算法
當然還有進一步的優化:
對于young區域的緩沖頁,每次訪問一個緩沖頁就要把它移動到LRU鏈表的頭部,開銷比較大。畢竟,young區域的緩沖頁都是熱點數據。所以我們可以這樣優化:只有被訪問的緩沖頁位于young區域1/4的后面時,才會被移動到LRU鏈表頭部。也就是說我們將young的前0.25部分稱為very young,very young里面的數據訪問不會移動到頭部,因為大家訪問頻率都是非常高的。
提醒一下,在LRUlist的節點不是freelist節點,可能是flushlist節點。不理解的話,再去上面看看兩個list定義。
然而這一切的目的只有一個:盡量高效地提高緩沖池命中率。
《DBNotes_single_table訪問方法、MRR多范圍讀取優化、索引合并》
single_table訪問方法
const
在主鍵列或者unique二級索引與一個常數進行等值比較時才有效。
如果主鍵或者unique二級索引的索引列由多個列構成,則只有在索引列中的每個列都與常數進行等值比較時,才是const訪問
ref
搜索條件為二級索引(非unique)與常數進行等值比較,形成的掃描區間為單點掃描區間(即【‘abc’,‘abc’】),采用二級索引來執行查詢的訪問方法為ref。注意采用二級索引執行查詢時,每獲取到一條二級索引記錄就會進行一次回表操作。
TIPS:
- 二級索引列允許存儲NULL值時不限制NULL值的數量,所以執行key is NULL查詢時最優只能執行ref操作
- 索引列中包含多個列的二級索引時,只要最左邊連續的列是與常數進行等值比較,就可以使用ref訪問。
ref_or_null
當想找出某個二級索引列的值等于某個常數的記錄,并且將該列中值為NULL的記錄也找出來:
select * from single_table where key1 = 'abc' or key1 is null;若使用二級索引,此時的掃描區間為:[‘abc’,‘abc’] 以及[NULL,NULL]。
這種訪問方法即為ref_or_null。
range
select * from single_table where key2 IN (1438,6328) OR (key2 >= 38 AND key2 <= 79);使用二級索引,掃描區間為[1438,1438] 、[6328,6328]、[38,79],改掃描區間為若干個單點掃描區間或者范圍掃描區間。訪問方法為range。當然(-無窮,+無窮)不為range訪問方法。
index
key_part1,key_part2,key_part3 為二級索引,它們三個構成了一個聯合索引,并且key_table2并不是聯合索引的最左列。
select key_part1,key_part2,key_part3 from single_table where key_table2 = 'abc';此時無法形成合適的范圍區間來減少掃描的記錄數量。
需要注意此時的查詢符合兩個條件:
- 查詢列表中key_part1,key_part2,key_part3,都包含在聯合索引中
- 搜索條件只有key_part2,這個列也包含在聯合索引中
很顯然,需要掃描全部的聯合索引,掃描區間為[-無窮,+無窮]。由于二級索引記錄只有存放索引列和主鍵,也不需要回表,所以此時掃描去不的二級索引記錄比直接掃描全部的聚集索引記錄成本要小。這種方法稱為index訪問。
又如:
select * from single_table order by id;通過全表掃描對表進行查詢時有order by。此時也是使用index方法。
all
全表掃描,直接掃描全部的聚集索引記錄。
MRR多范圍讀取優化
select * from single_table where key1 = 'abc' and key2 > 1000;該語句的執行步驟:
1、通過key1的索引定位掃描區間[‘abc’,‘abc’];
2、根據上面得到的主鍵值回表,得到完整用戶記錄,然后檢測記錄是否滿足key2 > 1000的條件,滿足則返回
3、重復2步驟,直到不滿足key1 = ‘abc’
每次從二級索引中讀取到一條記錄后,就會根據該記錄的主鍵值執行回表操作。
而某個掃描區間中的二級索引記錄的主鍵值是無序的,每次回表都會隨機讀取一個聚集索引頁面,帶來的IO開銷較大。
MRR會先讀取一部分二級索引記錄,將它們的主鍵值排序后再同意執行回表操作,節省IO開銷。
索引合并
intersection
使用多個索引完成一次查詢的執行方法稱為索引合并
select * from single_table where key1 = 'a' and key3 = 'b';可以先搜key1的索引,然后回表,根據key3條件篩選。
也可以先搜key1的索引,然后回表,根據key1條件篩選。
當然可以同時使用key1和key2的索引。在key1索引中掃描key1值得到區間[‘a’,‘a’],在key3索引中掃描key3值得到區間[‘b’,‘b’];
然后從兩者操作結果中找到id列值相同的記錄。然后根據共有的id值執行回表,這樣可能會省下回表操作帶來的開銷。
當然需要注意的是要求從不同二級索引中獲取到的二級索引記錄都按照主鍵值排好序:
- 從兩個有序集合中取交集比兩個從無序集合中取交集要容易
- 如果獲取到的id值有序排列,則在根據這些id值執行回表操作時不再是進行單純的隨機IO,就會提高效率。
如果從掃描區間中獲得的記錄并不是按照主鍵值排序的,那么就不能使用intersection索引合并。
union
select * from single_table where key1 = 'a' or key3 = 'b';同時使用key1和key2的索引。在key1索引中掃描key1值得到區間[‘a’,‘a’],在key3索引中掃描key3值得到區間[‘b’,‘b’];
然后對兩個結果進行去重,對去重后的id值進行回表操作。
同樣二級索引記錄都是要按照主鍵值排序,如果從掃描區間中獲得的記錄并不是按照主鍵值排序的,那么就不能使用union索引合并。
sort-union
union索引合并條件苛刻,下面的查詢就不能使用union索引合并
select * from single_table where key1 < 'a' or key3 > 'z';我們可以這樣操作;
1、根據key1<'a’條件從key1的二級索引中獲取記錄,并將獲取到的記錄的主鍵值排序
2、根據key3<'z’條件從key3的二級索引中獲取記錄,并將獲取到的記錄的主鍵值排序
3、按照union操作兩個記錄合并
sort-union 索引合并比union索引合并多了一步對二級索引記錄的主鍵值進行排序。
《MySQL8.0.22:Lock(鎖)知識總結以及源碼分析》
1、關于鎖的一些零碎知識,需要熟知
事務加鎖方式:
兩階段鎖:
整個事務分為兩個階段,前一個階段加鎖,后一個階段為解鎖。在加鎖階段,事務只能加鎖,也可以操作數據,但是不能解鎖,直到事務釋放第一個鎖,就進入了解鎖階段,此階段事務只能解鎖,也可以操作數據,不能再加鎖。
兩階段協議使得事務具有比較高的并發度,因為解鎖不必發生在事務結尾。
不過它沒有解決死鎖問題,因為它在加鎖階段沒有順序要求,如果兩個事務分別申請了A,B鎖,接著又申請對方的鎖,此時進入死鎖狀態。
Innodb事務隔離
在MVCC并發控制中,讀操作可以分為兩類:快照讀和當前讀。
快照讀讀取的是記錄的可見版本(有可能是歷史版本),不用加鎖。
當前讀,讀取的是記錄的最新版本,并且當前讀返回的記錄都會加上鎖,保證其他事務不再會并發修改這條記錄。
- Read Uncommited:可以讀未提交記錄
- Read Committed(RC):當前讀操作保證對獨到的記錄加鎖,存在幻讀現象。使用MVCC,但是讀取數據時讀取自身版本和最新版本,以最新為主,可以讀已提交記錄,存在不可重復
- Repeatable Read(RR):當前讀操作保證對讀到的記錄加鎖,同時保證對讀取的范圍加鎖,新的滿足查詢條件的記錄不能夠插入(間隙鎖),不存在幻讀現象。使用MVCC保存兩個事務操作的數據互相隔離,不存在不可重復讀現象。
- Serializable:MVCC并發控制退化為基于鎖的并發控制。不區分快照讀和當前讀,所有讀操作均為當前讀,讀加S鎖,寫加X鎖。
MVCC多版本并發控制
MVCC是一種多版本并發控制機制。鎖機制可以控制并發操作,但是其系統開銷較大,而MVCC可以在大多數情況下替代行級鎖,降低系統開銷。
MVCC是通過保存數據在某個時間點的快照來實現的,典型的有樂觀并發控制和悲觀并發控制。
InnoDB的MVCC,是通過在每行記錄后面保存兩個隱藏的列來實現的,這兩個列,分別保存這個行的創建時間和刪除時間,這里存儲的并不是實際的時間值,而是版本號,可以理解為事務的ID。每開始一個新的事務,這個版本號就會自動遞增。
對于幾種的操作:
- INSERT:為新插入的每一行保存當前版本號作為版本號
- UPDATE:新插入一行記錄,并且保存其創建時間為當前事務ID,同時保存當前
- DELETE:為刪除的每一行保存當前版本號作為版本號
- SELECT:
- InnoDB只會查找版本號小于等于事務系統版本號
- 行的刪除版本要么未定義要么大于當前事務版本號,這樣可以確保事務讀取的行,在事務開始刪除前未被刪除
事實上,在讀取滿足上述兩個條件的行時,InnoDB還會進行二次檢查。
活躍事務列表:RC隔離級別下,在語句開始時從全局事務表中獲取活躍(未提交)事務構造Read View,RR隔離級別下,事務開始時從全局事務表獲取活躍事務構造Read View:
1、取當前行的修改事務ID,和Read View中的事務ID做比較,若小于最小的ID或小于最大ID但不在列表中,轉2步驟。若是大于最大ID,轉3
2、若進入此步驟,可說明,最后更新當前行的事務,在構造Read View時已經提交,返回當前行數據
3、若進入此步驟,可說明,最后更新當前行的事務,在構造Read View時還未創建或者還未提交,取undo log中記錄的事務ID,重新進入步驟1.
根據上面策略,在讀取數據的時候,InnoDB幾乎不用獲得任何鎖,每個查詢都能通過版本查詢,只獲得自己需要的數據版本,從而大大提高了系統并發度。
缺點是:每行記錄都需要額外的存儲空間,更多的行檢查工作,額外的維護工作。
一般我們認為MVCC有幾個特點:
- 每個數據都存在一個版本,每次數據更新時都更新該版本
- 修改時copy出當前版本修改,各個事務之間沒有干擾
- 保存時比較版本號,如果成功,則覆蓋原記錄;失敗則rollback
看上去保存是根據版本號決定是否成功,有點樂觀鎖意味,但是Innodb實現方式是:
- 事務以排他鎖的形式修改原始數據
- 把修改前的數據存放于undo log,通過回滾指針與主數據關聯
- 修改成功后啥都不做,失敗則恢復undo log中的數據。
innodb沒有實現MVCC核心的多版本共存,undo log內容只是串行化的結果,記錄了多個事務的過程,不屬于多版本共存。當事務影響到多行數據,理想的MVCC無能為力。
如:事務1執行理想MVCC,修改row1成功,修改row2失敗,此時需要回滾row1,但是由于row1沒有被鎖定,其數據可能又被事務2修改,如果此時回滾row1內容,會破壞事務2的修改結果,導致事務2違反ACID。
理想的MVCC難以實現的根本原因在于企圖通過樂觀鎖代替二階段提交。修改兩行數據,但為了保證其一致性,與修改兩個分布式系統數據并無區別,而二階段提交是目前這種場景保證一致性的唯一手段。二階段提交的本質是鎖定,樂觀鎖的本質是消除鎖定,二者矛盾。innodb只是借了MVCC名字,提供了讀的非阻塞。
采用MVCC方式,讀-寫操作彼此并不沖突,性能更高;如果采用加鎖方式,讀-寫操作彼此需要排隊執行,從而影響性能。一般情況下,我們更愿意使用MVCC來解決讀-寫操作并發執行的問題,但是在一些特殊業務場景中,要求必須采用加鎖的方式執行。
常用語句 與 鎖的關系
對讀取的記錄加S鎖:
select ... lock in share mode;對讀取的記錄加X鎖:
select ... for update;delete:
對一條語句執行delete,先在B+樹中定位到這條記錄位置,然后獲取這條記錄的X鎖,最后執行delete mark操作。
update:
- 如果未修改該記錄鍵值并且被更新的列所占用的存儲空間在修改前后未發生變化,則現在B+樹定位到這條記錄的位置,然后再獲取記錄的X鎖,最后在原記錄的位置進行修改操作。
- 如果為修改該記錄的鍵值并且至少有一個被更新的列占用的存儲空間在修改后發生變化,則先在B+樹中定位到這條記錄的位置,然后獲取記錄的X鎖,然后將原記錄刪除,再重新插入一個新的記錄。
- 如果修改了該記錄的鍵值,則相當于在原記錄上執行delete操作之后再來一次insert操作。
insert:
新插入的一條記錄收到隱式鎖保護,不需要在內存中為其生成對應的鎖結構。
意向鎖
為了允許行鎖和表鎖共存,實現多粒度鎖機制。InnoDB還有兩種內部使用的意向鎖,兩種意向鎖都是表鎖。
意向共享鎖(IS):事務打算給數據行加行共享鎖,事務在給一個數據行加共享鎖前必須先取得該表的IS鎖
意向排他鎖(IX):事務打算給數據行加排他鎖,事務在給一個數據行加排他鎖前必須先取得該表的IX鎖。
意向鎖僅僅用于表鎖和行鎖的共存使用。它們的提出僅僅是為了在之后加表級S鎖或者X鎖是可以快速判斷表中的記錄是否被上鎖,以避免用遍歷的方式來查看表中有沒有上鎖的記錄。
需要注意的三點:
1、意向鎖是表級鎖,但是卻表示事務正在讀或寫某一行記錄
2、意向鎖之間不會沖突,因為意向鎖僅僅代表對某行記錄進行操作,在加行鎖的時候會判斷是否沖突
3、意向鎖是InnoDB自動加的,不需要用戶干預。
行級鎖
-
Record Lock:就是普通的行鎖,官方名稱:LOCK_REC_NOT_GAP,用來鎖住在聚集索引上的一條行記錄
-
Gap Lock:用來在可重復讀隔離級別下解決幻讀現象。已知幻讀還有一種方法解決:MVCC,還一種就是加鎖。但是在使用加鎖方案時有個問題,事務在第一次執行讀取操作時,“幻影記錄”還沒有插入,所以我們無法給“幻影記錄”加上Record Lock。InnoDB提出了Gap鎖,官方名稱:LOCK_GAP,若一條記錄的numberl列為8,前一行記錄number列為3,我們在這個記錄上加上gap鎖,意味著不允許別的事務在number值為(3,8)區間插入記錄。只有gap鎖的事務提交后將gap鎖釋放掉后,其他事務才能繼續插入。
注意:gap鎖只是用來防止插入幻影記錄的,共享gap和獨占gap起到作用相同。對一條記錄加了gap鎖不會限制其他事務對這條記錄加Record Lock或者繼續加gap鎖。另外對于向限制記錄后面的區間的話,可以使用Supremum表示該頁面中最大記錄。
-
Next-Key Lock:當我們既想鎖住某條記錄,又想阻止其他事務在該記錄前面的間隙插入新記錄,使用該鎖。官方名稱:LOCK_ORDINARY,本質上就是上面兩種鎖的結合。
-
Insert Intention Lock:一個事務在插入一條你記錄時需要判斷該區間點上是否存在gap鎖或Next-Key Lock,如果有的話,插入就需要阻塞。設計者規定,事務在等待時也需要在內存中生成一個鎖結構,表明有個事務想在某個間隙中插入記錄,但是處于等待狀態。這種狀態鎖稱為Insert Intention Lock,官方名稱:LOCK_INSERT_INTENTION,也可以稱為插入意向鎖。
2、鎖的內存結構以及一些解釋
一個事務對多條記錄加鎖時不一定就要創建多個鎖結構。如果符合下面條件的記錄的鎖可以放到一個鎖結構中:
- 在同一個事務中進行加鎖操作
- 被加鎖的記錄在同一個頁面中
- 加鎖的類型是一樣的
- 等待狀態是一樣的
type_mode是一個32位比特的數,被分為lock_mode、lock_type、rec_lock_type三個部分。
低4位表示:lock_mode,鎖的模式
0:表示IS鎖
1:表示IX鎖
2:表示S鎖
3:表示X鎖
4:表示AI鎖,就是auto-inc,自增鎖
第5~8位表示:lock_type,鎖的類型
LOCK_TABLE:第5位為1,表示表級鎖
LOCK_REC:第6位為1,表示行級鎖
其余高位表示:rec_lock_type,表示行鎖的具體類型,只有lock_type的值為LOCK_REC時,才會出現細分
LOCK_ORDINARY:為0,表示next-key鎖
LOCK_GAP:為512,即當第10位設置為1時,表示gap鎖
LOCK_REC_NOT_GAP:為1024,當第11位設置為1,表示正常記錄鎖
LOCK_INSERT_INTENTION:為2048,當第12位設置為1時,表示插入意向鎖
LOCK_WAIT:為256,當第9位設置為1時,表示is_waiting為false,表明當前事務獲取鎖成功。
一堆比特位
其他信息:涉及了一些哈希表和鏈表
更加細節的結構可以看這一張圖:
3、InnoDB的鎖代碼實現
鎖系統結構lock_sys_t
詳細講解見:https://dev.mysql.com/doc/dev/mysql-server/latest/structlock__sys__t.html#details
鎖系統結構,在innodb啟動的時候初始化,在innodb結束時釋放。保存鎖的hash表,相關事務、線程的一些信息
lock_t 、lock_rec_t 、lock_table_t
無論是行鎖還是表鎖都使用lock_t結構保存,其中用一個union來分別保存行鎖和表鎖不同的數據,分別為lock_table_t和lock_rec_t
/** Lock struct; protected by lock_sys latches */ struct lock_t {/** transaction owning the lock */trx_t *trx;/** list of the locks of the transaction */UT_LIST_NODE_T(lock_t) trx_locks;/** Index for a record lock */dict_index_t *index;/** Hash chain node for a record lock. The link node in a singlylinked list, used by the hash table. */lock_t *hash;union {/** Table lock */lock_table_t tab_lock;/** Record lock */lock_rec_t rec_lock;};/** Record lock for a page */ struct lock_rec_t {/** The id of the page on which records referenced by this lock's bitmap arelocated. */page_id_t page_id;/** number of bits in the lock bitmap;NOTE: the lock bitmap is placed immediately after the lock struct */uint32_t n_bits;/** Print the record lock into the given output stream@param[in,out] out the output stream@return the given output stream. */std::ostream &print(std::ostream &out) const; };struct lock_table_t {dict_table_t *table; /*!< database table in dictionarycache */UT_LIST_NODE_T(lock_t)locks; /*!< list of locks on the sametable *//** Print the table lock into the given output stream@param[in,out] out the output stream@return the given output stream. */std::ostream &print(std::ostream &out) const; };bitmap
Innodb 使用位圖來表示鎖具體鎖住了那幾行,在函數 lock_rec_create 中為 lock_t 分配內存空間的時候,會在對象地址后分配一段內存空間(當前行數 + 64)用來保存位圖。n_bits 表示位圖大小。
鎖的基本模式的兼容關系和強弱關系
/* LOCK COMPATIBILITY MATRIX* IS IX S X AI* IS + + + - +* IX + + - - +* S + - + - -* X - - - - -* AI + + - - -** Note that for rows, InnoDB only acquires S or X locks.* For tables, InnoDB normally acquires IS or IX locks.* S or X table locks are only acquired for LOCK TABLES.* Auto-increment (AI) locks are needed because of* statement-level MySQL binlog.* See also lock_mode_compatible().*/ static const byte lock_compatibility_matrix[5][5] = {/** IS IX S X AI *//* IS */ { TRUE, TRUE, TRUE, FALSE, TRUE},/* IX */ { TRUE, TRUE, FALSE, FALSE, TRUE},/* S */ { TRUE, FALSE, TRUE, FALSE, FALSE},/* X */ { FALSE, FALSE, FALSE, FALSE, FALSE},/* AI */ { TRUE, TRUE, FALSE, FALSE, FALSE}type_mode };/* STRONGER-OR-EQUAL RELATION (mode1=row, mode2=column)* IS IX S X AI* IS + - - - -* IX + + - - -* S + - + - -* X + + + + +* AI - - - - +* See lock_mode_stronger_or_eq().*/ static const byte lock_strength_matrix[5][5] = {/** IS IX S X AI *//* IS */ { TRUE, FALSE, FALSE, FALSE, FALSE},/* IX */ { TRUE, TRUE, FALSE, FALSE, FALSE},/* S */ { TRUE, FALSE, TRUE, FALSE, FALSE},/* X */ { TRUE, TRUE, TRUE, TRUE, TRUE},/* AI */ { FALSE, FALSE, FALSE, FALSE, TRUE} };行鎖類別代碼
#define LOCK_WAIT \256 /*!< Waiting lock flag; when set, it \means that the lock has not yet been \granted, it is just waiting for its \turn in the wait queue */ /* Precise modes */ #define LOCK_ORDINARY \0 /*!< this flag denotes an ordinary \next-key lock in contrast to LOCK_GAP \or LOCK_REC_NOT_GAP */ #define LOCK_GAP \512 /*!< when this bit is set, it means that the \lock holds only on the gap before the record; \for instance, an x-lock on the gap does not \give permission to modify the record on which \the bit is set; locks of this type are created \when records are removed from the index chain \of records */ #define LOCK_REC_NOT_GAP \1024 /*!< this bit means that the lock is only on \the index record and does NOT block inserts \to the gap before the index record; this is \used in the case when we retrieve a record \with a unique key, and is also used in \locking plain SELECTs (not part of UPDATE \or DELETE) when the user has set the READ \COMMITTED isolation level */ #define LOCK_INSERT_INTENTION \2048 /*!< this bit is set when we place a waiting \gap type record lock request in order to let \an insert of an index record to wait until \there are no conflicting locks by other \transactions on the gap; note that this flag \remains set when the waiting lock is granted, \or if the lock is inherited to a neighboring \record */ #define LOCK_PREDICATE 8192 /*!< Predicate lock */ #define LOCK_PRDT_PAGE 16384 /*!< Page lock */記錄鎖的alloc函數
Create the lock instance,創建一個lock實例,在create函數中被調用。主要就是分配一些內存,還有設置事務請求記錄鎖、鎖的索引號、鎖的模式、行鎖的pageid、n_bits。
/** Create the lock instance @param[in, out] trx The transaction requesting the lock @param[in, out] index Index on which record lock is required @param[in] mode The lock mode desired @param[in] rec_id The record id @param[in] size Size of the lock + bitmap requested @return a record lock instance */ lock_t *RecLock::lock_alloc(trx_t *trx, dict_index_t *index, ulint mode,const RecID &rec_id, ulint size) {ut_ad(locksys::owns_page_shard(rec_id.get_page_id()));/* We are about to modify structures in trx->lock which needs trx->mutex */ut_ad(trx_mutex_own(trx));lock_t *lock;if (trx->lock.rec_cached >= trx->lock.rec_pool.size() ||sizeof(*lock) + size > REC_LOCK_SIZE) {ulint n_bytes = size + sizeof(*lock);mem_heap_t *heap = trx->lock.lock_heap;lock = reinterpret_cast<lock_t *>(mem_heap_alloc(heap, n_bytes));} else {lock = trx->lock.rec_pool[trx->lock.rec_cached];++trx->lock.rec_cached;}lock->trx = trx;lock->index = index;/* Note the creation timestamp */ut_d(lock->m_seq = lock_sys->m_seq.fetch_add(1));/* Setup the lock attributes */lock->type_mode = LOCK_REC | (mode & ~LOCK_TYPE_MASK);lock_rec_t &rec_lock = lock->rec_lock;/* Predicate lock always on INFIMUM (0) */if (is_predicate_lock(mode)) {rec_lock.n_bits = 8;memset(&lock[1], 0x0, 1);} else {ut_ad(8 * size < UINT32_MAX);rec_lock.n_bits = static_cast<uint32_t>(8 * size);memset(&lock[1], 0x0, size);}rec_lock.page_id = rec_id.get_page_id();/* Set the bit corresponding to rec */lock_rec_set_nth_bit(lock, rec_id.m_heap_no);MONITOR_INC(MONITOR_NUM_RECLOCK);MONITOR_INC(MONITOR_RECLOCK_CREATED);return (lock); }記錄鎖的add函數
將鎖添加到記錄鎖哈希和事務的鎖列表中。
void RecLock::lock_add(lock_t *lock) {ut_ad((lock->type_mode | LOCK_REC) == (m_mode | LOCK_REC));ut_ad(m_rec_id.matches(lock));ut_ad(locksys::owns_page_shard(m_rec_id.get_page_id()));ut_ad(locksys::owns_page_shard(lock->rec_lock.page_id));ut_ad(trx_mutex_own(lock->trx));bool wait = m_mode & LOCK_WAIT;hash_table_t *lock_hash = lock_hash_get(m_mode);lock->index->table->n_rec_locks.fetch_add(1, std::memory_order_relaxed);if (!wait) {lock_rec_insert_to_granted(lock_hash, lock, m_rec_id);} else {lock_rec_insert_to_waiting(lock_hash, lock, m_rec_id);}#ifdef HAVE_PSI_THREAD_INTERFACE #ifdef HAVE_PSI_DATA_LOCK_INTERFACE/* The performance schema THREAD_ID and EVENT_ID are used onlywhen DATA_LOCKS are exposed. */PSI_THREAD_CALL(get_current_thread_event_id)(&lock->m_psi_internal_thread_id, &lock->m_psi_event_id); #endif /* HAVE_PSI_DATA_LOCK_INTERFACE */ #endif /* HAVE_PSI_THREAD_INTERFACE */locksys::add_to_trx_locks(lock);if (wait) {lock_set_lock_and_trx_wait(lock);} }記錄鎖的create函數
就是調用alloc,然后add加鎖,
Create a lock for a transaction and initialise it. @param[in, out] trx Transaction requesting the new lock @param[in] prdt Predicate lock (optional) @return new lock instance */ lock_t *RecLock::create(trx_t *trx, const lock_prdt_t *prdt) {ut_ad(locksys::owns_page_shard(m_rec_id.get_page_id()));/* Ensure that another transaction doesn't access the trxlock state and lock data structures while we are adding thelock and changing the transaction state to LOCK_WAIT.In particular it protects the lock_alloc which uses trx's private pool oflock structures.It might be the case that we already hold trx->mutex because we got here from:- lock_rec_convert_impl_to_expl_for_trx- add_to_waitq*/ut_ad(trx_mutex_own(trx));/* Create the explicit lock instance and initialise it. */lock_t *lock = lock_alloc(trx, m_index, m_mode, m_rec_id, m_size);#ifdef UNIV_DEBUG/* GAP lock shouldn't be taken on DD tables with some exceptions */if (m_index->table->is_dd_table &&strstr(m_index->table->name.m_name,"mysql/st_spatial_reference_systems") == nullptr &&strstr(m_index->table->name.m_name, "mysql/innodb_table_stats") ==nullptr &&strstr(m_index->table->name.m_name, "mysql/innodb_index_stats") ==nullptr &&strstr(m_index->table->name.m_name, "mysql/table_stats") == nullptr &&strstr(m_index->table->name.m_name, "mysql/index_stats") == nullptr) {ut_ad(lock_rec_get_rec_not_gap(lock));} #endif /* UNIV_DEBUG */if (prdt != nullptr && (m_mode & LOCK_PREDICATE)) {lock_prdt_set_prdt(lock, prdt);}lock_add(lock);return (lock); }4、鎖的流程
lock system 開始啟動 申請lock_sys_t結構,初始化結構體
lock system 結束關閉 釋放lock_sys_t結構的元素,釋放結構體
表鎖加鎖流程
1、檢查當前事務是否擁有更強的表鎖,如果有的話直接返回成功,否則繼續往下走2、遍歷表的鎖列表,判斷是否有沖突的鎖,沒有轉3,有轉43、直接創建一個表鎖,放入事務的lock list中,放入table 的lock list中,加鎖成功4、創建等待的表鎖,然后進行死鎖檢測和死鎖解決,回滾當前事務或者掛起當前事務行鎖加鎖流程
插入加鎖流程
1、對表加IX鎖2、對修改的頁面加X鎖3、如果需要檢測唯一鍵沖突,嘗試給需要加的唯一鍵加一個S | next-key lock。可能會產生鎖等待4、判斷是否插入意向鎖沖突,沖突的話加等待的插入意向鎖,不沖突直接插入數據5、釋放頁面鎖刪除加鎖流程帶來的死鎖
刪除加鎖有個問題,刪除并發的時候的加鎖會導致死鎖。
1、事務1獲取表IX鎖2、事務1獲取頁面X鎖3、事務1獲取第n行的 x | not gap鎖4、事務1刪除第n行5、事務1釋放頁面X鎖6、事務2獲取頁面X鎖7、事務2嘗試獲取第n行的 x | not gap鎖,發現沖突,等待8、事務2釋放頁面X鎖9、事務1釋放第n行的鎖,提交事務10、釋放第n行鎖的時候,檢查到事務2有一個等待鎖,發現可以加鎖了,喚醒事務2,成功加鎖11、事務3獲取頁面X鎖12、事務3嘗試刪除第n行,發現第n行已經被刪除,嘗試獲取第n行的next-key lock,發現事務2有個 x| gap鎖沖突,等待13、事務3釋放頁面X鎖14、事務2獲取頁面X鎖,檢查頁面是否改動,重新檢查第n行數據,發現被刪,嘗試獲取該行next- key lock,發現事務3在等待這個鎖,事務2沖突,進入等待15、造成死鎖釋放鎖流程
死鎖流程
構造wait-for graph
構造一個有向圖,圖中的節點代表一個事務,圖的一個邊A->B代表著A事務等待B事務的一個鎖
具體實現是在死鎖檢測時,從當前鎖的事務開始搜索,遍歷當前行的所有鎖,判斷當前事務是否需要等待現有鎖釋放,是的話,代表有一條邊,進行一次入棧操作
死鎖檢測
有向圖判斷環,用棧的方式,如果有依賴等待,進行入棧,如果當前事務所有依賴的事務遍歷完畢,進行一次出棧
回滾事務選擇
如果發現循環等待,選擇當前事務和等待的事務其中權重小的一個回滾,具體的權重比較函數是 trx_weight_ge, 如果一個事務修改了不支持事務的表,那么認為它的權重較高,否則認為 undo log 數加持有的鎖數之和較大的權重較高。
5、參考
1、https://segmentfault.com/a/1190000017076101?utm_source=coffeephp.com
2、Mysql 8.022源代碼
3、深入淺出MySQL 8.0 lock_sys鎖相關優化
count()用法
count()語義:該函數為一個聚合函數,對于返回的結果集一行行地判斷,如果count函數地參數不是NULL,累計值就加1,否則不加。最后返回累計值。
所以count(*),count(主鍵id)和count(1)都表示返回滿足條件地結果集地總行數;
而count(字段)則表示返回滿足條件地數據行里面,參數“字段”不為NULL的總個數。
count(主鍵id):
InnoDB引擎會遍歷整張表,把每一行的id值都取出來,返回給server層。
sever層拿到id后,判斷id是不可能為空的,就按行累加
count(1):
InnoDB引擎遍歷整張表,但不取值。server層對于返回的每一行,放一個數字“1”進去,判斷是不可能為空的,按行累加
count(字段):
1、如果這個字段定義為not null的話,一行行地從記錄里面讀出這個字段,判斷不能為null,按行累加
2、如果這個字段允許為null,那么在執行的時候,要判斷字段是否為null,不是null才累加
count(*):
不會把全部字段取出來,而是專門做了優化,不取值。并且count(*)肯定不是null,按行累加。
所以按照效率排序的話: count(字段) < count(主鍵id) < count(1) 約等于 count(*)
InnoDB是支持事務的,MyISAM不支持事務。
InnoDB每一行記錄都要判斷自己是否對這個會話是否可見,所以對于count(*)請求來說,InnoDB只好把數據一行一行地讀出依次判斷,可見地行才能夠用于計算“基于這個查詢”地表地總行數。
《MySQL——join語句優化tips》
要不要用join
1、如果使用的是Index Nested-Loop Join算法,即可以用上被驅動表的索引,可以用
2、如果使用的是Block Nested-Loop Join算法。掃描行數過多,尤其是大表join會導致掃描多次被驅動表,會占用大量系統資源,這種Join盡量不要用
Join驅動表選擇
1、如果是Index Nested-Loop Join算法,使用小表做驅動表
2、如果是Block Nested-Loop Join算法,在 join_buffer_size 足夠大,大表小表一樣,當 join_buffer_size 不夠大時,選擇小表做驅動表
注意,在決定哪個表做驅動表時,應該是兩個表按照各自條件過濾完成之后,計算參與join的各個字段的總數據量,數據量小的表,那就是小表。
Multi-Range Read優化
若有這樣查詢語句:
select * from t1 where a>=1 and a<=100;a值是遞增的,但是回表后的id并非如此,而是隨機的,會帶來性能損失。
大多數數據按照主鍵遞增順序插入得到,所以我們可以認為如果按照主鍵的遞增順序查找的話,對磁盤的讀比較接近順序讀,從而可以提升讀性能。
1、根據索引a,定位到滿足條件的記錄,將id值放入read_rnd_buffer中;
2、將read_rnd_buffer中的id進行遞增排序;
3、排序后的id數組,依次到主鍵id索引中查找記錄,并作為結果返回
總的來說就是:**先將索引數據緩存,查到id之后,排序之后再回表 **
用法:
設置:
set optimizer_switch="mrr_cost_based=off現在的優化器在判斷消耗時,更傾向于不使用MRR,所以需要設置為off后,就會固定使用MRR
Batched Key Access (BKA)對NLJ進行優化
Index Nested-Loop Join執行邏輯是:從驅動表t1,一行行取出a值,再到驅動表t2去做join。對于表t2來說,每次都是匹配一個值,MMR優勢用不上。
既然這樣,將表t1的數據取出來一部分,先放到一個臨時內存里:join_buffer.
然后在此基礎上復用MRR即可。
使用方法:
set optimizer_switch='mrr=on,mrr_cost_based=off,batched_key_access=on';BNL算法性能問題
之前提到過InnoDB的LRU優化:第一次從磁盤讀入內存的數據頁,會先放到old區域,如果1s后這個數據頁不再被訪問,就不會移動到LRU鏈表頭部,這樣對Buffer Pool命中率影響就不大了。
如果使用了BNL的join語句,多次掃描一個冷表,并且這個語句執行時間超過1s,就會在再次掃描冷表時,把冷表的數據頁移動到LRU鏈表頭部。
如果冷表數據很大, 會一直占據old區,正常頁無法進入,無法更新young區
tips: 冷表,指表中數據還沒有加載到bufferpool中,需要先從盤里讀出來的表
又因為優化機制,一個正常訪問的數據頁要進入young區域,需要隔1s再次被訪問到。由于join’語句在循環都磁盤和淘汰內存頁,進入old區域的數據頁很可能在1s之內就被淘汰了。
大表join后對于Buffer Pool的影響是持續性的,需要依靠后續的查詢請求慢慢恢復內存命中率。
總結,BNL對于系統的影響:
1、可能多次掃描被驅動表,占用磁盤IO資源
2、判斷join條件執行M * N次,占用CPU資源
3、可能導致Buffer Pool的熱數據被淘汰,影響內存命中率
所以我們需要優化BNL,通過給驅動表的join字段加索引的方式,將BNL轉換為BKA
BNL轉BKA
對于一些不常執行大表join的sql,不在被驅動表上創建索引的情況,可以創建一個臨時表 create templete table在這個臨時表上創建索引,然后讓驅動表與臨時表做join操作。 為什么不在被驅動表上創建索引,是因為這塊sql功能不常用,創建索引浪費空間,并且可能觸發這塊的join sql 也不經常調用。
創建臨時表以及join語句示例如下:
create temporary table temp_t(id int primary key, a int, b int, index(b))engine=innodb; insert into temp_t select * from t2 where b>=1 and b<=2000; select * from t1 join temp_t on (t1.b=temp_t.b);《MySQL——redo log 與 binlog 寫入機制》
WAL機制告訴我們:只要redo log與binlog保證持久化到磁盤里,就能確保MySQL異常重啟后,數據可以恢復。
下面主要記錄一下MySQL寫入binlog和redo log的流程。
binlog寫入機制
1、事務執行過程中,先把日志寫到binlog cache,事務提交的時候,再把binlog cache寫到binlog文件中。
2、binlog cache,系統為每個線程分配了一片binlog cache內存,參數binlog_cache_size控制單個線程內binlog cache大小。如果超過了這個大小就要暫存磁盤
3、事務提交的時候,執行器把binlog cache里完整的事務寫入binlog中。并清空binlog cache
4、每個線程都有自己的binlog cache,共用一份binlog文件
5、write,是把日志寫入到文件系統的page cache,內存中,沒有持久化到磁盤,所以速度比較快,圖中的fsync是將數據持久化到磁盤,占用磁盤的IOPS
關于何時write、fsync是由參數sync_binlog控制的:
1、sync_binlog = 0時,每次提交事務都只write,不fsync; 2、sync_binlog = 1時,每次提交事務都會執行fsync; 3、sync_binlog = N(N>1)時,表示每次提交事務都write,但累積N個事務后才fsync。sync_binlog控制binlog真正刷盤的頻率,對于一個IO非常大的情景,這個數字調大可以提高性能,但是如果容錯率非常低的情況下,必須設為1.(sync_binlog設置為N對應的風險是:如果主機發生異常重啟,會丟失最近N個事務的binlog日志)
redo log寫入機制
事務在執行過程中,生成的redo log是要先寫到redo log buffer的。
redo log buffer里面的內容并不需要每次生成后都要持久化到磁盤中。
如果事務執行期間MySQL發生異常重啟,那么這部分日志就丟了。由于事務并沒有提交,所以這時日志丟了也不會有損失。
事務沒提交的時候,redo log buffer部分日志也是有可能被持久化到磁盤中的。
上面三個顏色表征了redo log可能的三種狀態:
1、存在redo log buffer中,物理上是在MySQL進程內存中,即紅色部分;
2、寫到磁盤(write),但是沒有持久化(fsync),物理上實在文件系統的page cache里面,即黃色部分;
3、持久化到磁盤,對應的是hard disk,也就是圖中的綠色部分;
前兩步是寫內存,最后一步是磁盤IO,所以要在page cache夠大且不影響寫入page cache前將redo log 持久化到磁盤 。
為了控制redo log 的寫入策略,InnoDB提供了innodb_flush_log_at_trx_commit參數,他有三種可能取值:
1、設置為0,每次事務提交的時候都只是把redo log留在redo log buffer中; 2、設置為1,每次事務提交的時候都只是把redo log直接持久化到磁盤; 3、設置為2,每次事務提交時都只是把redo log寫到page cache;與binlog不同,binlog是每個線程都有一個binlog cache,而redo log是多個線程共用一個redo log buffer。
InnoDB有一個后臺線程,每隔1s,就會把redo log buffer中的日志,調用write寫到文件系統的page cache,然后調用fsync持久化到磁盤,事務執行過程中的redo log也是直接寫在redo log buffer上的,所以,未提交的事務的redolog也可能被持久化到磁盤。
還有兩種場景也會導致沒有提交的事務的redo log寫入到磁盤中:
情形1:
redo log buffer占用的空間即將達到innodb_log_buffer_size一半的時候,后臺線程會主動寫盤。
(這里只是write,沒有fsync)
情形2:
并行的事務提交的時候,順帶將這個事務的redo log buffer持久化到磁盤。
(事務A執行一半,部分redo log到buffer中;事務B提交,且 innodb_flush_log_at_trx_commit ,會把redo log buffer里的log全部持久化到磁盤中)
補充說明
兩階段提交在時序上redo log先prepare 再寫binlog,最后再把redo log commit;
innodb_flush_log_at_trx_commit 設置成 1,prepare階段redo log就已經落盤。所以redo log再commit的時候就不需要fsync了,只會write到文件系統的page cache中就夠了。
sync_binlog 和 innodb_flush_log_at_trx_commit都設置為1,即一個事務完整提交前,需要等待兩次刷盤,一次是redo log(prepare階段),一次是binlog。
組提交機制實現大量的TPS
首先介紹日志邏輯序列號(log sequence number,LSN)的概念。LSN是單調遞增的,每次寫入長度length的redo log,LSN的值就會加上length。
三個并發事務(trx1,trx2,trx3)在prepare階段,都寫完redo buffer,并持久化到磁盤。
對應的LSN為50、120、160.
對應流程:
1、trx1第一個到達,被選為這組的leader;
2、等trx1要開始寫盤的時候,這個組里面已經有三個事務,這時候LSN也變成了160;
3、trx1去寫盤的時候,LSN=160;trx1返回時,所有LSN<= 160的redo log都被持久化到磁盤中;
4、trx2與trx3直接返回。
總結:
一次組提交中,組員越多,節約磁盤IOPS的效果越好。如果是單線程,就只能一個事務對應一次持久化操作
| 兩階段提交 | 兩階段提交細化 |
這樣保證binlog也可以組提交了。由于step3速度快,所以集合到一起的binlog比較少,所以binlog的組提交效果不如redo log組提交。
提升binlog效果:
--1.binlog_group_commit_sync_delay :b表示延遲多少微秒后才調用fsync; --2.binlog_group_commit_sync_no_delay_count :表示累積多少次以后才調用fsync;理解WAL機制
WAL機制是減少磁盤寫,可是每次提交事務都要寫redo log和binlog ,磁盤讀寫次數沒有變少。
所以WAL機制主要得益于兩個方面:
--1、redo log和binlog都是順序寫,磁盤的順序寫比隨機寫速度要快 --2、組提交機制,可以降低磁盤IOPS消耗如何提升IO性能瓶頸
1、設置binlog_group_commit_sync_delay和binlog_group_commit_sync_no_delay_count參數,減少binlog的寫盤次數。這個方法是基于“額外故意等待”來實現的,可能會增加語句的響應時間,但是不會丟失數據
2、將sync_binlog設置為大于1的值(100~1000)。不過會有主機掉電時丟binlog日志的風險
3、將 innodb_flush_log_at_trx_commit 設置為2。會有主機掉電丟數據的風險
《MySQL——備庫多線程復制策略》
備庫并行復制能力
主要涉及兩個方面的并行度:
1、客戶端寫入主庫的能力
2、備庫上sql_thread執行中轉日志relay log
1的并行能力比2強。
主庫上由于InnoDB支持行鎖,對業務并行度的支持比較友好。
備庫上如果用單線程,會導致備庫應用日志不夠快,造成主備延遲。
現在MySQL使用的是多線程復制
coordinator 就是原來的sql_thread,不過現在它不再直接更新數據了,只負責讀取中轉日志和分發事務。真正更新日志的,是worker線程。線程個數由slave_parallel_workers決定,一般設置為8~16。
coordinator在分發事務的時候,要遵循兩個要求:
- 不能造成更新覆蓋。也就是說更新同一行的兩個事務必須被分發到同一個worker中。
- 同一個事務不能被拆開,必須放到同一個worker中。
MySQL5.6版本 并行復制策略
支持粒度:庫
用于決定分發策略的hash表key值:數據庫名
優勢:
1、構造hash值快;一個實例上的DB數目不會很多。
2、不要求binlog格式。row和statement格式的binlog都可以拿到庫名。
缺點:
1、主庫表在同一個DB中,策略失效
2、不同DB熱點不同,起不到并行效果
MariaDB 并行復制策略
策略:
1、能夠在同一組里提交的事務,一定不會修改同一行
2、主庫上可以并行執行的事務,備庫上一定是可以并行執行的
為了實現該策略,MariaDB實現方法為:
1、在一組里面一起提交的事務,有一個相同的commit_id,下一組就是commit_id+1
2、commit_id直接寫到binlog里
3、傳到備庫應用的時候,相同commit_id的事務分發到多個worker執行
4、一組全部執行完后,coordinator再去取下一批
這個策略目標就是備庫模擬主庫的并行模式。
不過主庫再一組事務commit的時候,下一組事務實際上是處于"執行中"狀態的。
而按照MariaDB策略,在備庫上執行的時候,要等一組事務完全執行完,下一組事務才能開始執行,這樣系統的吞吐量就不夠。
這個策略,對于長事務來說不友好。如果一組里有一個超大事務線程,該組其他線程執行完后要等待這個線程執行完,之后才能切換到下一組。這段時間,只有一個線程進行工作,浪費了資源。
MySQL5.7版本 并行復制策略
策略思想:
1、同時處于prepare狀態的事務,在備庫執行時是可以并行的
2、處于prepare狀態的事務,與處于commit狀態的事務之間,在備庫執行時也是可以并行的
通過調節binlog_group_commit_sync_delay和binlog_group_commit_sync_no_delay_count參數
來來拉長binlog從write到fsync的時間,以此減少binlog’的寫盤次數。同時在并行復制策略里,可以用來制造更多“同時處于prepare階段的事務”。這樣就能增加備庫復制的并行度。
通俗來講,這兩個參數,既可以讓主庫提交慢一點,又可以讓備庫執行快一點。在MySQL5.7處理備庫延遲時,可以調節這兩個參數,達到提升備庫復制并行度的目的。
MySQL5.7.22版本 并行復制策略
新增了一個參數binlog-transaction-dependency-tracking,用來控制是否啟用這個新策略。
可選值:
1、COMMIT_ORDER,表示根據同時進入prepare和commit來判斷是否可以并行
2、WRITESET,表示對于事務涉及更新的每一行,計算出這一行的hash值,組成集合writeset。如果兩個事務沒有操作相同的行,即writeset沒有交集,就可以并行。
3、WRITESET_SESSION,在WRITESET基礎上多了一個約束:在主庫上同一線程先后執行的兩個事務,在備庫執行的時候,要保證相同的先后順序
為了唯一標識,hash通過"庫名+表名+索引名+值"計算。如果表上除了主鍵索引外,還有其他唯一索引,那么對于每個唯一索引,insert語句對應的writeset就要多增加一個hash值。
這個版本的好處在于:
--1、writeset是在主庫生成后直接寫入到binlog里的,在備庫執行的時候,不需要解析binlog內容,節省了備庫計算量 --2、不需要把整個事務的binlog都掃一邊才能決定分發到哪個worker,更加節省內存 --3、備庫的分發策略不依賴于binlog內容,所以binlog是statement格式也是可以的對于表上沒有主鍵和外鍵約束的場景,WRITSET策略也沒有辦法并行,會暫時退化為單線程模型。 所以,表是否有主鍵,也是影響主備同步延遲原因之一。
總結
單線程復制能力低于多線程復制,對于更新壓力較大的主庫,備庫可能一直追不上主庫。
MySQL備庫并行策略,修改了binlog的內容,也就是說不是向上兼容的,所以需要注意。
《MySQL——查詢長時間不返回的三種原因與查詢慢的原因》
構造一張表,表有兩個字段id和c,再里面插入了10萬行記錄
create table 't' ('id' int(11) not null,'c' int(11) default null,primary key ('id') ) engine = InnoDB;delimiter ;; create procedure idata() begindeclare i int;set i = 1;while( i <= 100000) doinsert into t values(i,i);set i = i+1;end while; end;; delimiter ;call idata();查詢長時間不返回
在表t執行:
select * from t where id = 1;查詢結果長時間不返回。
等MDL鎖
大概率是表t被鎖住了,接下來分析原因:一般都是首先執行show processlist命令,看看當前語句處于什么狀態。
表示現在有個線程正在表t上請求或者持有MDL寫鎖,把select語句阻塞了:
session A通過lock table命令持有表t的MDL寫鎖,而sessionB 的查詢需要獲取MDL讀鎖,所以session B 進入等待狀態。
處理方式:找到誰持有MDL寫鎖,然后把它kill掉。
通過
select blocking_pid from sys.schema_table_lock_waits;得到blocking_pid = 4;
然后用kill命令斷開即可。
等flush
在表t上執行下面語句:
select * from information_schema.processlist where id=1;可以查看出該線程的狀態是Waiting for table flush;
表示現在有一個線程正要對表t做flush操作。
flush tables t with read lock; --只關閉表t --or flush tables with read lock; --關閉MySQL里面所有打開的表正常來說,這兩個語句執行起來都很快,除非它們也被別的線程堵住了。
所以可能是:有一個flush tables命令被別的語句堵住了,然后它又堵住了我們的select語句。
下圖是執行結果:
等行鎖
select * from t where id = 1 lock in share mode;由于訪問id = 1這個記錄時要加讀鎖,如果這時候已經有一個事務在這行記錄上持有一個寫鎖,我們的select語句就會被堵住,如下:
session A啟動事務,占用寫鎖,但是不提交,導致session B被堵住。
可以通過:
mysql> select * from t sys.innodb_lock_waits where locked_table='`test`.`t`'\G進行查詢,查出是誰占著這個寫鎖
發現是4號線程,然后我們kill 4
查詢慢
select * from t where c = 50000 limit 1;由于字段c上沒有索引,所以這個語句只能走id主鍵順序掃描,因此需要掃描5萬行。
掃描行數多,所以執行慢,這個很好理解。
而下面的這條語句掃描行數為1,但是執行時間取卻較長
select * from t where id = 1;
在這個場景下,session A先啟動了一個事務,之后session B才開始執行update語句。
session B 更新完 100 萬次,生成了 100 萬個回滾日志 (undo log)
帶lock in share mode 的sql是當前讀,因此會直接讀到 1000001 ,所以速度很快。
select * from t where id = 1語句是一致性讀,因此需要從 1000001 開始,依次執行undo log,執行100萬次后,才將1返回
**回滾日志過大引起的一致性讀慢,當前讀快 **
#《MySQL——幻讀與next-key lock與間隙鎖帶來的死鎖》
create table 't' ('id' int(11) not null,'c' int(11) default null,'d' int(11) default null,primary key ('id'),key 'c' ('c') ) engine = InnoDB;insert into t values(0,0,0),(5,5,5),(10,10,10),(15,15,15),(20,20,20),(25,25,25);
該表除了主鍵id,還有索引c。問下面的語句序列,是怎么加鎖的,加的鎖又是什么時候釋放的呢??```sql begin; select * from t where d = 5 for update; commit;這條語句會命中d=5這一行,對應主鍵id=5,因此在select語句執行完成后,id=5這一行會加一個寫鎖,并且由于兩階段鎖協議,這個寫鎖會在執行commit語句的時候釋放。
由于字段d上沒有索引,因此這條查詢語句會做全表掃描,那么,其他被掃描的不滿足的行記錄會不會被加鎖?
幻讀現象
如果旨在id=5這一行加鎖,而其他行不加鎖,在下面這個情況下:
session A執行了三次當前讀,并且加上了寫鎖。
幻讀指的是一個事務在前后兩次查詢同一個范圍的時候,后一次查詢看到了前一次查詢沒有看到的行。
幻讀與不可重復讀的區別
在同一個事務中,兩次讀取到的數據不一致的情況稱為幻讀和不可重復讀。幻讀是針對insert導致的數據不一致,不可重復讀是針對 delete、update導致的數據不一致。
1、在可重復讀隔離級別下,普通的查詢是快照讀,是不會看到別的事務插入的數據的。
2、session B的修改結果被session A之后的select語句用當前讀看到,不能稱為幻讀。幻讀僅僅指"新插入的行"
幻讀帶來的問題
1、破壞語義。
session A在T1就說了,把d=5的行鎖住,不準別的事務進行讀寫,此時被破壞。
因為如果我們這時插入d=5的數據,這條新的數據不在鎖的保護范圍之內。
2、數據一致性問題
鎖的設計是為了保證數據的一致性,不止是數據庫內部數據狀態在此刻的一致性,還包含了數據和日志在邏輯上的一致性。
即使給所有行加上了鎖,也避免不了幻讀,這是因為給行加鎖的時候,這條記錄還不存在,沒法加鎖 。
也就是說即使把所有的記錄都上鎖了,還是阻止不了新插入的記錄
如何解決幻讀
產生的幻讀的原因是:行鎖只能鎖住行
為了解決幻讀問題,InnoDB引入新的鎖:間隙鎖(Gap Lock)
間隙鎖,鎖的就是兩個值之間的空隙,比如在表t,初始化插入了6個記錄,就產生了7個間隙:
執行:
select * from t where d = 5 for update6個記錄加上了行鎖,同時加上了7個間隙鎖。
間隙鎖與行鎖有點不一樣
行鎖可以分為讀鎖與寫鎖
與行鎖有沖突關系的是另外一個行鎖。
間隙鎖不一樣,間隙鎖之間不存在沖突關系。
與間隙鎖存在沖突關系的,是"向間隙中插入一個記錄"這個操作。
舉例:
由于表t中并沒有c=7這個記錄,所以session A加的是間隙鎖(5,10)。而session B也是在這個間隙加的間隙鎖,它們的目標都是保護這個間隙,不允許插入值,所以兩者不沖突。
next-key lock
間隙鎖與行鎖合稱next-key lock,每個lock都是前開后閉區間。間隙鎖是開區間。
如上面我們插入數據,使用:
select * from t for update形成了7個next-key lock,分別是:
(-∞,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20, 25]、(25, +supremum]
supremum是一個不存在的最大值。
next-key lock 的引入解決了幻讀問題,但是也帶來了新的問題。
如,現在有這樣一個業務邏輯:
任意鎖住一行,如果這一行不存在的話就插入,如果存在這一行就更新它的數據。
begin; select * from t where id = N for update; --如果行不存在 insert into t values(N,N,N); --如果行存在 update t set d = N set id = N;commit;現在出現這個現象:這個邏輯一旦有并發,就會碰到死鎖。
死鎖的產生:兩個間隙鎖不沖突,相互等待行鎖
執行流程:
1、session A執行select…for update語句,由于id=9這一行不存在,因此會加上間隙鎖(5,10)
2、session B執行select…for update語句,同樣會加上間隙鎖(5,10)
3、session B插入(9,9,9),被session A的間隙鎖鎖住,進入等待
4、session A擦汗如·插入(9,9,9),被session B的間隙鎖鎖住。
InnoDB死鎖檢測發現了這對死鎖關系,然后報錯返回了。
所以說間隙鎖的引入可能會導致相同的語句鎖住更大的范圍,從而影響并發度。
間隙鎖是在可重復讀隔離級別下才會生效的。所以,你如果把隔離級別設置為讀提交的話,就沒有間隙鎖了。 但同時,你要解決可能出現的數據和日志不一致問題,需要把 binlog 格式設置為 row。
#《MySQL——臨時表》
## 內存表與臨時表區別
臨時表,一般是人手動創建。 內存表,是mysql自動創建和銷毀的。
內存表,指的是使用Memory引擎的表,建表語法:create table ... engine = memeory
表的數據存在內存里,系統重啟后會被清空,但是表的結構還在。
臨時表,可以使用各種引擎類型。如果使用的是InnoDB或者MyISAM引擎,寫數據是寫在磁盤上的。當然臨時表也可以使用Memory引擎。
臨時表特性
1、一個臨時表只能被創建它的session訪問,對于其他線程不可見,當此session結束時,會自動刪除臨時表
2、臨時表可以與普通表同名。如果同一個session里有同名的臨時表和普通表,使用show create語句以及增刪改查語句,訪問的是臨時表
3、show tables命令不顯示臨時表
臨時表的應用
由于不用擔心線程之間的重名沖突,臨時表經常被用在復雜查詢的優化過程中。其中,分庫分表系統的跨庫查詢就是一個典型的使用場景。
查詢語句到所有的分庫中查找滿足條件的行,然后統一做order by操作。
可以把各個分庫拿到的數據匯總到一個MySQL實例的一個表中,然后在這個匯總實例上做邏輯操作。如下:
至于臨時表的存儲位置,可以放在分庫中的某一個。
另外一個使用場景就是使用union(如果使用的是union all就不需要用了)。系統會先創建一個內部臨時表,執行第一個子查詢的結果放到臨時表中,執行第二個子查詢的結果先看看插入是否成功,成功則插入。最后從臨時表中按行取數據,然后返回結果,刪除臨時表。
臨時表可以重名的原因
無論是普通表還是臨時表,一個表都會對應一個table_def_def
- 一個普通表的table_def_def的值由"庫名+表名"得到。所以在同一個庫下創建兩個同名的普通表,會由重復性錯誤。
- 對于臨時表,table_def_def在“庫名+表名”的基礎上還加上了“server_id + thread_id”
在實現上,每個線程都維護了自己的臨時表鏈表,每次session內操作表的時候,先遍歷鏈表,檢查是否有這個名字的的臨時表,有就優先操作,否則再操作普通表。
session結束時,對鏈表中的每個臨時表,執行drop操作。這個操作也會被寫道binlog里用于主備復制。
臨時表的主備同步
row格式的binlog不會記錄臨時表相關語句,只有statement或者mixed格式才會記錄。
創建臨時表的語句會傳到備庫執行,因此備庫的同步線程就會創建這個臨時表。主庫在線程退出的時候會自動刪除臨時表,但是備庫同步線程還是在運行的,所以主庫還需要寫個DROP TEMPORARY TABLE傳給備庫。
當主庫上兩個session創建了同名臨時表t1,這兩個語句被傳給備庫上。
主庫執行語句的線程id會被寫道binlog中,備庫可以用線程id構造臨時表的table_def_key:
備庫名 + t1 + “主庫的serverid” + “session的thread_id”,所以兩個表在備庫的應用線程不會沖突。
#《MySQL——使用聯合索引、覆蓋索引,避免臨時表的排序操作》
## 聯合索引避免臨時表排序
在上一篇筆記(MySQL——order by邏輯(全字段排序與rowid排序))中,講到查詢語句查詢多個字段的時候使用order by語句實現返回值是有序的,而order by是使用到了臨時表的,會帶來時間和空間損失。
其實使用聯合索引,就可以避免臨時表的排序操作。
只要保證city這個索引上取出來的行天然就是按照name遞增排序的話,就可以不用再排序了。
在這個索引里面,通過樹搜索的方式定位到第一個滿足city = '杭州’的記錄,并且額外確保了,接下來按順序取“下一條記錄”的遍歷過程中,只要city值是杭州,name值一定有序。
查詢流程變為:
1、從索引(city,name)找到第一個滿足city = '杭州’條件的主鍵id;
2、到主鍵id索引取出整行,取name、city、age三個字段值,作為結果集的一部分直接返回
3、從索引(city,name)取下一個記錄主鍵id;
4、重復step2、3直到查到第1000條記錄,或者不滿足city = '杭州’條件時循環結束。
覆蓋索引優化查詢
可以使用覆蓋索引繼續優化查詢的執行流程:
覆蓋索引指,索引上的信息足夠滿足查詢請求,不需要再回到主鍵索引上取數據。
針對select city,name,age from t 這個查詢,可以創建一個city、name和age的聯合索引,對應語句為:
這時,對于city字段的值相同的行來說,還是按照name字段的值遞增排序。查詢語句的執行流程變為:
1、從索引(city,name,age)找到第一個滿足city = '杭州’條件的記錄,取出其中的city、name和age三個字段值,作為結果集的一部分直接返回
2、從索引(city,name,age)取下一個記錄,同樣取出這三個字段的值,作為結果集的一部分直接返回
3、重復步驟2,直到查到第1000條記錄,或者是不滿足city = '杭州’條件時循環結束。
當然,并不是說每個查詢能用上覆蓋索引,就要把語句中涉及的字段都建上聯合索引。因為索引有維護代價。
思考
假設表里面已經有了city_name(city,name)聯合索引。你需要查詢杭州和蘇州兩個城市中所有市民的名字,并且按名字排序,顯示前100條記錄。
select * from t where city in('杭州','蘇州') order by name limit 100;這個語句會有排序。因為條件是蘇州或杭州。如果只有一個條件如只有杭州,那么就不需要排序操作。
如果我們需要實現一個在數據庫端不需要排序的方案,可以這么實現:
把這一條語句拆成兩條語句,流程如下:
1、執行select * from t where city = '杭州' order by name limit 100;
(這個語句不需要排序,客戶端用一個長度為100的內存數組A保存結果)
2、執行select * from where city = '蘇州' order by name limit 100;
(相同的方法,結果被存入內存數組B)
3、對AB兩個有序數組采用歸并排序,得到name最小的前100值,這就是我們需要的結果了。
事務
事務的必要性
mysql中,事務是一個最小的不可分割的工作單元。事務能夠保證一個業務的完整性。
比如我們的銀行轉賬:
如果程序中,只有一條語句執行成功了,而另外一條沒有執行成功,就會出現前后不一致。就會有人白嫖。
因此,在執行多條有關聯 SQL 語句時,事務可能會要求這些 SQL 語句要么同時執行成功,要么就都執行失敗。
也就是說事務具有原子性。
MySQL中如何控制事務
1、mysql是默認開啟事務的(自動提交)
默認事務開啟的作用:
當我們執行一個sql語句時候,效果會立即體現出來,且不能回滾。
回滾舉例
執行插入語句后數據立刻生效,原因是 MySQL 中的事務自動將它提交到了數據庫中。那么所謂回滾的意思就是,撤銷執行過的所有 SQL 語句,使其回滾到最后一次提交數據時的狀態。
在 MySQL 中使用 ROLLBACK 執行回滾:
由于所有執行過的 SQL 語句都已經被提交過了,所以數據并沒有發生回滾。
將自動提交關閉后,可以數據回滾:
-- 關閉自動提交 SET AUTOCOMMIT = 0;-- 查詢自動提交狀態 SELECT @@AUTOCOMMIT; +--------------+ | @@AUTOCOMMIT | +--------------+ | 0 | +--------------+現在我們測試一下:
INSERT INTO user VALUES (2, 'b', 1000);-- 關閉 AUTOCOMMIT 后,數據的變化是在一張虛擬的臨時數據表中展示, -- 發生變化的數據并沒有真正插入到數據表中。 SELECT * FROM user; +----+------+-------+ | id | name | money | +----+------+-------+ | 1 | a | 1000 | | 2 | b | 1000 | +----+------+-------+-- 數據表中的真實數據其實還是: +----+------+-------+ | id | name | money | +----+------+-------+ | 1 | a | 1000 | +----+------+-------+-- 由于數據還沒有真正提交,可以使用回滾 ROLLBACK;-- 再次查詢 SELECT * FROM user; +----+------+-------+ | id | name | money | +----+------+-------+ | 1 | a | 1000 | +----+------+-------+可以使用COMMIT將虛擬的數據真正提交到數據庫中:
INSERT INTO user VALUES (2, 'b', 1000); -- 手動提交數據(持久性), -- 將數據真正提交到數據庫中,執行后不能再回滾提交過的數據。 COMMIT;-- 提交后測試回滾 ROLLBACK;-- 再次查詢(回滾無效了) SELECT * FROM user; +----+------+-------+ | id | name | money | +----+------+-------+ | 1 | a | 1000 | | 2 | b | 1000 | +----+------+-------+總結
1、查看自動提交狀態: select @@AUTOCOMMIT;
2、設置自動提交狀態: set AUTOCOMMIT = 0;
3、手動提交: 在 @@AUTOCOMMIT = 0 時,可以使用commit 命令提交事務
4、事務回滾: 在 @@AUTOCOMMIT = 0 時,可以使用rollback 命令回滾事務
事務給我們提供了一個可以反悔的機會,假設在轉賬時發生了意外,就可以使用 ROLLBACK 回滾到最后一次提交的狀態。假設數據沒有發生意外,這時可以手動將數據COMMIT 到數據表中。
手動開啟事務
可以使用BEGIN 或者 START TRANSACTION 手動開啟一個事務。
-- 使用 BEGIN 或者 START TRANSACTION 手動開啟一個事務 -- START TRANSACTION; BEGIN; UPDATE user set money = money - 100 WHERE name = 'a'; UPDATE user set money = money + 100 WHERE name = 'b';-- 由于手動開啟的事務沒有開啟自動提交, -- 此時發生變化的數據仍然是被保存在一張臨時表中。 SELECT * FROM user; +----+------+-------+ | id | name | money | +----+------+-------+ | 1 | a | 900 | | 2 | b | 1100 | +----+------+-------+-- 測試回滾 ROLLBACK;SELECT * FROM user; +----+------+-------+ | id | name | money | +----+------+-------+ | 1 | a | 1000 | | 2 | b | 1000 | +----+------+-------+當然事務開啟之后,使用commit提交后就不能回滾了。
事務的四大特征
事務的四大特征
A 原子性:事務是最小的單位,不可以分割
C 一致性:事務要求同一事務中的sql語句,必須要保證同時成功或者同時失敗
I 隔離性:事務1 和事務2 之間是具有隔離性的
D 持久性:事務一旦結束(commit or rollback),就不可以返回
事務開啟方式
1、修改默認提交 set autocommit = 0;
2、begin
3、start transaction
事務手動提交與手動回滾
手動提交:commit
手動回滾:rollback
事務的隔離性
事務的隔離性:
1、read uncommitted; 讀未提交的
2、read committed; 讀已經提交的
3、repeatable read; 可以重復讀
4、serializable; 串行化
臟讀現象
在read uncommitted的隔離級別下:
臟讀:一個事務讀到了另外有一個事務沒有提交的數據
實際開發不允許臟讀出現。
如果有兩個事務 a、b
a事務對數據進行操作,在操作的過程中,事務并沒有被提交,但是b可以看見a操作的結果。b看到轉賬到了,然后就不管了。后面a進行rollback操作,錢又回去了,完成白嫖。
不可重復讀現象
在read committed的隔離級別下:
小王一開始開啟了一個事務,然后提交了幾個數據,然后出去抽煙。
在他抽煙的時候,小明在其他電腦上開啟了一個事務,然后對那個表提交了一個數據。
小王煙抽完了,然后統計表中數據,發現不對勁。前后不一致了。
幻讀現象
在repeatable read;的隔離級別下:
事務a和事務b同時操作一張表,事務a提交的數據也不能被事務b讀到,就可以造成幻讀。
可以觀察如下步驟:
小明 在杭州 開啟一個事務;
小王 在北京 開啟一個事務;
小明 對table進行插入數據操作,然后commit;然后查看表,發現操作成功
小王在對table進行插入之前也查看表,然而并沒有小明插入的數據,于是乎他插入了同樣的一條數據,數據庫報錯。
小王很是疑惑,這就是幻讀現象。
串行化
在serializable的隔離級別下:
當user表被事務a操作的時候,事務b里面的寫操作是不可以進行的,會進入排隊狀態(串行化)。
“讀-讀”在串行化隔離級別允許并發。
直到事務a結束之后,事務b的寫入操作才會執行。
串行化的問題是性能特差。
一般來說,隔離級別越高,性能越差。
MySQL默認隔離級別是:repeatable read;
一些補充
使用長事務的弊病
從存儲空間上來說:
長事務意味著系統里面會存在很老的事務視圖。由于這些事務隨時可能訪問數據庫里面的任何數據,所以這個事務提交之前,數據庫里面它可能用到的回滾記錄都必須保留,這就會導致大量占用存儲空間。
長事務還占用鎖資源,也可能拖垮整個庫。
commit work and chain的語法是做什么用的?
提交上一個事務,并且再開啟一個新的事務。它的功能等效于:commit + begin。
怎么查詢各個表中的長事務?
這個表中記錄了所有正在運行的事務信息,里面有事務的開始時間。可以從這里看出哪些事務運行的時間比較長。
select * from information_schema.innodb_trx;如何避免長事務的出現?
從數據庫方面:
a.設置autocommit=1,不要設置為0。 b.寫腳本監控information_schemal.innodb_trx表中數據內容,發現長事務,kill掉它。 c.配置SQL語句所能執行的最大運行時間,如果查過最大運行時間后,中斷這個事務 從**SQL語句**方面: 設置回滾表空單獨存放,便于回收表空間從業務代碼方面:
1、檢查業務邏輯代碼,能拆分為小事務的不要用大事務。
2、檢查代碼,把沒有必要的select語句被事務包裹的情況去掉
事務隔離是怎么通過read-view(讀視圖)實現的?
每一行數有多個版本,當我們要去讀取數據的時候,要判斷這個數據的版本號,對當前事務而言,是否可見,如果不可見,則要根據回滾日志計算得到上一個版本。如果上一個版本也不符合要求,則要找到再上一個版本,
直到找到對應正確的數據版本。
參考
一天學會MySQL
https://time.geekbang.org/column/article/68963
索引
回表
回到主鍵索引樹搜索的過程,我們稱為回表。
覆蓋索引
覆蓋索引就是在這次的查詢中,所要的數據已經在這棵索引樹的葉子結點上了。
select ID from T where k between 3 and 5
ID 的值已經在 k 索引樹上了,因此可以直接提供查詢結果,不需要回表.
由于覆蓋索引可以減少樹的搜索次數,顯著提升查詢性能,所以使用覆蓋索引是一個常用的性能優化手段。
覆蓋索引的第二個使用:在聯合索引上使用,也可以避免回表。
如果現在有一個高頻請求,要根據市民的身份證號查詢他的姓名。我們可以建立一個(身份證號、姓名)的聯合索引。它可以在這個高頻請求上用到覆蓋索引,不再需要回表查整行記錄,減少語句的執行時間。
最左前綴原則
聯合索引先根據第一個字段排序,如果第一個字段有相同的,就按照第二個字段排序。
只要滿足最左前綴,就可以利用索引來加速檢索。這個最左前綴可以是聯合索引的最左 N 個字段,也可以是字符串索引的最左 M 個字符。
聯合索引的時候,如何安排索引內的字段順序?
第一原則:
如果通過調整順序,可以少維護一個索引,那么這個順序往往就是需要優先考慮采用的。
如果我們有個頻繁的要求:根據姓名找到該人身份證,那么應該建立聯合索引:(name,ID)
反之,如果我們有個頻繁的要求:根據該人身份證找到該人姓名,那么應該建立聯合索引:(ID,name)
索引下推
索引覆蓋是你要查的信息在二級索引中已經有了,就不需要回表。索引下推是你的過濾條件有一部分符合了最左前綴,那么會用上索引,如果此時不符合最左前綴的部分剛好有聯合索引中的字段,那么在利用最左前綴進行索引查詢的同時,會根據這些字段多做一步過濾,減少索引查詢出來的條數,這樣就減少了回表次數。
如:
| 1 每次都需要回表 | 2 下推,先濾除不符合的,再回表 |
重建索引問題
假設,我們有一個主鍵列為 ID 的表,表中有字段 k,并且在 k 上有索引。
mysql> create table T( id int primary key, k int not null, name varchar(16), index (k))engine=InnoDB;如果你要重建索引 k:
alter table T drop index k; alter table T add index(k);如果你要重建主鍵索引:
alter table T drop primary key; alter table T add primary key(id);上面這兩個重建索引的作法對此有什么理解?
為什么要重建索引?
索引可能因為刪除,或者頁分裂等原因,導致數據頁有空洞,重建索引的過程會創建一個新的索引,把數據按順序插入,這樣頁面的利用率最高,也就是索引更緊湊、更省空間。
理解
不論是刪除主鍵還是創建主鍵,都會將整個表重建。所以連著執行這兩個語句的話,第一個語句就白做了。
推薦使用:
聯合主鍵索引和 InnoDB 索引組織表問題
有這么一個表:
CREATE TABLE `geek` (`a` int(11) NOT NULL,`b` int(11) NOT NULL,`c` int(11) NOT NULL,`d` int(11) NOT NULL,PRIMARY KEY (`a`,`b`),KEY `c` (`c`),KEY `ca` (`c`,`a`),KEY `cb` (`c`,`b`) ) ENGINE=InnoDB;既然主鍵包含了 a、b 這兩個字段,那意味著單獨在字段 c 上創建一個索引,就已經包含了三個字段了呀,為什么要創建“ca”“cb”這兩個索引?同事告訴他,是因為他們的業務里面有這樣的兩種語句:
select * from geek where c=N order by a limit 1; select * from geek where c=N order by b limit 1;為了這兩個查詢模式,這兩個索引是否都是必須的?為什么呢?
表記錄:
主鍵 a,b 的聚簇索引組織順序相當于 order by a,b ,也就是先按 a 排序,再按 b 排序,c 無序。
| 1 | 2 | 3 | d |
| 1 | 3 | 2 | d |
| 1 | 4 | 3 | d |
| 2 | 1 | 3 | d |
| 2 | 2 | 2 | d |
| 2 | 3 | 4 | d |
索引 ca 的組織是先按 c 排序,再按 a 排序,同時記錄主鍵:
這個跟索引 c 的數據是一模一樣的。
| 2 | 1 | 3 |
| 2 | 2 | 2 |
| 3 | 1 | 2 |
| 3 | 1 | 4 |
| 3 | 2 | 1 |
| 4 | 2 | 3 |
索引 cb 的組織是先按 c 排序,再按 b 排序,同時記錄主鍵:
| 2 | 2 | 2 |
| 2 | 3 | 1 |
| 3 | 1 | 2 |
| 3 | 2 | 1 |
| 3 | 4 | 1 |
| 4 | 3 | 2 |
所以,結論是 ca 可以去掉,cb 需要保留。
in與between的區別
--1. select * from T where k in(1,2,3,4,5) --2. select * from T where k between 1 and 51.in 內部的數字是未知的,不知道是否有序,是否連續等,所以你只能一個一個去看。
2.一個已知的升序、范圍查詢,只需定位第一個值,后面遍歷就行了。
## 全局鎖是什么?全局鎖有什么用?全局鎖怎么用?
全局鎖主要用在邏輯備份過程中,對于InnoDB 引擎的庫,使用–single-transaction;
MySQL 提供了一個加全局讀鎖的方法,命令是 Flush tables with read lock (FTWRL),讓整個庫處于只讀狀態。
表鎖是什么?表鎖有什么用?表鎖怎么用?
表鎖一般是在數據庫引擎不支持行鎖的時候才會被用到的.
表鎖的語法是 lock tables … read/write;
加上讀鎖,不會限制別的線程讀,但會限制別的線程寫。加上寫鎖,會限制別的線程讀寫。
行鎖是什么?行鎖有什么用?行鎖怎么用?
行鎖就是針對數據表中行記錄的鎖。
比如事務 A 更新了一行,而這時候事務 B 也要更新同一行,則必須等事務 A 的操作完成后才能進行更新。
在 InnoDB 事務中,行鎖是在需要的時候才加上的,但并不是不需要了就立刻釋放,而是要等到事務結束時才釋放。這個就是兩階段鎖協議。
事務 B 的 update 語句會被阻塞,直到事務 A 執行 commit 之后,事務 B 才能繼續執行。
一定知道了事務 A 持有的兩個記錄的行鎖,都是在 commit 的時候才釋放的。若行鎖不是在 commit 之后被釋放,而是在該語句執行完就被釋放,則不會出現事務 B 被鎖住。
如果你的事務中需要鎖多個行,要把最可能造成鎖沖突、最可能影響并發度的鎖盡量往后放。
調整語句順序并不能完全避免死鎖。
死鎖與死鎖檢測
并發系統中不同線程出現循環資源依賴,涉及的線程都在等待別的線程釋放資源時,就會導致這幾個線程都進入無限等待的狀態,稱為死鎖。
為了避免這個操作,常用死鎖檢測。
發起死鎖檢測,發現死鎖后,主動回滾死鎖鏈條中的某一個事務,讓其他事務得以繼續執行。將參innodb_deadlock_detect 設置為 on,表示開啟這個邏輯。
死鎖檢測算法復雜度很高 N個進程,遍歷N遍,M個資源,每個資源操作一次。則復雜度 O(M*N^2)。
假設有 1000 個并發線程要同時更新同一行,那么死鎖檢測操作就是 100 萬這個量級的。最終檢測的結果可能是沒有死鎖,但是這期間要消耗大量的 CPU 資源。
何時會死鎖檢測
每條事務執行前都會進行檢測嗎?
并不是,如果他要加鎖訪問的行上有鎖,他才要檢測。
一致性讀不會加鎖,就不需要做死鎖檢測;
并不是每次死鎖檢測都都要掃所有事務。比如某個時刻,事務等待狀態是這樣的:
B在等A,
D在等C,
現在來了一個E,發現E需要等D,那么E就判斷跟D、C是否會形成死鎖,這個檢測不用管B和A。
死鎖檢測其實就是算法,環的檢測,不必每次遍歷一遍當前事務,只需要判斷事務鏈表中,每加入一個新事物后是否有環的生成,有就形成死鎖。這個方法和leetcode的鏈表中的環檢測應該是一個道理。
如何避免高量級的死鎖檢測
為了避免這個問題,一般來說有兩種方法:
1、果你能確保這個業務一定不會出現死鎖,可以臨時把死鎖檢測關掉。一旦發生死鎖現象,則會出現超時(50s)
2、控制并發度:
1、對于相同行的更新,在進入引擎之前排隊。
2、減少行更新鎖沖突的方法:將單行拆成邏輯上的多行
練習
如果你要刪除一個表里面的前 10000 行數據,有以下三種方法可以做到:
第一種,直接執行 delete from T limit 10000;
第二種,在一個連接中循環執行 20 次 delete from T limit 500;
第三種,在 20 個連接中同時執行 delete from T limit 500。
方案一,事務相對較長,則占用鎖的時間較長,會導致其他客戶端等待資源時間較長。
方案二,串行化執行,將相對長的事務分成多次相對短的事務,則每次事務占用鎖的時間相對較短,其他客戶端在等待相應資源的時間也較短。這樣的操作,同時也意味著將資源分片使用(每次執行使用不同片段的資源),可以提高并發性。
方案三,人為自己制造鎖競爭,加劇并發量。
主備一致性
備庫為什么要設置為只讀模式?
有這樣幾點考慮:
1、有時候一些運營類的查詢語句會被放到備庫上去查,設置為只讀可以防止誤操作
2、防止切換邏輯有bug,比如切換過程中出現雙寫( 同時寫兩個庫(A、B )),造成主備不一致
3、可以用 readonly 狀態,來判斷節點的角色
備庫設置為只讀,如何與主庫保持同步更新?
readonly的設置對于super權限用戶是無效的。用于同步的線程,就擁有super權限。
A到B的內部流程如何?
主庫接收到客戶端的更新請求后,執行內部事務的更新邏輯,同時寫binlog;
備庫B與主庫A之間維持一個長連接。主庫內部有一個線程,專門用于服務備庫B這個長連接。
一個事務日志同步的完整過程:
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讀取中轉日志relay log ,解析日志里的命令,并執行
binlog內容是什么?
在解釋內容之前,需要知道binlog的格式。
binlog有三種格式:statement 、row、mixed
statement
binlog_format=statement 時,binlog 里面記錄的就是 SQL 語句的原文
statement格式的binlog的缺陷有個缺陷:
主備使用的索引可能是不一致的,最終導致執行刪除時刪除的數據不一致。
**row **
row 格式的 binlog 里沒有了 SQL 語句的原文,而是替換成了兩個 event: Table_map和Delete_rows.
1、 Table_map , 用于說明操作的表是test庫的表t
2、Delete_rows , 用于定義刪除的行為
當binlog_format = row,binlog里面記錄了真實刪除行的主鍵id,這樣binlog傳到備庫去的時候,肯定不會出現主備刪除不同行的問題
mixed
mixed格式用于哪些場景呢?
statement格式可能會導致主備不一致,所以要使用row格式
row格式比較占空間,同時也更要耗費IO資源,影響執行速度
所以采用這種方案,采用mixed格式,MySQL自己會判斷這條SQL語句是否可能引起主備不一致,如果可能,使用row格式,否則使用statement格式。
row格式對于恢復數據有何好處
現在,越來越多場景要求使用row格式的binlog,可以從delete、insert、update三種sql語句角度看待這個問題。
使用delete語句,row格式會把被刪除的行的整行信息保存。所以刪錯之后,只需要把binlog記錄的delete語句轉成insert就能恢復了。
使用insert語句,row格式會記錄所有的字段信息。所以插入錯誤的時候,只需要把binlog記錄的insert語句轉成delete語句就能恢復了。
使用update語句,binlog會記錄修改前整行的數據和修改后的整行數據。所以如果update誤執行,只需要把event前后的兩行信息對調,再去數據庫執行,就能恢復數據了。
M-M結構的循環復制問題以及解決方案
| M-S結構 | M-M結構 |
圖1是M-S結構,但是現在常用的是M-M結構,M-M結構區別在于:節點A與節點B總是互為主備關系,所以在切換的時候就不用修改主備關系了。
M-M存在循環復制問題:
在節點A更新一個語句,把生成的binlog發給節點B。
節點B執行完更新語句后也會生成binlog。
如果A同時為B的備庫,A會把節點B新生成的binlog拿過去執行。節點A和B之間會不斷循環執行這個更新語句。
解決方案:
已知MySQL在binlog中記錄了命令第一次執行所在實例的server id。
1、規定兩個庫的server id 必須不同。若相同,則不能設定為主備關系
2、備庫接到binlog,生成與原binlog的server id相同的新的binlog
3、每個庫在收到從自己的主庫發過來的日志后,先判斷server id,如果和自己的相同,表示這個日志是自己生成的,丟棄這個日志。
所以使用M-M結構的日志執行流程如下:
1、從節點A更新的事務,binlog里記錄的都是A的server id
2、傳到節點B執行一次后,節點B生成的binlog的server id 也是A的server id
3、再傳給節點A,A判斷這個server id與自己的相同,不處理這個日志
> 如果能夠保證業務代碼不會寫入重復數據,就可以繼續往下看。 如果業務不能保證,那么必須創建唯一索引。
關于查詢能力
普通索引和唯一索引在查詢能力上是沒有很大差別的。
如:select id from T where k=5
1、普通索引查找到滿足條件的第一個記錄(5,500)后需要查找下一個記錄,直到碰到第一個不滿足k=5條件的記錄。
2、對于唯一索引,由于索引定義了唯一性,查找到第一個滿足條件的記錄后,就會停止搜索。
InnoDB的數據按照數據頁來讀寫,每一個數據頁大小默認為16KB.
對于普通索引來說,查找k=5的記錄,該記錄所在的數據頁都在內存里,無非就是多做一次
查找與判斷下一條記錄的操作。
當然,如果剛好k=5這個記錄在數據頁的最后一行,那么就得讀取下一個數據頁,這個會稍微復雜一點。
關于change buffer
需要更新一個數據頁時,如果數據頁在內存中就直接更新。
如果這個數據頁在磁盤中,InnoDB會將這些更新操作緩存在change buffer中,這樣就不需要從磁盤中讀這個數據頁了。
在下次查詢需要訪問這個數據頁的時候,將數據頁讀入內存,然后執行change buffer中的關于這個頁的操作。
change buffer 優點:
將更新操作先記錄到change buffer ,減少讀磁盤,語句執行速度會提升。
數據讀入內存會占用buffer pool,使用change buffer可以避免占用內存,提高內存利用率
change buffer 缺點:
1、唯一索引的更新不能使用change buffer
2、change buffer的主要目的就是將記錄變更動作緩存下來,在一個數據頁merge之前,change buffer上記錄越多,收益越大
如果一個業務的更新模式是寫入后馬上做查詢,這樣不會減少IO訪問,反而增加了change buffer的維護代價。
關于寫能力(基于change buffer)
普通索引在不需要立即讀時候可以很好的應用change buffer,所以大部分場合建議使用普通索引。
如果在更新之后,馬上伴隨這個記錄拆線呢,那么建議關閉change buffer。
redo log 主要節省的是隨機寫磁盤的IO消耗,change buffer 主要節省的則是隨機讀磁盤的IO消耗。
MySQL索引底層原理理解以及常見問題總結
二叉查找樹為索引
二叉樹的key為col2,value為索引所在行的磁盤地址。
但如果拿col1來作為key的話,會發現二叉搜索樹退化成鏈表。
紅黑樹為索引
仍然以col1作為索引key,發現找6只需要查找3次。比二叉查找樹更加合適一點
當表中有1百萬行數據時,這棵樹的高度會越來越大。如果我們查找的元素在葉子節點,查找次數會非常多。
B樹作為索引
可以在樹的橫向上做文章,每個節點原本只存儲一行數據的地址,現在可以修改為存儲多行數據。因為樹的高度越多說明IO操作越多,導致與磁盤的交互越多。
B樹:
葉節點具有相同的深度,葉節點的指針為空。
所有的索引元素不重復
節點中數據索引從左到右遞增排列
B+樹作為索引
B+樹
非葉子節點不存儲data,只存儲索引,這樣可以放更多索引
葉子節點包含所有索引字段。
葉子節點用指針連接,提高區間訪問性能。
也就是說在葉子節點存儲了完整的元素,然后把一些處于中間位置的索引元素提取出來,作為非葉子節點。
MySQL設置默認節點大小為16kb,一個bigint為8byte,一個指針為6byte。所以一個節點最多能存16kb/14b = 1170。
再假設葉子節點一個元素占空間大小為1kb。
如果全部節點存儲了滿了,h = 3的時候一共能夠存儲1170 * 1170 * 16 = 21902400;這樣可以存兩千多萬個數據了。
以下面為例:
注意,整個樹都放在磁盤中,每次load一個節點進入內存。一般來說,先從根節點開始load。
我們現在要找6。比對根節點的3,6大于3,向右比較,發現6大于5,于是從5右邊的指針找到下面一層的節點.
然后把這一層的節點從磁盤里面load到內存中。
我們還可以看到最底層的節點之間會有鏈表相連。
MyISAM存儲引擎索引實現
注意,存儲引擎是用來形容數據庫中的表的。
MyISAM索引文件和數據文件是分離的。
我們使用查詢語句:
首先查找是否是索引字段,如果是就從MYI文件中的B+樹里面去定位到這個元素。key存儲的是索引元素,data存儲的是索引元素所在的那一行的磁盤地址指針。拿到指針后去MYD文件定位。
InnoDB存儲引擎索引實現
索引和數據放到了同一個文件中:.ibd文件。
葉節點包含了完整的數據記錄,而不只是一個地址指針。
常見問題
聚集索引與非聚集索引
InnoDB就是聚集索引,索引和數據文件合在一起。
MyISAM是非聚集索引,索引和數據文件分離。
非聚集索引要查找兩次,一次找到指針地址,一次根據指針地址找具體數據。
聚集索引只需要查找一次,直接找到具體數據,所以效率要更高。
InnoDB基于主鍵索引和普通索引的查詢有什么區別?
如果語句是 select * from T where ID=500,即主鍵查詢方式,則只需要搜索 ID 這棵 B+ 樹;
如果語句是 select * from T where k=5,即普通索引查詢方式,則需要先搜索 k 索引樹,得到 ID 的值為 500,再到 ID 索引樹搜索一次。這個過程稱為回表。
也就是說,基于非主鍵索引的查詢需要多掃描一棵索引樹。因此,我們在應用中應該盡量使用主鍵查詢。
InnoDB主鍵索引為何是整型的自增主鍵
自增主鍵的使用,關于存儲和性能
InnoDB必須要有主鍵,而且推薦使用的是整型的自增主鍵。
因為數字好建立索引,方便比較,而且相比較于字符串類型,占用的空間更小。
關于自增:由于底層葉子節點是遞增排列的,如果此時主鍵是遞增的,那么新插入的元素就顯然在葉子節點的最右邊。
如果主鍵不是遞增的,插入一個新的元素可能就會在葉子節點鏈表中間某處。B+樹的結構調整就十分巨大了,可能上層的非葉子節點的索引值要修改。
例如這里我們插入8
樹的結構發生了很大變化,直接裂開。
自增主鍵的插入數據模式,每次插入一條新記錄,都是追加操作,都不涉及到挪動其他記錄,也不會觸發葉子節點的分裂。
何時使用業務字段作為主鍵呢?
只有唯一的索引,而且該索引為唯一索引。由于沒有其他索引,所以也就不用考慮其他索引的葉子節點大小的問題。
直接將這個索引設置為主鍵,可以避免每次查詢需要搜索兩棵樹。
哈希與B樹
哈希查找某個key很快,但是不支持范圍查找。
B樹用到范圍查找就很方便了。葉子節點從左到右是一個遞增的趨勢。并且葉子節點之間通過指針相連,所以不需要再返回到上層索引中尋找。如果我們要找大于20的元素,那么只要在最底層,20元素的右邊進行遍歷即可。
如果是小于某個元素的情況,就是從底層葉子節點的左邊開始,一直包含到邊界即可。
“N叉樹”的N值在MySQL中是可以被人工調整的么?
1, 通過改變key值來調整
N叉樹中非葉子節點存放的是索引信息,索引包含Key和Point指針。Point指針固定為6個字節,假如Key為10個字節,那么單個索引就是16個字節。如果B+樹中頁大小為16K,那么一個頁就可以存儲1024個索引,此時N就等于1024。我們通過改變Key的大小,就可以改變N的值
2, 改變頁的大小
頁越大,一頁存放的索引就越多,N就越大。
數據頁調整后,如果數據頁太小層數會太深,數據頁太大,加載到內存的時間和單個數據頁查詢時間會提高,需要達到平衡才行。
總結
以上是生活随笔為你收集整理的MySQL面试准备——64页pdf的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MySQL 8.0.22执行器源码分析H
- 下一篇: Mysql的undo、redo、bin