阿龙的学习笔记---MySQL45讲的总结(一)
學(xué)習(xí)這個(gè)大佬的課,的確超級(jí)有經(jīng)驗(yàn):https://time.geekbang.org/column/intro/100020801
查詢(xún)語(yǔ)句過(guò)程
-
連接器,分析器,優(yōu)化器,執(zhí)行器,最后到存儲(chǔ)引擎。
-
MySQL 可以分為 Server 層和存儲(chǔ)引擎層兩部分。
- Server 層包括連接器、查詢(xún)緩存、分析器、優(yōu)化器、執(zhí)行器等,涵蓋 MySQL 的大多數(shù)核心服務(wù)功能。
- 存儲(chǔ)引擎層負(fù)責(zé)數(shù)據(jù)的存儲(chǔ)和提取。
-
連接器:
- 第一步,你會(huì)先連接到這個(gè)數(shù)據(jù)庫(kù)上,這時(shí)候接待你的就是連接器。連接器負(fù)責(zé)跟客戶(hù)端建立連接、獲取權(quán)限、維持和管理連接。
-
查詢(xún)緩存:
- 連接完成后,執(zhí)行 select 語(yǔ)句了。會(huì)先查詢(xún)緩存。之前執(zhí)行過(guò)的語(yǔ)句及其結(jié)果可能會(huì)以 key-value 對(duì)的形式,被直接緩存在內(nèi)存中。key 是查詢(xún)的語(yǔ)句,value 是查詢(xún)的結(jié)果。
- 但是,只要有對(duì)一個(gè)表的更新,這個(gè)表上所有的查詢(xún)緩存都會(huì)被清空。所以靜態(tài)表比較適合,更新較多的表不適合,反而影響效率。
-
分析器:
- 分析器先會(huì)做“詞法分析”。再做“語(yǔ)法分析”。根據(jù)詞法分析的結(jié)果,語(yǔ)法分析器會(huì)根據(jù)語(yǔ)法規(guī)則,判斷你輸入的這個(gè) SQL 語(yǔ)句是否滿(mǎn)足 MySQL 語(yǔ)法。
-
優(yōu)化器
- 經(jīng)過(guò)了分析器,MySQL 就知道你要做什么了。在開(kāi)始執(zhí)行之前,還要先經(jīng)過(guò)優(yōu)化器的處理。優(yōu)化器是在表里面有多個(gè)索引的時(shí)候,決定使用哪個(gè)索引;或者在一個(gè)語(yǔ)句有多表關(guān)聯(lián)(join)的時(shí)候,決定各個(gè)表的連接順序。
-
執(zhí)行器:
- 優(yōu)化器知道了該怎么做,于是就進(jìn)入了執(zhí)行器階段,開(kāi)始執(zhí)行語(yǔ)句。如果有權(quán)限,就打開(kāi)表繼續(xù)執(zhí)行。打開(kāi)表的時(shí)候,執(zhí)行器就會(huì)根據(jù)表的引擎定義,去使用這個(gè)引擎提供的接口。
更新語(yǔ)句過(guò)程
-
依然走上面的路:
- 先連接
- 查詢(xún)緩存會(huì)清空。
- 分析器分析出是一條更新語(yǔ)句。
- 優(yōu)化器進(jìn)行索引等優(yōu)化。
- 執(zhí)行器執(zhí)行。但是除了更新表中數(shù)據(jù)外,還有redolog和binlog兩個(gè)日志文件的更新。即物理日志 redo log 和邏輯日志 binlog。
-
redo log: InnoDB的物理日志模塊:
- 是為了解決每次都得更新到數(shù)據(jù)庫(kù)而導(dǎo)致效率低下的問(wèn)題。
- 這個(gè)先日志的技術(shù)叫:WAL,全稱(chēng)是 Write-Ahead Logging。具體來(lái)說(shuō),當(dāng)有一條記錄需要更新的時(shí)候,InnoDB 引擎就會(huì)先把記錄寫(xiě)到 redo log(粉板)里面,并更新內(nèi)存,這個(gè)時(shí)候更新就算完成了。同時(shí),InnoDB 引擎會(huì)在適當(dāng)?shù)臅r(shí)候,將這個(gè)操作記錄更新到磁盤(pán)里面,而這個(gè)更新往往是在系統(tǒng)比較空閑的時(shí)候做,這就像打烊以后掌柜做的事。
- log存儲(chǔ)空間是一個(gè)環(huán)形結(jié)構(gòu),大小是固定的,如果滿(mǎn)了則需要寫(xiě)回?cái)?shù)據(jù)庫(kù)中一些。
- log還解決了數(shù)據(jù)庫(kù)如果發(fā)生異常宕機(jī)的問(wèn)題,重啟后可以恢復(fù)。要將持久化至磁盤(pán)的參數(shù)設(shè)置為每次事務(wù)都持久化到磁盤(pán)。
-
bin log: 系統(tǒng)server層的歸檔日志模塊:
- 前面我們講過(guò),MySQL 整體來(lái)看,一塊是 Server 層,它主要做的是 MySQL 功能層面的事情;一塊是引擎層,負(fù)責(zé)存儲(chǔ)相關(guān)的具體事宜。redo log 是 InnoDB 引擎特有的日志,而 Server 層也有自己的日志,稱(chēng)為 binlog(歸檔日志),所有引擎都能用。
- 除了這個(gè)不同之外,redo log 是物理日志,記錄的是“在某個(gè)數(shù)據(jù)頁(yè)上做了什么修改”;binlog 是邏輯日志,記錄的是這個(gè)語(yǔ)句的原始邏輯,比如“給 ID=2 這一行的 c 字段加 1 ”。redo log 是環(huán)形循環(huán)寫(xiě),binlog 是可以追加寫(xiě)到一定大小后會(huì)切換到下一個(gè)。
-
具體執(zhí)行流程:
- 先取到這一行的數(shù)據(jù),再調(diào)用引擎接口寫(xiě)入這行新數(shù)據(jù)。
- 引擎將這行新數(shù)據(jù)更新到內(nèi)存中,同時(shí)將這個(gè)更新操作記錄到 redo log 里面,變成 prepare。
- 執(zhí)行器生成這個(gè)操作的 binlog,并把 binlog 寫(xiě)入磁盤(pán)。
- 執(zhí)行器提交事務(wù),rodo log 變成 commit。
-
rodelog 的兩階段:prepare 和 commit:
- 為了 bin 和 redo 的一致性。
事務(wù)隔離:
-
針對(duì)ACID中的隔離性,問(wèn)題針對(duì)多個(gè)事務(wù)同時(shí)進(jìn)行產(chǎn)生的臟讀/不可重復(fù)讀/幻讀,定義了四個(gè)隔離級(jí)別以解決這些問(wèn)題。
未提交是指,一個(gè)事務(wù)還沒(méi)提交時(shí),它做的變更就能被別的事務(wù)看到。 讀提交是指,一個(gè)事務(wù)提交之后,它做的變更才會(huì)被其他事務(wù)看到。 可重復(fù)讀是指,事務(wù)在執(zhí)行期間看到的數(shù)據(jù)前后必須是一致的。 串行化,顧名思義是對(duì)于同一行記錄,所有操作是串行的(是這樣嗎?),“寫(xiě)”會(huì)加“寫(xiě)鎖”,“讀”會(huì)加“讀鎖”。當(dāng)出現(xiàn)讀寫(xiě)鎖沖突的時(shí)候,后訪(fǎng)問(wèn)的事務(wù)必須等前一個(gè)事務(wù)執(zhí)行完成,才能繼續(xù)執(zhí)行。 -
作者簡(jiǎn)單的闡述了一下更直觀(guān)的表述:
“讀未提交”隔離級(jí)別下直接返回記錄上的最新值,沒(méi)有視圖概念; “讀提交”隔離級(jí)別下,這個(gè)視圖是在每個(gè) SQL 語(yǔ)句開(kāi)始執(zhí)行的時(shí)候創(chuàng)建的; 可重復(fù)讀”隔離級(jí)別下,這個(gè)視圖是在事務(wù)啟動(dòng)時(shí)創(chuàng)建的,整個(gè)事務(wù)存在期間都用這個(gè)視圖。 而“串行化”隔離級(jí)別下直接用加鎖的方式來(lái)避免并行訪(fǎng)問(wèn)。 -
詳細(xì)描述”可重復(fù)讀“的實(shí)現(xiàn):
- 在 MySQL 中,實(shí)際上每條記錄在更新的時(shí)候都會(huì)同時(shí)記錄一條回滾(undo)操作。記錄上的最新值,通過(guò)回滾操作,都可以得到前一個(gè)狀態(tài)的值。
- 不同時(shí)刻啟動(dòng)的事務(wù)會(huì)有不同的 read-view,,同一條記錄在系統(tǒng)中可以存在多個(gè)版本,就是數(shù)據(jù)庫(kù)的多版本并發(fā)控制(MVCC)。
- 回滾日志(undo log)的刪除,在最早的視圖(read-view)之前的會(huì)被刪除。回滾日志記錄的數(shù)據(jù)量比較大,這也是為什么不應(yīng)該使用長(zhǎng)事務(wù)的原因之一,會(huì)導(dǎo)致回滾日志的文件很大。
-
事務(wù)的啟動(dòng):實(shí)際操作注意點(diǎn),誤操作導(dǎo)致長(zhǎng)事務(wù)
- autocommit 選項(xiàng)為1,則是設(shè)置在每個(gè)語(yǔ)句之后自動(dòng)commit(我理解為每個(gè)語(yǔ)句是一個(gè)事務(wù)),但如果一些情況下默認(rèn)設(shè)置為0,那么執(zhí)行一個(gè)語(yǔ)句之后,就開(kāi)啟了事務(wù),但是沒(méi)有提交,這樣會(huì)導(dǎo)致你可能并不想開(kāi)啟事務(wù),或者一直沒(méi)有commit而導(dǎo)致長(zhǎng)事務(wù)。
- 所以作者建議,如果要打開(kāi)事務(wù),每次手動(dòng)開(kāi)啟。即set autocommit=1, 通過(guò)顯式語(yǔ)句的方式來(lái)啟動(dòng)事務(wù)。
索引詳解:
- 索引的出現(xiàn)是為了提高查詢(xún)效率,但是實(shí)現(xiàn)索引的方式卻有很多種:
- 哈希表:查詢(xún)很快,但是無(wú)序,區(qū)間操作時(shí)不好使。所以,哈希表這種結(jié)構(gòu)適用于只有等值查詢(xún)的場(chǎng)景,比如 Memcached 及其他一些 NoSQL 引擎。
- 有序數(shù)組:等值查詢(xún)或是區(qū)間查詢(xún),都能二分,效率很高。但是插入/刪除操作效率極低。
- 多叉搜索樹(shù):優(yōu)點(diǎn)快,并且插入也快,需要做平衡, B+樹(shù)是InnoDB。
- 跳表、LSM 樹(shù)等數(shù)據(jù)結(jié)構(gòu)也被用于引擎設(shè)計(jì)中。(自己看)
- 經(jīng)驗(yàn):數(shù)據(jù)庫(kù)底層存儲(chǔ)的核心就是基于這些數(shù)據(jù)模型的。每碰到一個(gè)新數(shù)據(jù)庫(kù),我們需要先關(guān)注它的數(shù)據(jù)模型,這樣才能從理論上分析出這個(gè)數(shù)據(jù)庫(kù)的適用場(chǎng)景。
- InnoDB 的索引:
- B+樹(shù)。
- 索引類(lèi)型分為 主鍵索引 和 非主鍵索引。主鍵是聚簇索引;非主鍵索引中存儲(chǔ)的是主鍵的值,也叫二級(jí)索引,需要查詢(xún)兩次。
- 索引維護(hù):
- 自增的主鍵比較利于維護(hù),不會(huì)產(chǎn)生葉子節(jié)點(diǎn)的分裂或合并。
- 優(yōu)化索引:
- 覆蓋索引技巧:
- 比如當(dāng)我們查詢(xún)非主鍵索引的時(shí)候,取某個(gè)字段的值,則需要二次回表再去主鍵索引中找一遍,這樣就要查找兩個(gè)B+樹(shù)。這時(shí)覆蓋索引這個(gè)技巧就用得上了。
- 個(gè)人認(rèn)為這里說(shuō)的覆蓋索引不是一種類(lèi)型,只是一種技巧,是利用聯(lián)合索引實(shí)現(xiàn)的。比如從非主鍵查找主鍵的值,那么只需要一次。那么比如建立<身份證,姓名>的聯(lián)合索引,那么如果有很多請(qǐng)求都是通過(guò)身份證查找名字,那么在這個(gè)聯(lián)合索引中就能遍歷一次找到。
- 最左前綴原則:
- 兩個(gè)層面,一是字符串可以最左前綴匹配,因?yàn)榕判蚓褪前凑者@個(gè)比較方法排序的。
- 在聯(lián)合索引中,也是一個(gè)技巧和特性。聯(lián)合索引也支持最左前綴的匹配。比如還是<身份證,姓名>這個(gè)聯(lián)合索引,假如創(chuàng)建了這個(gè)聯(lián)合索引,那么<身份證>這個(gè)單獨(dú)的索引就可以不用建立了,因?yàn)槁?lián)合索引存儲(chǔ)時(shí),按照順序建立索引,身份證在前,前面匹配到即可,所以按身份證查詢(xún)時(shí),也能用到索引的優(yōu)勢(shì)。
- 索引下推優(yōu)化:
- MySQL5.6之后推出的。
- 比如建立了(name, age)的聯(lián)合索引。語(yǔ)句是:select * from tuser where name like '張%' and age=10 and ismale=1;
- 那么沒(méi)有索引下推的話(huà),應(yīng)該在(name,age)中查詢(xún)到前綴是”張“的人,然后依次回表,查詢(xún)是否滿(mǎn)足條件。
- 如果有的話(huà),那么命中了name=”張%“之后,還會(huì)再判斷 age 是否滿(mǎn)足。滿(mǎn)足了之后,才會(huì)回表,這樣回表次數(shù)更少。
- 總結(jié): 在滿(mǎn)足語(yǔ)句需求的情況下, 盡量少地訪(fǎng)問(wèn)資源是數(shù)據(jù)庫(kù)設(shè)計(jì)的重要原則之一。我們?cè)谑褂脭?shù)據(jù)庫(kù)的時(shí)候,尤其是在設(shè)計(jì)表結(jié)構(gòu)時(shí),也要以減少資源消耗作為目標(biāo)。
- 覆蓋索引技巧:
并發(fā)控制——全局鎖 & 表鎖
-
全局鎖:
- 將整個(gè)數(shù)據(jù)庫(kù)鎖住,處于只讀狀態(tài),其他更新/定于語(yǔ)句或事務(wù)都會(huì)阻塞。
- 典型使用場(chǎng)景是在全庫(kù)邏輯備份中。如果不加鎖,則可能導(dǎo)致數(shù)據(jù)不一致。
- 能拿到一致性視圖的不用全局鎖的方式:在InnoDB中,可重復(fù)讀的隔離級(jí)別,會(huì)在事務(wù)開(kāi)始時(shí)創(chuàng)建一致性視圖,那么這時(shí)備份則不用全局鎖。
- 但是只有InnoDB有啊,比如MyISAM就得用全局鎖。所以并不是沒(méi)用。
-
表級(jí)鎖:
- 表級(jí)鎖是鎖住整個(gè)表,而不是整個(gè)數(shù)據(jù)庫(kù)。InnoDB中很少用到表級(jí)鎖。
- 另一種表級(jí)鎖是元數(shù)據(jù)鎖MDL(metadata lock)。這個(gè)鎖主要是針對(duì)表結(jié)構(gòu)的。在訪(fǎng)問(wèn)某個(gè)表時(shí),這個(gè)表的結(jié)構(gòu)不應(yīng)該發(fā)生變化,會(huì)加讀鎖。在修改這個(gè)表結(jié)構(gòu)時(shí),加寫(xiě)鎖。上述是默認(rèn)自動(dòng)加鎖的。
- 不過(guò)這個(gè)鎖又可能會(huì)造成問(wèn)題,比如長(zhǎng)事務(wù)一直沒(méi)有釋放MDL讀鎖,你要改變結(jié)構(gòu)會(huì)加MDL寫(xiě)鎖,然后會(huì)等待之前的讀鎖事務(wù)釋放。那么之后的所有讀操作都不能進(jìn)行了。
- 解決方法:首先對(duì)于長(zhǎng)事務(wù)的處理,之前提過(guò)。再者更改表結(jié)構(gòu)的語(yǔ)句可以設(shè)置一個(gè)等待時(shí)間,超時(shí)則放棄重試。
并發(fā)控制——行鎖
- MySQL 的行鎖是在引擎層由各個(gè)引擎自己實(shí)現(xiàn)的。
- 在 InnoDB 事務(wù)中,行鎖是在需要的時(shí)候才加上的,但并不是不需要了就立刻釋放,而是要等到事務(wù)結(jié)束時(shí)才釋放。這個(gè)就是兩階段鎖協(xié)議。
- 所以經(jīng)驗(yàn)是要把最可能造成鎖沖突、最可能影響并發(fā)度的鎖盡量往后放。
- 死鎖和死鎖檢測(cè):
- 兩種策略:
- 一種是超時(shí)等待,但時(shí)間太長(zhǎng)影響性能,時(shí)間太短會(huì)誤操作;
- 另一種是死鎖檢測(cè)。是有額外負(fù)擔(dān)的,時(shí)間復(fù)雜度O(n),會(huì)導(dǎo)致性能下降。
- 解決熱點(diǎn)行更新的性能問(wèn)題:
- 去掉死鎖檢測(cè),不太靠譜,除非保證沒(méi)有死鎖。
- 控制并發(fā)度。如果同時(shí)只有10個(gè)線(xiàn)程更新熱點(diǎn)行,那么死鎖檢測(cè)也很快,其他的排隊(duì)。牛逼的專(zhuān)家可以修改MySQL源碼,或者使用中間件排隊(duì)。再或者將熱點(diǎn)行拆分成多行,這個(gè)需要考慮業(yè)務(wù)具體邏輯。
- 兩種策略:
事務(wù)隔離?查詢(xún)與更新的差異
-
前面提到可重復(fù)讀隔離級(jí)別,事務(wù) T 啟動(dòng)的時(shí)候會(huì)創(chuàng)建一個(gè)視圖 read-view,之后事務(wù) T 執(zhí)行期間,即使有其他事務(wù)修改了數(shù)據(jù),事務(wù) T 看到的仍然跟在啟動(dòng)時(shí)看到的一樣。
-
但是在更新的時(shí)候,情況就復(fù)制了起來(lái),這時(shí)要加行鎖,等到鎖了之后,修改的是視圖中的值,還是最新的值呢??
-
比如如下操作:表中有id=1,k=1這個(gè)數(shù)據(jù)。
注意: begin/start transaction 命令并不是一個(gè)事務(wù)的起點(diǎn),在執(zhí)行到它們之后的第一個(gè)操作 InnoDB 表的語(yǔ)句,事務(wù)才真正啟動(dòng)。如果你想要馬上啟動(dòng)一個(gè)事務(wù),可以使用 start transaction with consistent snapshot 這個(gè)命令。第一種啟動(dòng)方式,一致性視圖是在執(zhí)行第一個(gè)快照讀語(yǔ)句時(shí)創(chuàng)建的;第二種啟動(dòng)方式,一致性視圖是在執(zhí)行 start transaction with consistent snapshot 時(shí)創(chuàng)建的。
- 結(jié)論:事務(wù)A讀取到的是1,事務(wù)B select 到的是最新的3。先看一下MVCC具體實(shí)現(xiàn),在分析查詢(xún)與更新。
-
MVCC
- InnoDB 里面每個(gè)事務(wù)有一個(gè)唯一的事務(wù) ID,叫作 transaction id,事務(wù)開(kāi)始時(shí)申請(qǐng),嚴(yán)格遞增。
- 每一行也有多個(gè)版本,按順序,每個(gè)版本會(huì)記錄更新的事務(wù)的trx_id。多個(gè)版本也不是物理存儲(chǔ)多個(gè)版本,而是通過(guò)**回滾日志(undo_log)**進(jìn)行回滾。否則每次創(chuàng)建快照都要將數(shù)據(jù)版本全取出來(lái)就太復(fù)雜了。
- 可重復(fù)讀的定義,一個(gè)事務(wù)啟動(dòng)的時(shí)候,能夠看到所有已經(jīng)提交的事務(wù)結(jié)果。
- 具體實(shí)現(xiàn):
-
在實(shí)現(xiàn)上, InnoDB 為每個(gè)事務(wù)構(gòu)造了一個(gè)啟動(dòng)時(shí)活躍事務(wù)數(shù)組,用來(lái)保存這個(gè)事務(wù)啟動(dòng)瞬間,當(dāng)前正在“活躍”的所有事務(wù) ID。“活躍”指的就是,啟動(dòng)了但還沒(méi)提交。數(shù)組里面事務(wù) ID 的最小值記為低水位,當(dāng)前系統(tǒng)里面已經(jīng)創(chuàng)建過(guò)的事務(wù) ID 的最大值加 1 記為高水位。
-
這樣,對(duì)于當(dāng)前事務(wù)的啟動(dòng)瞬間來(lái)說(shuō),一個(gè)數(shù)據(jù)版本的 row trx_id,有以下幾種可能:
- 如果落在綠色部分,表示這個(gè)版本是已提交的事務(wù)或者是當(dāng)前事務(wù)自己生成的,這個(gè)數(shù)據(jù)是可見(jiàn)的;
- 如果落在紅色部分,表示這個(gè)版本是由將來(lái)啟動(dòng)的事務(wù)生成的,是肯定不可見(jiàn)的;
- 如果落在黃色部分,那就包括兩種情況
- 若 row trx_id 在數(shù)組中,表示這個(gè)版本是由還沒(méi)提交的事務(wù)生成的,不可見(jiàn);
- 若 row trx_id 不在數(shù)組中,表示這個(gè)版本是已經(jīng)提交了的事務(wù)生成的,可見(jiàn)。
-
-
利用這個(gè)特性,開(kāi)始事務(wù)創(chuàng)建快照的時(shí)候只需要?jiǎng)?chuàng)建這個(gè)數(shù)組即可。
查詢(xún)與更新的差異:
- 普通的查詢(xún)我們可以知道,在開(kāi)始事務(wù)時(shí)創(chuàng)建的視圖,查詢(xún)的版本肯定按照上述的規(guī)則,所以在上面的例子中,不會(huì)查詢(xún)到B和C的更改。
- 而更新就不太一樣了,只看下圖的B和C。B事務(wù)先開(kāi)始,C事務(wù)后開(kāi)始然后提交。所以按理說(shuō)B是看不到C的更新這個(gè)操作的。**但是B需要更新操作,更新數(shù)據(jù)都是先讀后寫(xiě)的,而這個(gè)讀,只能讀當(dāng)前的值,稱(chēng)為“當(dāng)前讀”(current read)。**這時(shí) k 讀出是 2, 加 1 是 3。而自己的更新時(shí)能被本事務(wù)看到的,所以之后select k則結(jié)果是3。
總結(jié)
以上是生活随笔為你收集整理的阿龙的学习笔记---MySQL45讲的总结(一)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 申宝策略-行业与概念板块跌多涨少
- 下一篇: R语言-股票数据库(4)-股票行业和概念