1. 程式人生 > 遊戲攻略 >《幻塔》人工島風景點打卡位置一覽

《幻塔》人工島風景點打卡位置一覽

一、基礎架構

  • 一條 SQL 語句是如何執行的

  • MySQL分為Server層和儲存引擎層兩部分

1. Server層

Server層包括 聯結器、查詢快取、分析器、優化器、執行器 五大部分,以及包含了所有的內建函式、跨儲存引擎的功能(儲存過程、觸發器、檢視)

(1) 聯結器

聯結器負責跟客戶端建立連線、獲取許可權、維持和管理連線

首先我們先連結上資料庫,使用如下命令:

mysql -h$ip -P$port -u$user -p
  1. 如果賬號或者密碼錯誤,則提示 Access denied for user 錯誤,然後客戶端程式結束執行
  2. 如果密碼認證通過,聯結器會查詢當前使用者的所有許可權,之後這個連線裡面的許可權判斷邏輯都依賴此時讀取到的許可權
    ,這意味著使用者連線成功後再對這個使用者進行許可權修改,此時不會生效,只有新建新的連線才會使用新的許可權配置

連結成功後,如果沒有其他操作,連線就是處於空閒狀態,如果超過一定時間,就會自動斷開連結,該引數由 wait_timeout 控制,預設是 8小時

在資料庫裡面,長連線是指連線成功後,如果客戶端持續有請求,則一直使用同一個連線;短連線是指每次執行完很少的幾次查詢就斷開連線,下次查詢再重新建立一個

建立連線的過程通常是比較複雜的,因此儘量使用長連線,但是全部使用長連線後,你可能會發現,有些時候 MySQL 佔用記憶體漲得特別快,這是因為 MySQL 在執行過程中臨時使用的記憶體是管理在連線物件裡面的。這些資源會在連線斷開的時候才釋放。所以如果長連線累積下來,可能導致記憶體佔用太大,被系統強行殺掉(OOM)

,從現象看就是 MySQL 異常重啟了。我們有兩種方案解決:

  1. 定期斷開長連線。使用一段時間,或者程式裡面判斷執行過一個佔用記憶體的大查詢後,斷開連線,之後要查詢再重連
  2. 如果用的是 MySQL 5.7 或更新版本,可以在每次執行一個比較大的操作後,通過執行 mysql_reset_connection 來重新初始化連線資源。這個過程不需要重連和重新做許可權驗證,但是會將連線恢復到剛剛建立完時的狀態

(2) 查詢快取

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

大多數情況下建議不要使用查詢快取:因為查詢快取的失效非常頻繁,只要有對一個表的更新,這個表上所有的查詢快取都會被清空

可以將引數 query_cache_type 設定成 DEMAND,這樣對於預設的 SQL 語句都不使用查詢快取。而對於你確定要使用查詢快取的語句,可以用 SQL_CACHE 顯式指定:

select SQL_CACHE * from T where ID=10;

MySQL 8.0 版本直接將查詢快取的整塊功能刪掉了,也就是說 8.0 開始徹底沒有這個功能了

(3) 分析器

通過分析器進行 詞法分析,詞法分析作用就是分析 SQL 語句裡面的每個字串都代表什麼

做完上面的操作後,接下來進行語法分析,就是判斷我們輸入的這個 SQL 語句是否滿足 MySQL 的語法

(4) 優化器

優化器的作用就是在表有多個索引的時候,決定使用哪個索引、或者有多表關聯 (join) 的時候,決定表的連線順序,優化器會選擇認為效率更高的那個方案

(5) 執行器

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

mysql> select * from T where ID=10;
ERROR 1142 (42000): SELECT command denied to user 'b'@'localhost' for table 'T'

如果有許可權,就開啟表繼續執行。開啟表的時候,執行器就會根據表的引擎定義,去使用這個引擎提供的介面

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

  1. 呼叫 InnoDB 引擎介面取這個表的第一行,判斷 ID 值是不是 10,如果不是則跳過,如果是則將這行存在結果集中
  2. 呼叫引擎介面取“下一行”,重複相同的判斷邏輯,直到取到這個表的最後一行
  3. 執行器將上述遍歷過程中所有滿足條件的行組成的記錄集作為結果集返回給客戶端

對於有索引的表,執行的邏輯也差不多:第一次呼叫的是“取滿足條件的第一行”這個介面,之後迴圈取“滿足條件的下一行”這個介面,這些介面都是引擎中已經定義好的

你會在資料庫的慢查詢日誌中看到一個 rows_examined 的欄位,表示這個語句執行過程中掃描了多少行。這個值就是在執行器每次呼叫引擎獲取資料行的時候累加的。在有些場景下,執行器呼叫一次,在引擎內部則掃描了多行,因此引擎掃描行數跟 rows_examined 並不是完全相同的

2. 儲存引擎層

儲存引擎層負責資料的儲存和提取,架構模式是外掛式的,即不同儲存引擎公用 Server層。常見的儲存引擎有 InnoDB(預設)MemoryMyISAM 等。我們可以再建立表的時候通過指定 engine 引數選擇儲存引擎,現在最常用的儲存引擎是 InnoDB,它從 MySQL 5.5.5 版本開始成為了預設儲存引擎

二、日誌系統

  • 一條 SQL 語句是如何更新的
  • 查詢語句的那一套流程,更新語句也是同樣會走一遍。與查詢流程不一樣的是,更新流程還涉及兩個重要的日誌模組:redo log(重做日誌)和 binlog(歸檔日誌)

1. redo log: 重做日誌

物理日誌 - 儲存的是在某個資料頁上做了什麼更改

如果每一次的更新操作都要寫入道磁盤裡,那麼整個過程的 O/I 成本就會特別高了,為了解決這個問題,MySQL 使用到了 WAL(Write-Ahead Logging) 技術,即先寫到日誌 (redo log) 裡面,再寫磁碟,這個時候就代表更新完成了,等到系統空閒的時候再將資料寫入到磁碟中

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

write pos 是當前記錄的位置,一邊寫一邊後移,寫到第 3 號檔案末尾後就回到 0 號檔案開頭。checkpoint 是當前要擦除的位置,也是往後推移並且迴圈的,擦除記錄前要把記錄更新到資料檔案

write pos 和 checkpoint 之間的是“粉板”上還空著的部分,可以用來記錄新的操作。如果 write pos 追上 checkpoint,表示日誌檔案滿了,這時候不能再執行新的更新,得停下來先擦掉一些記錄,把 checkpoint 推進一下。有了 redo log,InnoDB 就可以保證即使資料庫發生異常重啟,之前提交的記錄都不會丟失,這個能力稱為 crash-safe

2. binlog: 歸檔日誌

邏輯日誌 - 記錄的是這個語句的原始邏輯

  • redo log 是 InnoDB 引擎特有的日誌;binlog(歸檔日誌)是Server 層自己的日誌。

最開始 MySQL 裡並沒有 InnoDB 引擎。MySQL 自帶的引擎是 MyISAM,但是 MyISAM 沒有 crash-safe 的能力,binlog 日誌只能用於歸檔。而 InnoDB 是另一個公司以外掛形式引入 MySQL 的,既然只依靠 binlog 是沒有 crash-safe 能力的,所以 InnoDB 使用另外一套日誌系統——也就是 redo log 來實現 crash-safe 能力

這兩種日誌有以下三點不同:

  1. redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 層實現的,所有引擎都可以使用
  2. redo log 是物理日誌,記錄的是 “在某個資料頁上做了什麼修改” ;binlog 是邏輯日誌,記錄的是這個語句的原始邏輯,比如 “給 ID=2 這一行的 c 欄位加 1”
  3. redo log 是迴圈寫的,空間固定會用完;binlog 是可以追加寫入的。“追加寫”是指 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 內部執行的,深色框表示是在執行器中執行的:

最後三步看上去有點“繞”,將 redo log 的寫入拆成了兩個步驟:prepare 和 commit,這就是"兩階段提交"

3. 兩階段提交

為什麼必須有“兩階段提交”呢?這是為了讓兩份日誌之間的邏輯一致。要說明這個問題,我們得從文章開頭的那個問題說起:怎樣讓資料庫恢復到半個月內任意一秒的狀態?

前面我們說過了,binlog 會記錄所有的邏輯操作,並且是採用“追加寫”的形式。如果你的 DBA 承諾說半個月內可以恢復,那麼備份系統中一定會儲存最近半個月的所有 binlog,同時系統會定期做整庫備份。這裡的“定期”取決於系統的重要性,可以是一天一備,也可以是一週一備

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

  • 首先,找到最近的一次全量備份,如果你運氣好,可能就是昨天晚上的一個備份,從這個備份恢復到臨時庫

  • 然後,從備份的時間點開始,將備份的 binlog 依次取出來,重放到中午誤刪表之前的那個時刻

這樣你的臨時庫就跟誤刪之前的線上庫一樣了,然後你可以把表資料從臨時庫取出來,按需要恢復到線上庫去

好了,說完了資料恢復過程,我們回來說說,為什麼日誌需要“兩階段提交”。這裡不妨用反證法來進行解釋

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

  1. 先寫 redo log 後寫 binlog:假設在 redo log 寫完,binlog 還沒有寫完的時候,MySQL 程序異常重啟,但是由於redo log 的 crash_safe 特性,即使宕機重啟後也能夠恢復資料,此時看起來是正常的,但是要注意 binlog 中沒有儲存這條記錄,因此以後我們在資料恢復的時候,就會漏掉了這條資料,導致資料與原庫仍不一致
  2. 先寫 binlog 後寫 redo log:如果先寫 binlog 之後,redo log 還沒寫資料庫就宕機然後異常重啟,此時這個還沒寫進 redo log,那麼重啟後這個事務就是失效的,但是我們在 binlog 中已經儲存了這個記錄,以後在資料恢復的時候就會導致多了這條記錄,與原來資料庫的值不同

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

你可能會說,這個概率是不是很低,平時也沒有什麼動不動就需要恢復臨時庫的場景呀?其實不是的,不只是誤操作後需要用這個過程來恢復資料。當你需要擴容的時候,也就是需要再多搭建一些備庫來增加系統的讀能力的時候,現在常見的做法也是用全量備份加上應用 binlog 來實現的,這個“不一致”就會導致你的線上出現主從資料庫不一致的情況。簡單說,redo log 和 binlog 都可以用於表示事務的提交狀態,而兩階段提交就是讓這兩個狀態保持邏輯上的一致

4. 總結

  • redo log 用於保證 crash-safe 能力。innodb_flush_log_at_trx_commit 這個引數設定成 1 表示每次事務的 redo log 都直接持久化到磁碟;設定成 0 表示每秒將 log buffer 同步到 os buffer 並且從 os buffer 刷到磁碟的日誌檔案中;設定成 2 表示每次事物都將 log buffer 同步到 os buffer 但每秒才從 os buffer 刷到磁碟的日誌檔案中。這個引數建議你設定成 1,這樣可以保證 MySQL 異常重啟之後資料不丟失
  • sync_binlog 這個引數設定成 1 的時候,表示每次事務的 binlog 都持久化到磁碟。這個引數我也建議你設定成 1,這樣可以保證 MySQL 異常重啟之後 binlog 不丟失
  • 在做主從讀寫分離的時候,從伺服器將 innodb_flush_log_at_trx_commit 和 sync_binlog 這兩個引數設定成0能有效地提高 SQL 的執行效率
  • 兩階段提交是跨系統維持資料邏輯一致性時常用的一個方案