細談數據庫表鎖和行鎖


爲什麼需要數據庫鎖呢?其實就是爲了解決併發的一些問題;鎖保證了併發下數據訪問的規則性和合理性!

根據加鎖的範圍,MySQL裏面的鎖大致可以分爲全局鎖、表級鎖、行鎖

1. 全局鎖

加全局鎖命令:flush table with read lock;(FTWRL

mysql> flush table with read lock;
Query OK, 0 rows affected (0.05 sec)

釋放全局鎖命令:unlock tables;

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

1. 全局鎖的特點

  • 全局鎖讓整個數據庫(所有表)處於只讀狀態,使用這個命令後,數據庫表的增刪改(DML)、表結構的更改(DDL)、更新類事物的提交都會被阻塞

    例如下面,前面我已經給該數據庫加上了全局鎖,此時對其中一個表進行查詢和插入操作:

    mysql> select * from test;
    +----+------+-----------+
    | id | name | adress    |
    +----+------+-----------+
    |  1 | yy   | ChongQing |
    |  2 | lch  | XiAn      |
    +----+------+-----------+
    2 rows in set (0.00 sec)
    
    mysql> insert into test values('yyg','zhongxian');
    ERROR 1223 (HY000): Can't execute the query because you have a conflicting read lock
    

    可以看到,查詢是允許的,而插入是禁止的;

2. 全局鎖的作用(全庫邏輯備份)

上面看到了全局鎖會讓數據庫只處於可讀的狀態,這種狀態會使數據庫處於一個多麼低效率的狀態,那麼爲什麼還需要它呢?

低效率的原因:

  • 如果你在主庫上備份,那麼在備份期間都不能執行更新,業務基本上就得停擺;
  • 如果你在從庫上備份,那麼備份期間從庫不能執行主庫同步過來的binlog,會導致主從延遲;

因爲在以前,全局鎖的主要作用就是:做全庫邏輯備份;
即在備份的時候,加上全局鎖,讓表只處於可讀狀態,處於這種

那麼爲什麼這麼做呢?即爲什麼需要在備份的時候加全局鎖呢,這裏用反證法來證明:

**案例:**假如一個商城裏有兩張表,一張用戶所購商品表,一張是用戶餘額表,假如在備份商品表剛完成還沒開始備份用戶餘額表的時候,一位用戶購買了某個產品,此時它的餘額扣除成功,然後備份了用戶餘額表,這時造成的現象就是:備份的商品表沒有用戶買的那個商品,但備份的餘額表卻扣除了錢;

假如是備份完餘額表,用戶下單,再備份商品表的話,結果就是:用戶的餘額沒扣,卻多了商品;

上面的案例說明了,在做全庫邏輯備份的時候,如果不加鎖,會造成備份得到的庫裏面的表不是一個邏輯時間點 ,這個視圖是邏輯不一致的;那麼提到這,對於視圖一致性,事物的可重複讀這個隔離性不就能夠實現嗎;所以官方自帶邏輯備份工具是mysqldump。當mysqldump使用參數-single-transaction的時候,導數據之前就會啓動一個事物,來確保拿到一致性視圖。而由於MVCC的支持,這個過程中數據是可以正常更新的;

那麼,有了mysqldump這個功能,爲什麼還需要FTWRL?(⭐)
因爲mysqldump是基於事物的,而有些引擎不支持事物,比如MyISAM,這種引擎在做全庫邏輯備份的時候就只能使用全局鎖了;

對於全庫只讀,還有一種方式可以實現:

set global readonly = true

那麼到底使用set的方式還是使用FTWRL的方式來進行全庫邏輯備份呢?這裏有兩個原因推薦使用FTWRL:

  • 1.在有些系統裏,readonly的值會被用來做其他邏輯,比如用來判斷一個庫是主庫還是備庫,因此修改global變量的影響比較大,不建議使用;
  • 2.兩者在異常處理機制上有差異
    • 執行FTWRL命令之後由於客戶端的發生異常斷開,那麼MySQL會自動釋放這個全局鎖,整個庫可以回到正常更新的狀態;
    • 執行set global這個方式的話,如果客戶端發生異常,則數據庫還是一直會保持只讀狀態,這樣會導致整個庫長時間處於不可寫狀態,風險較高;

2. 表級鎖

MySQL裏面表級別的鎖分兩種:

  • 表鎖
  • 元數據鎖(mete data lock,MDL)

1. 表鎖

加鎖命令: lock tables 表名 read/write

mysql> lock table test read;
Query OK, 0 rows affected (0.00 sec)

釋放鎖的命令:unlock tables;

1. 特點

  • 當還沒有出現更細粒度的鎖時,表鎖是常用的處理併發問題的方式,而對於InnoDB這種支持行鎖的引擎,一般不適用表鎖,因爲表鎖的影響效率還是很大;

  • 對某個表加表鎖鎖,不僅影響其他線程對該表的對應操作,也會影響當前線程對這張表的操作,例如:

    mysql> lock table test read;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> insert into test(name,adress) values('ygz','zhongxian');
    ERROR 1099 (HY000): Table 'test' was locked with a READ lock and can't be updated
    

    上面表示了對該表加讀鎖後,自己也不能對其進行修改;自己和其他線程只能讀取該表;

  • 當對某個表執加上寫鎖後(lock table t1 write),該線程可以對這個表進行讀寫,其他線程對該表的讀和寫都受到阻塞; (⭐)
    例子:啓動一個命令行(線程)連接數據庫,對test表進行加上寫鎖,然後在該線程中執行讀和寫:

    mysql> insert into test(name,adress) values('zsf','zhongxian');
    Query OK, 1 row affected (0.64 sec)
    
    mysql> select * from test;
    +----+------+-----------+
    | id | name | adress    |
    +----+------+-----------+
    |  1 | yy   | ChongQing |
    |  2 | lch  | XiAn      |
    |  3 | yyg  | zhongxian |
    |  4 | ygz  | zhongxian |
    |  5 | zsf  | zhongxian |
    +----+------+-----------+
    5 rows in set (0.00 sec)
    

    然後啓動另外一個線程(重新打開一個cmd連接該數據庫),然後執行查詢test表,如下,回車後將會阻塞於此,處於無結果狀態(ctrol+c可以撤銷):

    mysql> select * from test;
    
    

    當在第一個線程中執行unlock tables後(即釋放這個寫鎖),第二個線程的查詢馬上就有了結果;
    同樣,在第二個線程中對該表的更新也是一樣的效果;

2. MDL元數據鎖(metadata lock)

1. 特點

  • MDL是在MySQL5.5中引入的,MDL不需要顯示的使用,在訪問一個表的時候會自動加上,它的作用是保證讀寫的正確性;
  • 當對一個表做增刪查改的時候,加MDL讀鎖,當對表結構做更改操作的時候,加MDL寫鎖;
    • 讀鎖之間不互斥,所以可以多個線程同時對一張表增刪查改;
    • 讀寫鎖之間,寫鎖之間是互斥的,如果有多個線程要同時給一個表加字段,其中一個要等待另外一個執行完成才能開始執行;
  • 事物中的MDL鎖,在語句執行時開始申請,,但是語句結束後並不會馬上釋放,而是等到這個事物提交後才釋放; (⭐)

MDL鎖作用:(例子)

假如一個線程正在遍歷一個表,在此期間另一個線程對這個表的結構做了變更,比如加了個字段,那麼遍歷查詢的線程拿到的結果跟表結構對不上,這肯定是不行的;

3. MDL鎖的坑————給一個小表加字段

1. 問題描述

這裏將講述關於給一個小表加字段的注意事項,曾經有人因爲這導致了整個庫掛掉;

在這裏插入圖片描述
如上圖,事物A先開啓,這時會對錶t加一個MDL讀鎖,由於事物B需要的也是MDL讀鎖,因此可以正常執行;
但此時事物C需要更改表的結構,則需要獲取MDL寫鎖,但此時事物A的讀鎖還沒有釋放,所以事物C就會被阻塞,事物C一阻塞,就算後面需要的是MDL讀鎖,也都會被阻塞,這將可能會導致後面的所有事物都不能對這個表進行增刪改查,如果這個表上的查詢語句很頻繁,而且客戶端有重試機制,這將會導致這個數據庫線程很快就會爆滿;

注意:事物中的MDL鎖,在語句執行時開始申請,,但是語句結束後並不會馬上釋放,而是等到這個事物提交後才釋放; (⭐)
爲驗證這一點,我們來做個實驗,開啓一個cmd連接數據庫,然後開啓一個事物(A),執行查詢test表的操作,然後開啓另一個cmd鏈接數據庫,開啓新的事物(B),此時在後面的事物中執行alter表test的操作,會發現阻塞在那;
事物A:

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

mysql> select * from test;
+----+------+-----------+
| id | name | adress    |
+----+------+-----------+
|  1 | yy   | ChongQing |
|  2 | lch  | XiAn      |
|  3 | yyg  | zhongxian |
|  4 | ygz  | zhongxian |
|  5 | zsf  | zhongxian |
+----+------+-----------+
5 rows in set (0.09 sec)

事物B:

mysql> use java7;
Database changed
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from test;
+----+------+-----------+
| id | name | adress    |
+----+------+-----------+
|  1 | yy   | ChongQing |
|  2 | lch  | XiAn      |
|  3 | yyg  | zhongxian |
|  4 | ygz  | zhongxian |
|  5 | zsf  | zhongxian |
+----+------+-----------+
5 rows in set (0.00 sec)

mysql> alter table test add sex char;

發現阻塞於此。。。。

此時將事物A中的事物提交,這邊馬上就修改成功了;
事物B:(會發現修改表結構成功)

mysql> alter table test add sex char;
Query OK, 0 rows affected (7 min 38.42 sec)
Records: 0  Duplicates: 0  Warnings: 0

2. 解決方法

從上面的案例可以看到,假如事物A儘早的提交,也就不會造成阻塞的連鎖反應,所以要解決上面的問題,首先要解決長事物; 事物一直不提交,就會一直佔着MDL鎖;

要查看當前有哪些事物正在執行,可以進入MySQL的information_schema庫,查看其中的innodb_trx表;
如果你要做DDL變更的表正好有長事物在執行,你就考慮先暫停DDL,或者kill掉這個長事物;

新問題:
如果你要變更的表是一個熱點表,雖然數據量不大,但是上面的請求又很頻繁,而你又不得不加個字段,這時候該怎麼做呢?

  • 這時候kill掉就未必有用了,因爲新的請求又會馬上到來,所以這時比較理想的機制是,在alter table語句裏面設定等待時間,如果在這個指定時間內能夠拿到MDL寫鎖最好,拿不到也不要阻塞後面的業務語句,先放棄進行表結構更改;之後開發人員或者DBA再通過重試命令重複這個過程;

    • MariaDB已經合併了AliSQL這個功能,所以這兩個開源分支都支持 DDL NOWAIT/WAIT N 這個語法;

      alter table test nowait drop sex;
      alter table test wait n drop sex;
      

3. online ddl(ddl:更改表結構)

在MySQL5.6支持online ddl,這是什麼意思呢,下面來看看online ddl的步驟:

  • 拿MDL寫鎖
  • 降級成MDL讀鎖
  • 真正做DDL
  • 升級成MDL寫鎖
  • 釋放MDL鎖

對於上面的案例,是不是用online ddl就不會出現那樣的情況了呢?
答案是:還會出現那樣的情況,因爲上面的案例是我們在第一步就阻塞了,也就是根本還沒有拿到MDL寫鎖呢,要等事物一提交了才能拿到,所以阻塞於此,online ddl做到的優化只是在真正拿到MDL寫鎖後,可以讓讀也能同時進行

3. 行鎖

行鎖是在引擎層由各個引擎自己實現的,有的引擎並不支持行鎖,比如MyISAM就不支持行鎖,這意味着:

  • 併發控制只能使用表鎖,對於這種引擎(MyISAM)的表,同一張表上任何時刻只能有一個更新在執行,這嚴重影響了併發度
  • InnoDB是支持行鎖的,這也是MyISAM被InnoDB代替的主要原因;

1. 行鎖特性

首先注意:InnoDB的行鎖是針對索引加的鎖,不是針對記錄加的鎖,並且該索引不能失效,否則都會從行鎖升級爲表鎖; (⭐)

下面來看一個例子:
開啓兩個cdm窗口,啓動兩個事物A、B,在事物A中更改表中的一行數據,此時未提交事物A,再在事物B中查詢該表,會發現查詢的結果是A未修改的結果,也就是事物A還沒提交,它對錶test的更新對B不可見

事物A:

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

mysql> update test set name='WangWu' where id=1;
Query OK, 1 row affected (0.04 sec)
Rows matched: 1  Changed: 1  Warnings: 0

事物B:(可以看到還是原來的數據)

mysql> select * from test;
+----+---------+-----------+
| id | name    | adress    |
+----+---------+-----------+
|  1 | zangsan | ChongQing |
|  2 | lch     | XiAn      |
|  3 | yyg     | zhongxian |
|  4 | ygz     | zhongxian |
|  5 | zsf     | zhongxian |
+----+---------+-----------+
5 rows in set (0.02 sec)

此時再做一個實驗,(事物A還未提交),在事物B中更改其他行,看是否能成功:

mysql> update test set name='LiSi' where id=2;
Query OK, 1 row affected (0.11 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from test;
+----+---------+-----------+
| id | name    | adress    |
+----+---------+-----------+
|  1 | zangsan | ChongQing |
|  2 | LiSi    | XiAn      |
|  3 | yyg     | zhongxian |
|  4 | ygz     | zhongxian |
|  5 | zsf     | zhongxian |
+----+---------+-----------+
5 rows in set (0.00 sec)

mysql> update test set name='LiSi' where id=1;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

可以看到,在事物A未提交的情況下:

  • B中不能更新A中更新的那一行(會受到阻塞,一定時間如果還沒獲取到行鎖會自動放棄更新),其他行都能更新;
  • 當A一提交,B中更新A中更新的那一行就會不再阻塞,執行完畢;

爲驗證行鎖是建立在索引之上的,我們在在事物A中不用id更新test表,如下:
(id是主鍵,所以是有索引的)

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

mysql> update test set name='TaoLiu' where name='ygz';
Query OK, 1 row affected (0.02 sec)
Rows matched: 1  Changed: 1  Warnings: 0

此時在事物B中更新另外一行:

mysql> update test set name='ZangLiu' where id=2;

發現阻塞於此,沒有更新同一行啊,爲什麼會被鎖住?
因爲這裏事物A中的更新沒有基於索引(name沒加索引),所以這裏由行鎖會降級成表鎖,所以在事物B中不能對該表進行任何更新,只能讀;

2. 兩階段鎖協議

在InnoDB事物中,行鎖是在需要的時候才加上的,但並不是不需要了就立刻釋放,而是要等到事物提交了纔會釋放,這個就是兩階段鎖協議;

知道了這個協議後,對我們的某些開發會得到效率提升,比如:

  • 如果你的事物中需要鎖多個行,要把最可能造成鎖衝突、最可能影響併發度的鎖儘量往後放

4. 死鎖

死鎖這個名詞相信大家都不陌生,同樣數據庫也會有死鎖的出現,這裏舉一個例子(以行鎖導致的死鎖爲例):
在這裏插入圖片描述
如上圖,當執行完事物B的update最後一句時,回車出現如下:

ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

1. 處理死鎖策略

  • (1):直接進入等待,直到超時,這個超時時間可以通過參數innodb_lock_wait_timeout來進行設置,如下:(InnoDB中查看這個參數默認是50秒

    mysql> show variables like 'innodb_lock_wait_timeout';
    +--------------------------+-------+
    | Variable_name            | Value |
    +--------------------------+-------+
    | innodb_lock_wait_timeout | 50    |
    +--------------------------+-------+
    1 row in set, 1 warning (0.00 sec)
    

    設置的話,語句是:

    mysql> set innodb_lock_wait_timeout = 50;
    Query OK, 0 rows affected (0.00 sec)
    
  • 2)發起死鎖檢測,發現死鎖後,主動回滾死鎖鏈條中的某一個事物,讓其他事物得以執行;
    將參數innodb_deadlock_detect設置爲on,就代表開啓; (InnoDB默認開啓死鎖檢測)

    mysql> show variables like 'innodb_deadlock_detect';
    +------------------------+-------+
    | Variable_name          | Value |
    +------------------------+-------+
    | innodb_deadlock_detect | ON    |
    +------------------------+-------+
    1 row in set, 1 warning (0.04 sec)
    

    設置語句爲:

    mysql> set global innodb_deadlock_detect = on;
    Query OK, 0 rows affected (0.05 sec)
    
  • 兩種方案區別:

    • 第一種等待50s,這顯然對於在線服務起來說是等不起的,時間設置太短又會造成誤判;
    • 所以一般採用第二種:死鎖檢測,InnoDB本身默認就是將那個參數設置爲on的,但這種方式也是有弊端的,畢竟死鎖檢測需要消耗資源,具體詳細下面來講;

2. 死鎖檢測(⭐)

上面講了,死鎖檢測是數據庫檢驗死鎖的一個策略,檢測所需要的代價就是:

  • 每當一個事物被鎖住的時候,就要看看它所依賴的線程有沒有被別人鎖住,如此循環,最後判斷是否出現了循環等待,即死鎖;

進行死鎖檢測的條件:

  • 當前事物需要加鎖訪問的行上被別人鎖住時,纔會進行死鎖檢測

注意點:

  • 一致性讀的時候不會加鎖,所以不用死鎖檢測
  • 並不是每次死鎖檢測都要掃描所有的事物,比如下面這種情況:
    B在等A
    D在等C
    現在事物E來了,發現E需要等D,則此時E需要判斷跟D、C是否成環(形成死鎖),並不會去檢測B和A,因爲他們訪問的肯定不是同一個資源;

3. 典型案例(CPU利用率高,但效率低的場景)

那麼假如出現這樣一個場景:
一千個事物要同時更新test表中的同一行數據,這時其實並不會發生死鎖,但會發現效率極低,這是爲什麼呢
因爲每一個被(行鎖)堵住的線程都會去判斷是不是由於自己的加入導致了死鎖,這是一個時間複雜度爲O(n)的操作,一千個事物,此時時間複雜度高達100萬這個數量級,雖然最終檢測沒有死鎖,但是期間消耗了大量的CPU資源,所以你將會看到,CPU利用率很高,但是卻執行不了幾個事物;

那麼怎麼解決上面這種問題呢?

  • (1)如果你確認你的操作中不會出現死鎖,就關閉死鎖檢測;當然這種方法風險是很大的,畢竟死鎖的出現不是我們能預估的,一旦出現,就會造成超時等待;
  • (2)控制併發度;
    • 在上面的例子中,是同時有大量線程去更新同一行導致的,假如把併發度降到很低就不會出現時間複雜度過大的死鎖檢測了,具體做法就是對於更新相同行數據的線程,在進入引擎前排隊,這樣在InnoDB裏面就不會同時有大量的死鎖檢測工作了;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章