溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

數(shù)據(jù)庫的鎖介紹和使用

發(fā)布時間:2020-06-06 19:28:22 來源:億速云 閱讀:433 作者:Leah 欄目:編程語言

這篇文章的知識點包括:全局鎖、表級鎖和行鎖的概念和使用、解決死鎖和避免死鎖檢測的損耗,閱讀完整文相信大家對數(shù)據(jù)庫的鎖有了一定的認(rèn)識。

全局鎖

什么是全局鎖?

全局鎖會讓整個庫處于只讀狀態(tài),其他線程語句(DML,DDL,更新事務(wù)類)的語句都被會阻塞。

使用全局鎖的場景

在做全庫邏輯備份時,會把整庫進(jìn)行 select 然后保存成文本。

為什么要使用全局鎖?

想象這樣一個場景,要備份一個購買系統(tǒng),其中購買操作設(shè)計到更新賬號余額表和用戶課程表。

現(xiàn)在進(jìn)行邏輯備份,在備份過程中,一位用戶購買了一門課程,這時需要在余額表扣掉余額,然后在購買的課程中加上一門課。正確的順序肯定是先進(jìn)行購買操作,減少余額和增加課程然后在進(jìn)行備份。但卻有可能出現(xiàn)這樣的問題:

  1. 如果在時間順序上先備份余額表 (u_account),然后用戶購買(操作兩張表),再備份用戶課程表(u_course)?

    這時用備份的數(shù)據(jù)做恢復(fù)時,會發(fā)現(xiàn)用戶沒花錢卻買了一堂課。原因在于,先備份余額表,說明用戶余額不變。之后才進(jìn)行購買操作,余額表減錢,課程表增加一門課程。接著備份課程表,課程表課程加一。購買操作在已經(jīng)備份完的余額表后進(jìn)行。

  2. 如果在時間順序上先備份用戶課程表(u_course),然后用戶購買(操作兩張表),再備份余額表 (u_account)?

    同樣的,如果先備份課程表,課程沒有增加,因為沒有進(jìn)行購買操作。之后進(jìn)行購買操作后,余額表減錢,然后被備份。就出現(xiàn)了,用戶花錢卻沒有購買成功的情況。

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

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

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

但使用 FTWRL 存在的問題是:

  1. 在主庫上備份時,備份期間不能執(zhí)行更新,業(yè)務(wù)基本暫停。
  2. 在從庫上備份,備份期間從庫不能執(zhí)行主庫同步過來的 binlog,導(dǎo)致主從延遲。

對于支持事務(wù)并且開啟一致性視圖(可重復(fù)讀級別)下配合上 MVCC 的功能的引擎(InnoDB),備份就很簡單了。

使用官方的 mysqldump 工具時,加上 --single-transaction 選項,再導(dǎo)出數(shù)據(jù)前就會啟動一個事務(wù),來確保拿到一致性視圖。并且由于 MVCC 的支持,同時可以進(jìn)行更新操作。

全庫只讀設(shè)置方法的比較

為什么不推薦使用 set global readonly=true ,要使用 FTWRL :

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

  2. 在異常處理機(jī)制上有差異。

    執(zhí)行 FTWRL 命令之后由于客戶端發(fā)生異常斷開,那么 MySQL 會自動釋放這個全局鎖,整個庫回到可以正常更新的狀態(tài)。

    將整個庫設(shè)置為 readonly 之后,如果客戶端發(fā)生異常,則數(shù)據(jù)庫就會一直保持 readonly 狀態(tài),這樣會導(dǎo)致整個庫長時間處于不可寫狀態(tài),風(fēng)險較高。

表級鎖

什么是表級鎖?

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

表鎖

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

需要注意的是,這樣方式的鎖表,不但會限制其他線程的讀寫,也限定了自己線程的操作對象。

假如,線程 A 執(zhí)行 lock tables t1 read, t2 write; 操作。

這時對于表 t1 來說,其他線程只能只讀,線程 A 也只能只讀,不能寫。

對于表 t2 來說,只允許線程 A 讀寫,其他線程讀寫都會被阻塞。

元數(shù)據(jù)鎖

與表鎖手動加鎖不同,元數(shù)據(jù)鎖會自動加上。

為什么要有 MDL?

MDL 保證的就是讀寫的正確性,比如在查詢一個中的數(shù)據(jù)時,此時另一個線程改變了表結(jié)構(gòu),查詢的結(jié)果和表結(jié)構(gòu)不一致肯定不行。簡單來說,*MDL 就是解決 DML 和 DDL 之間同時操作的問題。*

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

讀鎖間不互斥,允許多個線程同時對同一張表進(jìn)行 DML。

讀寫鎖之間、寫鎖之間是互斥的,用來保證變更表結(jié)構(gòu)操作的安全性。

  1. 如果有兩個線程要同時給一個表加字段,其中一個要等另一個執(zhí)行完才能開始執(zhí)行。
  2. 如果一個線程要讀,另一個線程要寫。根據(jù)訪問表的時間,一個操作進(jìn)行完之后,另一個才可以進(jìn)行。

