1. 程式人生 > >【MySQL 讀書筆記】全局鎖 | 表鎖 | 行鎖

【MySQL 讀書筆記】全局鎖 | 表鎖 | 行鎖

xxx 行數 size 很快 自己 session mage 使用場景 進程

全局鎖

全局鎖是針對數據庫實例的直接加鎖,MySQL 提供了一個加全局鎖的方法, Flush tables with read lock 可以使用鎖將整個表的增刪改操作都鎖上其中包括 ddl 語句,只允許全局讀操作。

全局鎖的典型使用場景是做全庫的邏輯備份。

不過現在使用官方自帶工具 mysqldump 使用參數 --single-transaction 的時候,導出數據之前就會啟動一個事務。來確保拿到一致性視圖。這個應該類似於在可重復讀隔離級別下啟動一個一致性事務。由於 MVCC 的支持,這個過程中數據可以正常更新。

另外提一點不太容易遇到的, --single-transaction 既然可以不用鎖表,為什麽還需要使用全局鎖?原因是 --single-transaction 的時候需要支持一致性讀,但是不支持事務的引擎是不支持一致性讀的。這個時候就需要 FTWRL 命令了。

還有另外一種方法用來支持設置數據庫為只讀狀態

set global readonly=true

這裏 丁奇 不建議這樣設置來設置數據庫為只讀有兩個原因

一是,在有些系統中,readonly 的值會被用來做其他邏輯,比如用來判斷一個庫是主庫還是備庫。因此,修改 global 變量的方式影響面更大,我不建議你使用。

二是,在異常處理機制上有差異。如果執行 FTWRL 命令之後由於客戶端發生異常斷開,那麽 MySQL 會自動釋放這個全局鎖,整個庫回到可以正常更新的狀態。而將整個庫設置為 readonly 之後,如果客戶端發生異常,則數據庫就會一直保持 readonly 狀態,這樣會導致整個庫長時間處於不可寫狀態,風險較高。

表級鎖

MySQL 中表級別鎖有兩種:一種是普通表鎖,一種是元數據鎖(metadata lock. MDL)

表鎖的語法是 lock tables xxx read/write 同樣使用 unlock tables 來釋放鎖。通過加讀鎖我們可以限制其他語句進行寫入,但是重復加讀鎖不受影響。但是當我們加寫鎖的時候,既不可以讀也不可以寫。同樣在使用 unlock tables 之後可以解除鎖定。

另外一種表級鎖是 MDL 鎖(metadata lock) MDL 鎖不需要顯示的使用,在訪問一個表的時候自動就被加上了。 MDL 鎖是用來保證讀寫正確性的,當我們對一個表在做 增刪改查操作的時候都會被加上 MDL 讀鎖。當要進行 ddl 的時候需要加 MDL 寫鎖。

MDL 讀鎖與讀鎖之間不互斥,因此我們可以多個線程進程對一個表進行增刪改查。

MDL 讀寫鎖之間互斥,用來保證表結構變更的安全性。因此如果有兩個線程同時要給同一個表加字段,其中一個要等另外一個執行完成之後再開始執行。

下面我們來看一個比較有代表性的場景 MDL 讀鎖寫鎖互斥導致表無法讀寫被死鎖。

技術分享圖片

session A: 開始一個事務,然後查詢 t 表,這會給 t 表加上 MDL 讀鎖。(註意該事務被打開後就一直沒有結束)

session B: 查詢一個 t 表。這裏應該是 autoocommit 會自動成功。

session C: 修改表 ddl 會加 MDL 寫鎖,和 session A 的讀鎖互斥。這個時候就鎖住了表。

session D: 由於 session C 造成了寫鎖阻塞,所以後面所有的請求都會被鎖住。

如果該表查詢頻繁,而且客戶端有重試的機制,那麽這個數據庫的查詢線程會很快被打滿。

可能在進行 web 開發的同學會經常遇到類似的情況。比如我在 ipython 裏面打開了一個數據庫某個表的連接,然後我一直沒有 commit 。就可能造成該表在加寫鎖的時候阻塞後面所有的操作。

這種事情非常常見。

