數據庫併發處理 - 上的一把好"鎖"

爲何要有鎖?mysql

咱們都是知道,數據庫中鎖的設計是解決多用戶同時訪問共享資源時的併發問題。在訪問共享資源時,鎖定義了用戶訪問的規則。根據加鎖的範圍,MySQL 中的鎖可大體分紅全局鎖,表級鎖和行鎖三類。在本篇文章中,會依次介紹三種類型的鎖。在閱讀本篇文章後,應該掌握以下的內容:sql

  1. 爲何要在備份時使用全局鎖?
  2. 爲何推薦使用 InnoDB 做爲引擎進行備份?
  3. 設置全局只讀的方法
  4. 表級鎖的兩種類型
  5. MDL 致使數據庫掛掉的問題
  6. 如何利用兩段鎖協議減小鎖衝突
  7. 如何解決死鎖
  8. 對於熱點表,如何避免死鎖檢測的損耗?

全局鎖

什麼是全局鎖?

全局鎖會讓整個庫處於只讀狀態,其餘線程語句(DML,DDL,更新事務類)的語句都被會阻塞。數據庫

使用全局鎖的場景

在作全庫邏輯備份時,會把整庫進行 select 而後保存成文本。安全

爲何要使用全局鎖?

想象這樣一個場景,要備份一個購買系統,其中購買操做設計到更新帳號餘額表和用戶課程表。session

如今進行邏輯備份,在備份過程當中,一位用戶購買了一門課程,這時須要在餘額表扣掉餘額,而後在購買的課程中加上一門課。正確的順序確定是先進行購買操做,減小余額和增長課程而後在進行備份。但卻有可能出現這樣的問題:併發

  1. 若是在時間順序上先備份餘額表 (u_account),而後用戶購買(操做兩張表),再備份用戶課程表(u_course)?ide

    這時用備份的數據作恢復時,會發現用戶沒花錢卻買了一堂課。緣由在於,先備份餘額表,說明用戶餘額不變。以後才進行購買操做,餘額表減錢,課程表增長一門課程。接着備份課程表,課程表課程加一。購買操做在已經備份完的餘額表後進行。工具

  2. 若是在時間順序上先備份用戶課程表(u_course),而後用戶購買(操做兩張表),再備份餘額表 (u_account)?線程

    一樣的,若是先備份課程表,課程沒有增長,由於沒有進行購買操做。以後進行購買操做後,餘額表減錢,而後被備份。就出現了,用戶花錢卻沒有購買成功的狀況。設計

也就是說,不加鎖的話,備份系統的獲得的庫不是一個邏輯時間點,這個視圖是邏輯不一致。

如何解決視圖邏輯不一致的問題?

對於不支持事務的引擎,像 MyISAM. 經過使用 Flush tables with read lock (FTWRL) 命令來開啓全局鎖。

但使用 FTWRL 存在的問題是:

  1. 在主庫上備份時,備份期間不能執行更新,業務基本暫停。
  2. 在從庫上備份,備份期間從庫不能執行主庫同步過來的 binlog,致使主從延遲。

對於支持事務而且開啓一致性視圖(可重複讀級別)下配合上 MVCC 的功能的引擎(InnoDB),備份就很簡單了。

使用官方的 mysqldump 工具時,加上 --single-transaction 選項,再導出數據前就會啓動一個事務,來確保拿到一致性視圖。而且因爲 MVCC 的支持,同時能夠進行更新操做。

全庫只讀設置方法的比較

爲何不推薦使用 set global readonly=true ,要使用 FTWRL :

  1. 在有些系統中,readonly 的值會被用來作其餘邏輯,好比用來判斷一個庫是主庫仍是備庫。所以,修改 global 變量的方式影響面更大,不建議使用。

  2. 在異常處理機制上有差別。

    執行 FTWRL 命令以後因爲客戶端發生異常斷開,那麼 MySQL 會自動釋放這個全局鎖,整個庫回到能夠正常更新的狀態。

    將整個庫設置爲 readonly 以後,若是客戶端發生異常,則數據庫就會一直保持 readonly 狀態,這樣會致使整個庫長時間處於不可寫狀態,風險較高。

表級鎖

什麼是表級鎖?

表級鎖的做用域是對某張表進行加鎖,在 MySQL 中表級別的鎖有兩種,一種是表鎖,一種是元數據鎖(meta data lock,MDL)。

表鎖

與 FTWRL 相似,可使用 lock tables … read/write 來鎖定某張表。在釋放時,可使用 unlock tables 來釋放鎖或者斷開鏈接時,主動釋放。

須要注意的是,這樣方式的鎖表,不但會限制其餘線程的讀寫,也限定了本身線程的操做對象。

假如,線程 A 執行 lock tables t1 read, t2 write; 操做。

這時對於表 t1 來講,其餘線程只能只讀,線程 A 也只能只讀,不能寫。

對於表 t2 來講,只容許線程 A 讀寫,其餘線程讀寫都會被阻塞。

元數據鎖

與表鎖手動加鎖不一樣,元數據鎖會自動加上。

爲何要有 MDL?

MDL 保證的就是讀寫的正確性,好比在查詢一箇中的數據時,此時另外一個線程改變了表結構,查詢的結果和表結構不一致確定不行。簡單來講,*MDL 就是解決 DML 和 DDL 之間同時操做的問題。*

在 MySQL 5.5 引入了 MDL,在對一個進行 DML 時,會加 DML 讀鎖。進行 DDL 時,會加 MDL寫鎖。

讀鎖間不互斥,容許多個線程同時對同一張表進行 DML。

讀寫鎖之間、寫鎖之間是互斥的,用來保證變動表結構操做的安全性。

  1. 若是有兩個線程要同時給一個表加字段,其中一個要等另外一個執行完才能開始執行。
  2. 若是一個線程要讀,另外一個線程要寫。根據訪問表的時間,一個操做進行完以後,另外一個才能夠進行。

MDL 引起的問題?

給表加字段,卻致使庫掛了?

因爲 MDL 是自動加的,而且在給表加字段或者修改字段或者加索引時,須要掃描全表的數據。因此在對大表操做時,要很是當心,以避免對線上的服務形成影響。但實際上,操做小表時,也可能出問題。假設 t 是小表。按照下圖所示,打開四個 session.

MySQL 5.7.27

假設有一張叫 sync_test 的表:

mysql> desc sync_test;
+-------+--------------+------+-----+---------+----------------+
| Field | Type         | Null | Key | Default | Extra          |
+-------+--------------+------+-----+---------+----------------+
| id    | int(11)      | NO   | PRI | NULL    | auto_increment |
| name  | varchar(255) | NO   |     | NULL    |                |
+-------+--------------+------+-----+---------+----------------+
2 rows in set (0.00 sec)

開啓事務1, 插入數據。對於事務 1 來講,自動申請了表 sync_test 的 MDL 讀鎖:

數據庫併發處理 - 上的一把好

開啓事務2,插入數據。對於事務 2 來講,自動申請了表 sync_test 的 MDL 讀鎖:

數據庫併發處理 - 上的一把好

開啓事務3,改變表結構。對於事務 3 來講,會申請表 sync_test 的 MDL 寫鎖,這時因爲讀寫鎖互斥,被阻塞:

數據庫併發處理 - 上的一把好

開啓事務 4,插入數據。對於事務 4 來講,會申請 sync_test 的 MDL 讀鎖,因爲以前事務 3 提早申請了寫鎖,互斥因此被阻塞:

數據庫併發處理 - 上的一把好

這時若是在這張表上的查詢語句很頻繁,並且客戶端有重連機制,在超時後會再起一個新 session 請求,這個庫的線程就很快會爆滿了。

如何安全的給表加資源

經過上面的例子也能夠看到,MDL 會直到事務提交才釋放,在作表結構變動的時候,必定要當心不要致使鎖住線上查詢和更新。在開啓事務後,並無在短期內結束,也就是因爲所謂的長事務形成的。若是想對某個表進行 DDL 的操做時,能夠先查詢下是否有長事務的運行(information_schema 下的 innodb_trx 表),能夠先 kill 這個事務,而後作 DDL 操做。

但有時 kill 也未必能夠,在表被頻繁使用時,新的事務可能立刻就來了。比較理想的狀況,在 alter table 中設定等待時間,若是在時間內拿到最好,不然就放棄,不要阻塞語句。以後再重複這個操做。

MariaDB 已經合併了 AliSQL 的這個功能,因此這兩個開源分支目前都支持 DDL NOWAIT/WAIT n 這個語法。

ALTER TABLE tbl_name NOWAIT add column ...
ALTER TABLE tbl_name WAIT N add column ...

行級鎖

什麼是行級鎖?

MySQL 的行鎖是由引擎層本身實現的,全部不是全部的引擎都執行行鎖,好比在 MyISAM 引擎就不支持行鎖。不支持行鎖意味着併發控制只能用表鎖,這就形成了在同一時刻只有一個更新在執行,就影響到了業務的併發度。InnoDB 支持行鎖是讓 MyISAM 被取代的重要緣由。

行鎖就是對數據庫表中行記錄的鎖。好比事務 A,B 同時想要更新一行數據,在更新時必定會按照必定的順序進行,而不能同時更新。

行鎖的目的就是減小像表級別的鎖衝突,來提高業務的併發度。

兩階段鎖協議

在 InnoDB 的事務中,行鎖是在須要的時候在加上,但並非使用完就釋放,而是在事務結束後才釋放,這就是兩階段鎖協議。

假設有一個表 t,事務 A, B 操做表 t 的過程以下:

數據庫併發處理 - 上的一把好

在事務 A 的兩條語句更新後,事務 B 更新操做會被阻塞。直到事務 A 中執行 commit 操做後才能執行。

兩階段鎖在事務上的幫助

因爲兩階段鎖的特色,在事務結束時纔會釋放鎖,因此須要遵循的一個原則是事務中須要鎖多個行時,把有可能形成鎖衝突,最可能影響併發度的鎖儘可能向後放。

好比購買課程的例子,顧客 A 購買培訓機構 B 一門課程。涉及到操做:

  1. 顧客 A 的餘額減小
  2. 培訓機構 B 所在的餘額增長。
  3. 插入一條交易信息的操做。

對於第二個操做,當有許多人同時購買時併發度就較高,出現鎖衝突的狀況也較高。因此將操做 2 放置一個事務的最後就更好。

當有時併發度過大時,咱們會發現一種現象 CPU 的使用率接近 100%,但事務執行數量卻不多。這就可能出現了死鎖。

死鎖的檢查

當併發系統中不一樣的線程出現循環的資源依賴,等待別的線程釋放資源時,就會讓涉及的線程處於一直等待的狀況。這就稱爲死鎖。

數據庫併發處理 - 上的一把好

如上圖中,事務 A 對id =1 的所在行,加入了行鎖。等待 id=2 的行鎖。事務 B 對 id = 2 的行,加入了行鎖。等待 id=1 的行鎖。事務 A,B 等待對方資源的釋放。

如何解決死鎖

方式 一: 設置死鎖的等待時間 innodb_lock_wait_timeout

仍是 sync_test 這張表,模擬簡單的鎖等待狀況,注意這裏並非死鎖。開啓兩個事務 A,B. 同時對 id=1 這行進行更新。

事務 A 更新操做:

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update sync_test set name="dead_lock_test" where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

事務 B 更新操做:

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update sync_test set name="dead_lock_test2" where id = 1;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

能夠看到事務 B 拋出了死鎖等待的錯誤。

設置等待時間的問題

在 InnoDB 中,MySQL 默認的死鎖等待時間是 50s. 意味着在出現死鎖後,被鎖住的線程要過 50s 被能退出,這對於在線服務說,等待時間過長。但若是把值設置的太小,若是是像上述例子這樣是簡單的鎖等待呢,並非死鎖怎麼辦,就會出現誤傷的狀況。

方式二:發起死鎖檢測,發現死鎖後,主動回滾某個事務,讓其餘事務繼續執行。

MySQL 中默認就是打開狀態,可以快速發現死鎖的狀況。

set innodb_deadlock_detect=on

事務 A,B 互相依賴,形成死鎖的例子:

開啓事務 A:

mysql> begin;
mysql> update sync_test set name="dead_lock_test1" where id = 1;

開啓事務 A:

mysql> begin;
mysql> update sync_test set name="dead_lock_test3" where id = 3;

繼續操做事務 A:

mysql> update sync_test set name="dead_lock_test3_1" where id = 3;

# 會出現阻塞的狀況

繼續操做事務 B:

mysql> update sync_test set name="dead_lock_test1_2" where id = 1;
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

此時事務 A 阻塞取消,執行成功。

不過檢測死鎖也是有額外負擔的,每當一個事務被鎖的時候,就要看看它所依賴的線程有沒有被別人鎖住,如此循環,最後判斷是否出現了循環等待,也就是死鎖。若是是全部事務都要更新同一行的場景呢?每一個新來的被堵住的線程,都要判斷會不會因爲本身的加入致使了死鎖,這是一個時間複雜度是 O(n) 的操做。假設有 1000 個併發線程要同時更新同一行,那麼死鎖檢測操做就是 1000*1000=100 萬這個量級的。

因此,對於更新頻繁併發量大的表,死鎖檢測會致使消耗大量的 CPU.

如何避免死鎖檢測的損耗

方法一:若是保證業務必定不會出現死鎖,能夠臨時把死鎖檢查關掉。

但這樣存在必定的風險,由於業務設計時不會把死鎖當作嚴重的問題,出現死鎖後回滾後,再重試就沒有問題了。但關掉死鎖檢測後,可能出現大量超時的狀況。

方法二:控制併發度。

若是對於併發量能控制,好比同一行同時最多隻有 10 個線程在更新,那麼死鎖檢測的成本很低,就不會出現這個問題。具體來講在客戶端作併發控制,但對於客戶端較多的應用,也沒法控制。因此併發控制在數據庫服務端,若是有中間件,也能夠考慮在中間件中實現。

方法三:下降死鎖的機率

將一行統計的結構,拆成多行累計的結構。好比將以前某個教學機構的金額由一行拆成 10 行,總收入就等於這 10 行數據的累計。這樣原來鎖衝突的機率變爲原來的 1/10, 也就減小了死鎖檢測的 CPU 消耗。但在一部分行記錄變成0 時,代碼須要特殊處理。

總結

本篇文章中,依次介紹了全局鎖、表級鎖和行鎖的概念。

對於全局鎖來講,使用 InnoDB 引擎 在 RR 級別和 MVCC 的幫助下,可讓其在備份的同事更新數據。

對於表級鎖來講,對於更新熱點表的表結構時,要注意 MDL 讀寫鎖互斥的狀況,形成數據庫掛掉。

對於行級鎖來講,合理的利用兩段鎖協議,下降鎖的衝突。並要注意死鎖發生的狀況,採起合適的死鎖檢測手段。

相關文章
相關標籤/搜索