MDL 引發(fā)的問題?

給表加字段,卻導(dǎo)致庫掛了?

由于 MDL 是自動加的,并且在給表加字段或者修改字段或者加索引時,需要掃描全表的數(shù)據(jù)。所以在對大表操作時,要非常小心,以免對線上的服務(wù)造成影響。但實際上,操作小表時,也可能出問題。假設(shè) t 是小表。按照下圖所示,打開四個 session.

MySQL 5.7.27

假設(shè)有一張叫 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)

開啟事務(wù)1, 插入數(shù)據(jù)。對于事務(wù) 1 來說,自動申請了表 sync_test 的 MDL 讀鎖:

數(shù)據(jù)庫的鎖介紹和使用

開啟事務(wù)2,插入數(shù)據(jù)。對于事務(wù) 2 來說,自動申請了表 sync_test 的 MDL 讀鎖:

數(shù)據(jù)庫的鎖介紹和使用

開啟事務(wù)3,改變表結(jié)構(gòu)。對于事務(wù) 3 來說,會申請表 sync_test 的 MDL 寫鎖,這時由于讀寫鎖互斥,被阻塞:

數(shù)據(jù)庫的鎖介紹和使用

開啟事務(wù) 4,插入數(shù)據(jù)。對于事務(wù) 4 來說,會申請 sync_test 的 MDL 讀鎖,由于之前事務(wù) 3 提前申請了寫鎖,互斥所以被阻塞:

數(shù)據(jù)庫的鎖介紹和使用

這時如果在這張表上的查詢語句很頻繁,而且客戶端有重連機(jī)制,在超時后會再起一個新 session 請求,這個庫的線程就很快會爆滿了。

如何安全的給表加資源

通過上面的例子也可以看到,MDL 會直到事務(wù)提交才釋放,在做表結(jié)構(gòu)變更的時候,一定要小心不要導(dǎo)致鎖住線上查詢和更新。在開啟事務(wù)后,并沒有在短時間內(nèi)結(jié)束,也就是由于所謂的長事務(wù)造成的。如果想對某個表進(jìn)行 DDL 的操作時,可以先查詢下是否有長事務(wù)的運行(information_schema 下的 innodb_trx 表),可以先 kill 這個事務(wù),然后做 DDL 操作。

但有時 kill 也未必可以,在表被頻繁使用時,新的事務(wù)可能馬上就來了。比較理想的情況,在 alter table 中設(shè)定等待時間,如果在時間內(nèi)拿到最好,否則就放棄,不要阻塞語句。之后再重復(fù)這個操作。

MariaDB 已經(jīng)合并了 AliSQL 的這個功能,所以這兩個開源分支目前都支持 DDL NOWAIT/WAIT n 這個語法。

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

行級鎖

什么是行級鎖?

MySQL 的行鎖是由引擎層自己實現(xiàn)的,所有不是所有的引擎都執(zhí)行行鎖,比如在 MyISAM 引擎就不支持行鎖。不支持行鎖意味著并發(fā)控制只能用表鎖,這就造成了在同一時刻只有一個更新在執(zhí)行,就影響到了業(yè)務(wù)的并發(fā)度。InnoDB 支持行鎖是讓 MyISAM 被取代的重要原因。

行鎖就是對數(shù)據(jù)庫表中行記錄的鎖。比如事務(wù) A,B 同時想要更新一行數(shù)據(jù),在更新時一定會按照一定的順序進(jìn)行,而不能同時更新。

行鎖的目的就是減少像表級別的鎖沖突,來提升業(yè)務(wù)的并發(fā)度。

兩階段鎖協(xié)議

在 InnoDB 的事務(wù)中,行鎖是在需要的時候在加上,但并不是使用完就釋放,而是在事務(wù)結(jié)束后才釋放,這就是兩階段鎖協(xié)議。

假設(shè)有一個表 t,事務(wù) A, B 操作表 t 的過程如下:

數(shù)據(jù)庫的鎖介紹和使用

在事務(wù) A 的兩條語句更新后,事務(wù) B 更新操作會被阻塞。直到事務(wù) A 中執(zhí)行 commit 操作后才能執(zhí)行。

兩階段鎖在事務(wù)上的幫助

由于兩階段鎖的特點,在事務(wù)結(jié)束時才會釋放鎖,所以需要遵循的一個原則是事務(wù)中需要鎖多個行時,把有可能造成鎖沖突,最可能影響并發(fā)度的鎖盡量向后放。

比如購買課程的例子,顧客 A 購買培訓(xùn)機(jī)構(gòu) B 一門課程。涉及到操作:

  1. 顧客 A 的余額減少
  2. 培訓(xùn)機(jī)構(gòu) B 所在的余額增加。
  3. 插入一條交易信息的操作。

