深入理解Mysql-基礎架構

MySQL, the most popular Open Source SQL database management system, is developed, distributed, and supported by Oracle Corporation.

前言

MySQL是最受歡迎的開源SQL數據庫管理系統,由瑞典MySQL AB 公司開發,目前屬於 Oracle 旗下產品。

MySQL 是最流行的關係型數據庫管理系統之一,在 WEB 應用方面,MySQL是最好的 RDBMS (Relational Database Management System,關係數據庫管理系統) 應用軟件之一。

基礎架構

連接器

連接器負責跟客戶端建立連接、獲取權限、維持和管理連接。

連接命令一般是這麼寫的:mysql -h$ip -P$port -u$user -p

如果用戶名密碼認證通過,連接器會到權限表裏面查出你擁有的權限。之後,這個連接裏面的權限判斷邏輯,都將依賴於此時讀到的權限。

如果你沒有後續的動作,這個連接就處於空閒狀態,你可以在show processlist命令中看到它。其中的Command列顯示爲“Sleep”的這一行,就表示現在系統裏面有一個空閒連接。

客戶端如果太長時間沒動靜,連接器就會自動將它斷開。 這個時間是由參數wait_timeout控制的,默認值是8小時。

數據庫裏面,長連接是指連接成功後,如果客戶端持續有請求,則一直使用同一個連接。

短連接則是指每次執行完很少的幾次查詢就斷開連接,下次查詢再重新建立一個。

建立連接的過程通常是比較複雜的,所以要儘量減少建立連接的動作,也就是儘量使用長連接。

但是全部使用長連接後,你可能會發現,有些時候MySQL佔用內存漲得特別快,這是因爲MySQL在執行過程中臨時使用的內存是管理在連接對象裏面的。這些資源會在連接斷開的時候才釋放。

所以如果長連接累積下來,可能導致內存佔用太大,被系統強行殺掉(OOM),從現象看就是MySQL異常重啓了。

怎麼解決這個問題呢?你可以考慮以下兩種方案。

  1. 定期斷開長連接。使用一段時間,或者程序裏面判斷執行過一個佔用內存的大查詢後,斷開連接,之後要查詢再重連。

  2. 如果你用的是MySQL 5.7或更新版本,可以在每次執行一個比較大的操作後,通過執行 mysql_reset_connection來重新初始化連接資源。這個過程不需要重連和重新做權限驗證,但是會將連接恢復到剛剛創建完時的狀態。

查詢緩存

連接建立完成後,你就可以執行select語句了。執行邏輯就會來到第二步:查詢緩存。

MySQL拿到一個查詢請求後,會先到查詢緩存看看,之前是不是執行過這條語句。之前執行過的語句及其結果可能會以key-value對的形式,被直接緩存在內存中。key是查詢的語句,value是查詢的結果。如果你的查詢能夠直接在這個緩存中找到key,那麼這個value就會被直接返回給客戶端。

如果語句不在查詢緩存中,就會繼續後面的執行階段。執行完成後,執行結果會被存入查詢緩存中。你可以看到,如果查詢命中緩存,MySQL不需要執行後面的複雜操作,就可以直接返回結果,這個效率會很高。

但是大多數情況下查詢緩存往往弊大於利。

查詢緩存的失效非常頻繁。只要有對一個表的更新,這個表上所有的查詢緩存都會被清空。因此很可能你費勁地把結果存起來,還沒使用呢,就被一個更新全清空了。對於更新壓力大的數據庫來說,查詢緩存的命中率會非常低。除非你的業務就是有一張靜態表,很長時間纔會更新一次。比如,一個系統配置表,那這張表上的查詢才適合使用查詢緩存。

好在MySQL也提供了這種“按需使用”的方式。你可以將參數query_cache_type設置成DEMAND,這樣對於默認的SQL語句都不使用查詢緩存。而對於你確定要使用查詢緩存的語句,可以用SQL_CACHE顯式指定,像下面這個語句一樣:

mysql> select SQL_CACHE * from T where ID=10;

需要注意的是,MySQL 8.0版本直接將查詢緩存的整塊功能刪掉了,也就是說8.0開始徹底沒有這個功能了。

分析器

如果沒有命中查詢緩存,就要開始真正執行語句了。

首先,MySQL需要知道你要做什麼,因此需要對SQL語句做解析。

分析器先會做“詞法分析”。你輸入的是由多個字符串和空格組成的一條SQL語句,MySQL需要識別出裏面的字符串分別是什麼,代表什麼。

MySQL從你輸入的"select"這個關鍵字識別出來,這是一個查詢語句。它也要把字符串“T”識別成“表名T”,把字符串“ID”識別成“列ID”。

做完了這些識別以後,就要做“語法分析”。根據詞法分析的結果,語法分析器會根據語法規則,判斷你輸入的這個SQL語句是否滿足MySQL語法。

如果你的語句不對,就會收到“You have an error in your SQL syntax”的錯誤提醒,比如下面這個語句select少打了開頭的字母“s”。

mysql> elect * from t where ID=1;

ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'elect * from t where ID=1' at line 1

一般語法錯誤會提示第一個出現錯誤的位置,所以你要關注的是緊接“use near”的內容。

優化器

在開始執行之前,還要先經過優化器的處理。

優化器是在表裏面有多個索引的時候,決定使用哪個索引;或者在一個語句有多表關聯(join)的時候,決定各個表的連接順序。比如你執行下面這樣的語句,這個語句是執行兩個表的join:

mysql> select * from t1 join t2 using(ID) where t1.c=10 and t2.d=20;

既可以先從表t1裏面取出c=10的記錄的ID值,再根據ID值關聯到表t2,再判斷t2裏面d的值是否等於20。

也可以先從表t2裏面取出d=20的記錄的ID值,再根據ID值關聯到t1,再判斷t1裏面c的值是否等於10。

這兩種執行方法的邏輯結果是一樣的,但是執行的效率會有不同,而優化器的作用就是決定選擇使用哪一個方案。

優化器階段完成後,這個語句的執行方案就確定下來了,然後進入執行器階段。

執行器

開始執行的時候,要先判斷一下你對這個表T有沒有執行查詢的權限,如果沒有,就會返回沒有權限的錯誤,如下所示(在工程實現上,如果命中查詢緩存,會在查詢緩存返回結果的時候,做權限驗證。查詢也會在優化器之前調用precheck驗證權限)。

mysql> select * from T where ID=10;

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

如果有權限,就打開表繼續執行。打開表的時候,執行器就會根據表的引擎定義,去使用這個引擎提供的接口。

比如我們這個例子中的表T中,ID字段沒有索引,那麼執行器的執行流程是這樣的:

調用InnoDB引擎接口取這個表的第一行,判斷ID值是不是10,如果不是則跳過,如果是則將這行存在結果集中;

調用引擎接口取“下一行”,重複相同的判斷邏輯,直到取到這個表的最後一行。

執行器將上述遍歷過程中所有滿足條件的行組成的記錄集作爲結果集返回給客戶端。

至此,這個語句就執行完成了。

對於有索引的表,執行的邏輯也差不多。第一次調用的是“取滿足條件的第一行”這個接口,之後循環取“滿足條件的下一行”這個接口,這些接口都是引擎中已經定義好的。

你會在數據庫的慢查詢日誌中看到一個rows_examined的字段,表示這個語句執行過程中掃描了多少行。這個值就是在執行器每次調用引擎獲取數據行的時候累加的。

在有些場景下,執行器調用一次,在引擎內部則掃描了多行,因此引擎掃描行數跟rows_examined並不是完全相同的。

日誌系統

MySQL整體來看,其實就有兩塊:一塊是Server層,它主要做的是MySQL功能層面的事情;還有一塊是引擎層,負責存儲相關的具體事宜。

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

這兩種日誌有以下四點點不同。

  • redo log是InnoDB引擎特有的;binlog是MySQL的Server層實現的,所有引擎都可以使用。

  • redo log是物理日誌,記錄的是“在某個數據頁上做了什麼修改”;binlog是邏輯日誌,記錄的是這個語句的原始邏輯,比如“給ID=2這一行的c字段加1 ”。

  • redo log是循環寫的,空間固定會用完;binlog是可以追加寫入的。“追加寫”是指binlog文件寫到一定大小後會切換到下一個,並不會覆蓋以前的日誌。

  • Binlog有兩種模式,statement 格式的話是記sql語句, row格式會記錄行的內容,記兩條,更新前和更新後都有。

以下面SQL爲例:

mysql> create table T(ID int primary key, c int);

mysql> update T set c=c+1 where ID=2;

redo log

粉板和賬本配合的整個過程,其實就是MySQL裏經常說到的WAL技術,WAL的全稱是Write-Ahead Logging,它的關鍵點就是先寫日誌,再寫磁盤,也就是先寫粉板,等不忙的時候再寫賬本。

具體來說,當有一條記錄需要更新的時候,InnoDB引擎就會先把記錄寫到redo log(粉板)裏面,並更新內存,這個時候更新就算完成了。同時,InnoDB引擎會在適當的時候,將這個操作記錄更新到磁盤裏面,而這個更新往往是在系統比較空閒的時候做,這就像打烊以後掌櫃做的事。

InnoDB的redo log是固定大小的,比如可以配置爲一組4個文件,每個文件的大小是1GB,那麼這塊“粉板”總共就可以記錄4GB的操作。從頭開始寫,寫到末尾就又回到開頭循環寫,如下面這個圖所示。

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

write pos和checkpoint之間的是“粉板”上還空着的部分,可以用來記錄新的操作。如果write pos追上checkpoint,表示“粉板”滿了,這時候不能再執行新的更新,得停下來先擦掉一些記錄,把checkpoint推進一下。

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

要理解crash-safe這個概念,可以想想我們前面賒賬記錄的例子。只要賒賬記錄記在了粉板上或寫在了賬本上,之後即使掌櫃忘記了,比如突然停業幾天,恢復生意後依然可以通過賬本和粉板上的數據明確賒賬賬目。

binlog

我們再來看執行器和InnoDB引擎在執行這個簡單的update語句時的內部流程。

  1. 執行器先找引擎取ID=2這一行。ID是主鍵,引擎直接用樹搜索找到這一行。如果ID=2這一行所在的數據頁本來就在內存中,就直接返回給執行器;否則,需要先從磁盤讀入內存,然後再返回。

  2. 執行器拿到引擎給的行數據,把這個值加上1,比如原來是N,現在就是N+1,得到新的一行數據,再調用引擎接口寫入這行新數據。

  3. 引擎將這行新數據更新到內存中,同時將這個更新操作記錄到redo log裏面,此時redo log處於prepare狀態。然後告知執行器執行完成了,隨時可以提交事務。

  4. 執行器生成這個操作的binlog,並把binlog寫入磁盤。

  5. 執行器調用引擎的提交事務接口,引擎把剛剛寫入的redo log改成提交(commit)狀態,更新完成。

這裏我給出這個update語句的執行流程圖,圖中淺色框表示是在InnoDB內部執行的,深色框表示是在執行器中執行的。

兩階段提交

binlog會記錄所有的邏輯操作,並且是採用“追加寫”的形式。

當需要恢復到指定的某一秒時,比如某天下午兩點發現中午十二點有一次誤刪表,需要找回數據,那你可以這麼做:

  1. 首先,找到最近的一次全量備份,如果你運氣好,可能就是昨天晚上的一個備份,從這個備份恢復到臨時庫;
  2. 然後,從備份的時間點開始,將備份的binlog依次取出來,重放到中午誤刪表之前的那個時刻。
  3. 這樣你的臨時庫就跟誤刪之前的線上庫一樣了,然後你可以把表數據從臨時庫取出來,按需要恢復到線上庫去。

爲什麼日誌需要“兩階段提交”。這裏不妨用反證法來進行解釋。

由於redo log和binlog是兩個獨立的邏輯,如果不用兩階段提交,要麼就是先寫完redo log再寫binlog,或者採用反過來的順序。我們看看這兩種方式會有什麼問題。

假設當前ID=2的行,字段c的值是0,再假設執行update語句過程中在寫完第一個日誌後,第二個日誌還沒有寫完期間發生了crash,會出現什麼情況呢?

  1. 先寫redo log後寫binlog。假設在redo log寫完,binlog還沒有寫完的時候,MySQL進程異常重啓。由於我們前面說過的,redo log寫完之後,系統即使崩潰,仍然能夠把數據恢復回來,所以恢復後這一行c的值是1。但是由於binlog沒寫完就crash了,這時候binlog裏面就沒有記錄這個語句。因此,之後備份日誌的時候,存起來的binlog裏面就沒有這條語句。然後你會發現,如果需要用這個binlog來恢復臨時庫的話,由於這個語句的binlog丟失,這個臨時庫就會少了這一次更新,恢復出來的這一行c的值就是0,與原庫的值不同。

  2. 先寫binlog後寫redo log。如果在binlog寫完之後crash,由於redo log還沒寫,崩潰恢復以後這個事務無效,所以這一行c的值是0。但是binlog裏面已經記錄了“把c從0改成1”這個日誌。所以,在之後用binlog來恢復的時候就多了一個事務出來,恢復出來的這一行c的值就是1,與原庫的值不同。

可以看到,如果不使用“兩階段提交”,那麼數據庫的狀態就有可能和用它的日誌恢復出來的庫的狀態不一致。

當你需要擴容的時候,也就是需要再多搭建一些備庫來增加系統的讀能力的時候,現在常見的做法也是用全量備份加上應用binlog來實現的,這個“不一致”就會導致你的線上出現主從數據庫不一致的情況。

簡單說,redo log和binlog都可以用於表示事務的提交狀態,而兩階段提交就是讓這兩個狀態保持邏輯上的一致。

事務隔離

簡單來說,事務就是要保證一組數據庫操作,要麼全部成功,要麼全部失敗。

在MySQL中,事務支持是在引擎層實現的。MySQL是一個支持多引擎的系統,但並不是所有的引擎都支持事務。比如MySQL原生的MyISAM引擎就不支持事務。

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

隔離得越嚴實,效率就會越低。因此很多時候,我們都要在二者之間尋找一個平衡點。 SQL標準的事務隔離級別包括:讀未提交(read uncommitted)、讀提交(read committed)、可重複讀(repeatable read)和串行化(serializable )。下面我逐一爲你解釋:

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

事務隔離的實現:每條記錄在更新的時候都會同時記錄一條回滾操作。同一條記錄在系統中可以存在多個版本,這就是數據庫的多版本併發控制(MVCC)。

事務啓動方式:一、顯式啓動事務語句,begin或者start transaction,提交commit,回滾rollback;二、set autocommit=0,該命令會把這個線程的自動提交關掉。這樣只要執行一個select語句,事務就啓動,並不會自動提交,直到主動執行commit或rollback或斷開連接。

全局鎖、表鎖、行鎖

根據加鎖的範圍,MySQL裏面的鎖大致可以分成全局鎖、表級鎖和行鎖三類。

全局鎖

對整個數據庫實例加鎖。MySQL提供加全局讀鎖的方法:Flush tables with read lock(FTWRL)。

這個命令可以使整個庫處於只讀狀態。使用該命令之後,數據更新語句、數據定義語句和更新類事務的提交語句等操作都會被阻塞。

  • 使用場景:全庫邏輯備份。
  • 風險:
    1. 如果在主庫備份,在備份期間不能更新,業務停擺
    2. 如果在從庫備份,備份期間不能執行主庫同步的binlog,導致主從延遲。

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

一致性讀是好,但是前提是引擎要支持這個隔離級別。

如果要全庫只讀,爲什麼不使用set global readonly=true的方式?

  1. 在有些系統中,readonly的值會被用來做其他邏輯,比如判斷主備庫。所以修改global變量的方式影響太大。
  2. 在異常處理機制上有差異。如果執行FTWRL命令之後由於客戶端發生異常斷開,那麼MySQL會自動釋放這個全局鎖,整個庫回到可以正常更新的狀態。而將整個庫設置爲readonly之後,如果客戶端發生異常,則數據庫就會一直保持readonly狀態,這樣會導致整個庫長時間處於不可寫狀態,風險較高。

表級鎖

MySQL裏面表級鎖有兩種,一種是表鎖,一種是元數據鎖(meta data lock,MDL) 表鎖的語法是:lock tables ... read/write。

可以用unlock tables主動釋放鎖,也可以在客戶端斷開的時候自動釋放。lock tables語法除了會限制別的線程的讀寫外,也限定了本線程接下來的操作對象。

對於InnoDB這種支持行鎖的引擎,一般不使用lock tables命令來控制併發,畢竟鎖住整個表的影響面還是太大。 MDL:不需要顯式使用,在訪問一個表的時候會被自動加上。 MDL的作用:保證讀寫的正確性。

在對一個表做增刪改查操作的時候,加MDL讀鎖;當要對錶做結構變更操作的時候,加MDL寫鎖。

讀鎖之間不互斥。讀寫鎖之間,寫鎖之間是互斥的,用來保證變更表結構操作的安全性。

MDL 會直到事務提交纔會釋放,在做表結構變更的時候,一定要小心不要導致鎖住線上查詢和更新。

行鎖

行鎖就是針對數據表中行記錄的鎖。

MySQL 的行鎖是在引擎層由各個引擎自己實現的。

  • MyISAM只支持表級鎖
  • Innodb支持全局鎖、表級鎖、行鎖

兩階段鎖

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

如果你的事務中需要鎖多個行,要把最可能造成鎖衝突、最可能影響併發度的鎖儘量往後放,最大程度地減少事務之間的鎖等待,提升併發度。

死鎖和死鎖檢索

當併發系統中不同線程出現循環資源依賴,涉及的線程都在等待別的線程釋放資源時,就會導致這幾個線程都進入無限等待的狀態,稱爲死鎖。

當出現死鎖以後,有兩種策略:

  • 一種策略是,直接進入等待,直到超時。這個超時時間可以通過參數 innodb_lock_wait_timeout 來設置,默認是50s。
  • 另一種策略是,發起死鎖檢測,發現死鎖後,主動回滾死鎖鏈條中的某一個事務,讓其他事務得以繼續執行。將參數 innodb_deadlock_detect 設置爲 on,表示開啓這個邏輯。

參考

https://dev.mysql.com/doc/refman/5.7/en/manual-info.html

說明:本文根據極客時間《MySQL實戰45講》整理而成。

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