那麽我們如何安全的給小表加字段,首先我們應該解決長事務或者腳本事務的問題,因為他們會一直掛讀鎖不結束。在 MySQL 的 information_schema 中的 innodb_trx 中可以查詢到執行中的長事務,但是比較麻煩的是這個看不到很短的事務。但是往往進行 sleep 的短事務也可能因為一直沒有 commit 而導致上面的情況出現。

這個時候就需要把對應表的 sleep 進程 kill 掉使其恢復正常。

行級鎖

先來看個描述兩階段鎖的例子:

技術分享圖片

事務 A 會持有兩條記錄的行鎖,並且只會在 commit 之後才會釋放。

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

知道了這個設定我們應該在長事務中把影響並發度的鎖盡量往後放。下面的這一段的介紹比較復雜,我覺得 丁奇 講得還是比較清楚的所以直接引用原文了。

假設你負責實現一個電影票在線交易業務,顧客 A 要在影院 B 購買電影票。我們簡化一點,這個業務需要涉及到以下操作:

1. 從顧客 A 賬戶余額中扣除電影票價;

2. 給影院 B 的賬戶余額增加這張電影票價;

3. 記錄一條交易日誌。

也就是說,要完成這個交易,我們需要 update 兩條記錄,並 insert 一條記錄。當然,為了保證交易的原子性,我們要把這三個操作放在一個事務中。那麽,你會怎樣安排這三個語句在事務中的順序呢?

試想如果同時有另外一個顧客 C 要在影院 B 買票,那麽這兩個事務沖突的部分就是語句 2 了。因為它們要更新同一個影院賬戶的余額,需要修改同一行數據。

根據兩階段鎖協議,不論你怎樣安排語句順序,所有的操作需要的行鎖都是在事務提交的時候才釋放的。所以,如果你把語句 2 安排在最後,比如按照 3、1、2 這樣的順序,那麽影院賬戶余額這一行的鎖時間就最少。這就最大程度地減少了事務之間的鎖等待,提升了並發度。

死鎖和死鎖檢測

如果出現下面的不慎操作就會發生死鎖。

技術分享圖片

事務 A 開啟事務,並且拿了 id = 1 的行鎖。

事務 B 開啟事務,拿到 id = 2 的行鎖。

事務 A 試圖去拿 id = 2 的行鎖被 block。

事務 B 試圖去拿 id = 1 的行鎖被 block。

解決死鎖 MySQL 目前有兩種策略,第二種策略的參數我沒有在 MySQL 5.6 版本中找到,在 MySQL 5.7 中找到。

1. 一種策略是,直接進入等待,直到超時。這個超時時間可以通過參數 innodb_lock_wait_timeout 來設置。這個參數默認是 50s。

2. 另一種策略是,發起死鎖檢測,發現死鎖後,主動回滾死鎖鏈條中的某一個事務,讓其他事務得以繼續執行。將參數 innodb_deadlock_detect 設置為 on,表示開啟這個邏輯。

很顯然,等待 50 s 失效在現實業務中是不切實際的。肯定會造成高並發的業務大量的阻塞和 500 。所以看上去我們可以依賴第二種辦法?

但是第二種辦法也有副作用。

死鎖檢測會對每個新來的被堵住的線程,都判斷會不會由於自己的加入導致了死鎖,這是一個時間復雜度是 O(n) 的操作。假設有 1000 個並發線程要同時更新同一行,那麽死鎖檢測操作就是 100 萬這個量級的。雖然最終檢測的結果是沒有死鎖,但是這期間要消耗大量的 CPU 資源。因此,你就會看到 CPU 利用率很高,但是每秒卻執行不了幾個事務。

解決這個的方法是

1. 如果我們能確保業務中就是不會存在死鎖的邏輯,那麽我們可以關閉死鎖檢測。

2. 我們控制並發度,不讓某些業務更新這麽快。對客戶端的並發控制下來之後,死鎖檢測的效率是高的,也可以解決這個問題。

Reference:

本讀書筆記皆來自發布在極客時間的 林曉斌(丁奇)的 MySQL 實戰45講:

極客時間版權所有: https://time.geekbang.org/ 版權所有:

https://time.geekbang.org/column/article/69862

https://time.geekbang.org/column/article/70215

【MySQL 讀書筆記】全局鎖 | 表鎖 | 行鎖