對于第二個操作,當(dāng)有許多人同時購買時并發(fā)度就較高,出現(xiàn)鎖沖突的情況也較高。所以將操作 2 放置一個事務(wù)的最后就更好。

當(dāng)有時并發(fā)度過大時,我們會發(fā)現(xiàn)一種現(xiàn)象 CPU 的使用率接近 100%,但事務(wù)執(zhí)行數(shù)量卻很少。這就可能出現(xiàn)了死鎖。

死鎖的檢查

當(dāng)并發(fā)系統(tǒng)中不同的線程出現(xiàn)循環(huán)的資源依賴,等待別的線程釋放資源時,就會讓涉及的線程處于一直等待的情況。這就稱為死鎖。

數(shù)據(jù)庫的鎖介紹和使用

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

如何解決死鎖

方式 一: 設(shè)置死鎖的等待時間 innodb_lock_wait_timeout

還是 sync_test 這張表,模擬簡單的鎖等待情況,注意這里并不是死鎖。開啟兩個事務(wù) A,B. 同時對 id=1 這行進(jìn)行更新。

事務(wù) 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

事務(wù) 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

可以看到事務(wù) B 拋出了死鎖等待的錯誤。

設(shè)置等待時間的問題

在 InnoDB 中,MySQL 默認(rèn)的死鎖等待時間是 50s. 意味著在出現(xiàn)死鎖后,被鎖住的線程要過 50s 被能退出,這對于在線服務(wù)說,等待時間過長。但如果把值設(shè)置的過小,如果是像上述例子這樣是簡單的鎖等待呢,并不是死鎖怎么辦,就會出現(xiàn)誤傷的情況。

方式二:發(fā)起死鎖檢測,發(fā)現(xiàn)死鎖后,主動回滾某個事務(wù),讓其他事務(wù)繼續(xù)執(zhí)行。

MySQL 中默認(rèn)就是打開狀態(tài),能夠快速發(fā)現(xiàn)死鎖的情況。

set innodb_deadlock_detect=on

事務(wù) A,B 互相依賴,造成死鎖的例子:

開啟事務(wù) A:

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

開啟事務(wù) A:

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

繼續(xù)操作事務(wù) A:

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

# 會出現(xiàn)阻塞的情況

繼續(xù)操作事務(wù) 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

此時事務(wù) A 阻塞取消,執(zhí)行成功。

不過檢測死鎖也是有額外負(fù)擔(dān)的,每當(dāng)一個事務(wù)被鎖的時候,就要看看它所依賴的線程有沒有被別人鎖住,如此循環(huán),最后判斷是否出現(xiàn)了循環(huán)等待,也就是死鎖。如果是所有事務(wù)都要更新同一行的場景呢?每個新來的被堵住的線程,都要判斷會不會由于自己的加入導(dǎo)致了死鎖,這是一個時間復(fù)雜度是 O(n) 的操作。假設(shè)有 1000 個并發(fā)線程要同時更新同一行,那么死鎖檢測操作就是 1000*1000=100 萬這個量級的。

所以,對于更新頻繁并發(fā)量大的表,死鎖檢測會導(dǎo)致消耗大量的 CPU.

如何避免死鎖檢測的損耗

方法一:如果保證業(yè)務(wù)一定不會出現(xiàn)死鎖,可以臨時把死鎖檢查關(guān)掉。

但這樣存在一定的風(fēng)險,因為業(yè)務(wù)設(shè)計時不會把死鎖當(dāng)做嚴(yán)重的問題,出現(xiàn)死鎖后回滾后,再重試就沒有問題了。但關(guān)掉死鎖檢測后,可能出現(xiàn)大量超時的情況。

方法二:控制并發(fā)度。

如果對于并發(fā)量能控制,比如同一行同時最多只有 10 個線程在更新,那么死鎖檢測的成本很低,就不會出現(xiàn)這個問題。具體來說在客戶端做并發(fā)控制,但對于客戶端較多的應(yīng)用,也無法控制。所以并發(fā)控制在數(shù)據(jù)庫服務(wù)端,如果有中間件,也可以考慮在中間件中實現(xiàn)。

方法三:降低死鎖的概率

將一行統(tǒng)計的結(jié)構(gòu),拆成多行累計的結(jié)構(gòu)。比如將之前某個教學(xué)機(jī)構(gòu)的金額由一行拆成 10 行,總收入就等于這 10 行數(shù)據(jù)的累計。這樣原來鎖沖突的概率變?yōu)樵瓉淼?1/10, 也就減少了死鎖檢測的 CPU 消耗。但在一部分行記錄變成0 時,代碼需要特殊處理。

看完上述內(nèi)容,你們對數(shù)據(jù)庫的鎖有進(jìn)一步的了解嗎?如果還想學(xué)到更多技能或想了解更多相關(guān)內(nèi)容,歡迎關(guān)注億速云行業(yè)資訊頻道,感謝各位的閱讀。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI