1. 程式人生 > >MySQL表鎖和行鎖

MySQL表鎖和行鎖

意向鎖 adl 固定 serializa 個數 讀一行 mvcc 不兼容 就是

鎖粒度

MySQL 不同的存儲引擎支持不同的鎖機制,所有的存儲引擎都以自己的方式顯現了鎖機制,服務器層完全不了解存儲引擎中的鎖實現:

  • InnoDB 存儲引擎既支持行級鎖(row-level locking),也支持表級鎖,但默認情況下是采用行級鎖。
  • MyISAM 和 MEMORY 存儲引擎采用的是表級鎖(table-level locking)
  • BDB 存儲引擎采用的是頁面鎖(page-level locking),但也支持表級鎖

默認情況下,表鎖和行鎖都是自動獲得的, 不需要額外的命令。

但是在有的情況下, 用戶需要明確地進行鎖表或者進行事務的控制, 以便確保整個事務的完整性,這樣就需要使用事務控制和鎖定語句來完成。

不同粒度鎖的比較

表級鎖

開銷小,加鎖快;不會出現死鎖;鎖定粒度大,發生鎖沖突的概率最高,並發度最低。

存儲引擎通過總是一次性同時獲取所有需要的鎖以及總是按相同的順序獲取表鎖來避免死鎖。

表級鎖更適合於以查詢為主,並發用戶少,只有少量按索引條件更新數據的應用,如Web 應用

行級鎖

開銷大,加鎖慢;會出現死鎖;鎖定粒度最小,發生鎖沖突的概率最低,並發度也最高。

最大程度的支持並發,同時也帶來了最大的鎖開銷。

在 InnoDB 中,除單個 SQL 組成的事務外,鎖是逐步獲得的,這就決定了在 InnoDB 中發生死鎖是可能的。

行級鎖只在存儲引擎層實現,而Mysql服務器層沒有實現。 行級鎖更適合於有大量按索引條件並發更新少量不同數據,同時又有並發查詢的應用,如一些在線事務處理(OLTP)系統

頁面鎖

開銷和加鎖時間界於表鎖和行鎖之間;會出現死鎖;鎖定粒度界於表鎖和行鎖之間,並發度一般。

MyISAM 表鎖

MyISAM表級鎖模式

  • 表共享讀鎖 (Table Read Lock):不會阻塞其他用戶對同一表的讀請求,但會阻塞對同一表的寫請求;
  • 表獨占寫鎖 (Table Write Lock):會阻塞其他用戶對同一表的讀和寫操作;

MyISAM 表的讀操作與寫操作之間,以及寫操作之間是串行的。當一個線程獲得對一個表的寫鎖後, 只有持有鎖的線程可以對表進行更新操作。 其他線程的讀、 寫操作都會等待,直到鎖被釋放為止。

默認情況下,寫鎖比讀鎖具有更高的優先級:當一個鎖釋放時,這個鎖會優先給寫鎖隊列中等候的獲取鎖請求,然後再給讀鎖隊列中等候的獲取鎖請求。

這也正是 MyISAM 表不太適合於有大量更新操作和查詢操作應用的原因,因為,大量的更新操作會造成查詢操作很難獲得讀鎖,從而可能永遠阻塞。同時,一些需要長時間運行的查詢操作,也會使寫線程“餓死” ,應用中應盡量避免出現長時間運行的查詢操作(在可能的情況下可以通過使用中間表等措施對SQL語句做一定的“分解” ,使每一步查詢都能在較短時間完成,從而減少鎖沖突。如果復雜查詢不可避免,應盡量安排在數據庫空閑時段執行,比如一些定期統計可以安排在夜間執行)。

可以調整設置改變讀鎖和寫鎖的優先級。

MyISAM加表鎖方法

MyISAM 在執行查詢語句(SELECT)前,會自動給涉及的表加讀鎖,在執行更新操作(UPDATE、DELETE、INSERT 等)前,會自動給涉及的表加寫鎖,這個過程並不需要用戶幹預,因此,用戶一般不需要直接用 LOCK TABLE 命令給 MyISAM 表顯式加鎖。

在自動加鎖的情況下,MyISAM 總是一次獲得 SQL 語句所需要的全部鎖,因此 MyISAM 表不會出現死鎖(Deadlock Free)。

查詢表級鎖爭用情況

可以通過檢查 table_locks_waited 和 table_locks_immediate 狀態變量來分析系統上的表鎖的爭奪,如果 Table_locks_waited 的值比較高,則說明存在著較嚴重的表級鎖爭用情況:

mysql> SHOW STATUS LIKE Table%;
+-----------------------+---------+
| Variable_name | Value |
+-----------------------+---------+
| Table_locks_immediate | 1151552 |
| Table_locks_waited | 15324 |
+-----------------------+---------+

InnoDB行級鎖和表級鎖

InnoDB鎖模式

InnoDB 實現了以下兩種類型的行鎖:

  • 共享鎖(S):允許一個事務去讀一行,阻止其他事務獲得相同數據集的排他鎖。
  • 排他鎖(X):允許獲得排他鎖的事務更新數據,阻止其他事務取得相同數據集的共享讀鎖和排他寫鎖。

為了允許行鎖和表鎖共存,實現多粒度鎖機制,InnoDB 還有兩種內部使用的意向鎖(Intention Locks),這兩種意向鎖都是表鎖:

  • 意向共享鎖(IS):事務打算給數據行加行共享鎖,事務在給一個數據行加共享鎖前必須先取得該表的 IS 鎖。
  • 意向排他鎖(IX):事務打算給數據行加行排他鎖,事務在給一個數據行加排他鎖前必須先取得該表的 IX 鎖。

鎖模式的兼容情況:

技術分享圖片
(如果一個事務請求的鎖模式與當前的鎖兼容, InnoDB 就將請求的鎖授予該事務; 反之, 如果兩者不兼容,該事務就要等待鎖釋放。)

InnoDB加鎖方法

意向鎖是 InnoDB 自動加的, 不需用戶幹預。

對於 UPDATE、 DELETE 和 INSERT 語句, InnoDB會自動給涉及數據集加排他鎖(X);

對於普通 SELECT 語句,InnoDB 不會加任何鎖;

事務可以通過以下語句顯式給記錄集加共享鎖或排他鎖:

  • 共享鎖(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE。 其他 session 仍然可以查詢記錄,並也可以對該記錄加 share mode 的共享鎖。但是如果當前事務需要對該記錄進行更新操作,則很有可能造成死鎖。
  • 排他鎖(X):SELECT * FROM table_name WHERE ... FOR UPDATE。其他 session 可以查詢該記錄,但是不能對該記錄加共享鎖或排他鎖,而是等待獲得鎖。

隱式鎖定

InnoDB在事務執行過程中,使用兩階段鎖協議:

隨時都可以執行鎖定,InnoDB會根據隔離級別在需要的時候自動加鎖;

鎖只有在執行commit或者rollback的時候才會釋放,並且所有的鎖都是在同一時刻被釋放。

顯式鎖定

select ... lock in share mode //共享鎖 
select ... for update //排他鎖

select for update:

在執行這個 select 查詢語句的時候,會將對應的索引訪問條目進行上排他鎖(X 鎖),也就是說這個語句對應的鎖就相當於update帶來的效果。

select *** for update 的使用場景:為了讓自己查到的數據確保是最新數據,並且查到後的數據只允許自己來修改的時候,需要用到 for update 子句。

select lock in share mode :

in share mode 子句的作用就是將查找到的數據加上一個 share 鎖,這個就是表示其他的事務只能對這些數據進行簡單的select 操作,並不能夠進行 DML 操作。

select *** lock in share mode的使用場景:為了確保自己查到的數據沒有被其他的事務正在修改,也就是說確保查到的數據是最新的數據,並且不允許其他人來修改數據。但是自己不一定能夠修改數據,因為有可能其他的事務也對這些數據 使用了 in share mode 的方式上了 S 鎖。

性能影響

select for update 語句,相當於一個 update 語句。在業務繁忙的情況下,如果事務沒有及時的commit或者rollback 可能會造成其他事務長時間的等待,從而影響數據庫的並發使用效率。

select lock in share mode 語句是一個給查找的數據上一個共享鎖(S 鎖)的功能,它允許其他的事務也對該數據上S鎖,但是不能夠允許對該數據進行修改。如果不及時的commit 或者rollback 也可能會造成大量的事務等待。

for update 和 lock in share mode 的區別:

前一個上的是排他鎖(X 鎖),一旦一個事務獲取了這個鎖,其他的事務是沒法在這些數據上執行 for update ;後一個是共享鎖,多個事務可以同時的對相同數據執行 lock in share mode。

InnoDB 行鎖實現方式

InnoDB 行鎖是通過給索引上的索引項加鎖來實現的,這一點 MySQL 與 Oracle 不同,後者是通過在數據塊中對相應數據行加鎖來實現的。InnoDB 這種行鎖實現特點意味著:只有通過索引條件檢索數據,InnoDB 才使用行級鎖,否則,InnoDB 將使用表鎖!

不論是使用主鍵索引、唯一索引或普通索引,InnoDB 都會使用行鎖來對數據加鎖。

只有執行計劃真正使用了索引,才能使用行鎖:即便在條件中使用了索引字段,但是否使用索引來檢索數據是由 MySQL 通過判斷不同執行計劃的代價來決定的,如果 MySQL 認為全表掃描效率更高,比如對一些很小的表,它就不會使用索引,這種情況下 InnoDB 將使用表鎖,而不是行鎖。因此,在分析鎖沖突時,別忘了檢查 SQL 的執行計劃(可以通過 explain 檢查 SQL 的執行計劃),以確認是否真正使用了索引。(更多閱讀:MySQL索引總結)

由於 MySQL 的行鎖是針對索引加的鎖,不是針對記錄加的鎖,所以雖然多個session是訪問不同行的記錄, 但是如果是使用相同的索引鍵, 是會出現鎖沖突的(後使用這些索引的session需要等待先使用索引的session釋放鎖後,才能獲取鎖)。 應用設計的時候要註意這一點。

InnoDB的間隙鎖

當我們用範圍條件而不是相等條件檢索數據,並請求共享或排他鎖時,InnoDB會給符合條件的已有數據記錄的索引項加鎖;對於鍵值在條件範圍內但並不存在的記錄,叫做“間隙(GAP)”,InnoDB也會對這個“間隙”加鎖,這種鎖機制就是所謂的間隙鎖(Next-Key鎖)。

很顯然,在使用範圍條件檢索並鎖定記錄時,InnoDB這種加鎖機制會阻塞符合條件範圍內鍵值的並發插入,這往往會造成嚴重的鎖等待。因此,在實際應用開發中,尤其是並發插入比較多的應用,我們要盡量優化業務邏輯,盡量使用相等條件來訪問更新數據,避免使用範圍條件。

InnoDB使用間隙鎖的目的

  • 防止幻讀,以滿足相關隔離級別的要求;
  • 滿足恢復和復制的需要:

MySQL 通過 BINLOG 錄入執行成功的 INSERT、UPDATE、DELETE 等更新數據的 SQL 語句,並由此實現 MySQL 數據庫的恢復和主從復制。MySQL 的恢復機制(復制其實就是在 Slave Mysql 不斷做基於 BINLOG 的恢復)有以下特點:

  • 一是 MySQL 的恢復是 SQL 語句級的,也就是重新執行 BINLOG 中的 SQL 語句。
  • 二是 MySQL 的 Binlog 是按照事務提交的先後順序記錄的, 恢復也是按這個順序進行的。

由此可見,MySQL 的恢復機制要求:在一個事務未提交前,其他並發事務不能插入滿足其鎖定條件的任何記錄,也就是不允許出現幻讀。

InnoDB 在不同隔離級別下的一致性讀及鎖的差異

鎖和多版本數據(MVCC)是 InnoDB 實現一致性讀和 ISO/ANSI SQL92 隔離級別的手段。

因此,在不同的隔離級別下,InnoDB 處理 SQL 時采用的一致性讀策略和需要的鎖是不同的:

技術分享圖片

技術分享圖片

對於許多 SQL,隔離級別越高,InnoDB 給記錄集加的鎖就越嚴格(尤其是使用範圍條件的時候),產生鎖沖突的可能性也就越高,從而對並發性事務處理性能的 影響也就越大。

因此, 我們在應用中, 應該盡量使用較低的隔離級別, 以減少鎖爭用的機率。實際上,通過優化事務邏輯,大部分應用使用 Read Commited 隔離級別就足夠了。對於一些確實需要更高隔離級別的事務, 可以通過在程序中執行 SET SESSION TRANSACTION ISOLATION

LEVEL REPEATABLE READ 或 SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE 動態改變隔離級別的方式滿足需求。

獲取 InnoDB 行鎖爭用情況

可以通過檢查 InnoDB_row_lock 狀態變量來分析系統上的行鎖的爭奪情況:

mysql> show status like innodb_row_lock%; 
+-------------------------------+-------+ 
| Variable_name | Value | 
+-------------------------------+-------+ 
| InnoDB_row_lock_current_waits | 0 | 
| InnoDB_row_lock_time | 0 | 
| InnoDB_row_lock_time_avg | 0 | 
| InnoDB_row_lock_time_max | 0 | 
| InnoDB_row_lock_waits | 0 | 
+-------------------------------+-------+ 
5 rows in set (0.01 sec)

LOCK TABLES 和 UNLOCK TABLES

Mysql也支持lock tables和unlock tables,這都是在服務器層(MySQL Server層)實現的,和存儲引擎無關,它們有自己的用途,並不能替代事務處理。 (除了禁用了autocommint後可以使用,其他情況不建議使用):

  • LOCK TABLES 可以鎖定用於當前線程的表。如果表被其他線程鎖定,則當前線程會等待,直到可以獲取所有鎖定為止。
  • UNLOCK TABLES 可以釋放當前線程獲得的任何鎖定。當前線程執行另一個 LOCK TABLES 時,或當與服務器的連接被關閉時,所有由當前線程鎖定的表被隱含地解鎖

LOCK TABLES語法

  • 在用 LOCK TABLES 對 InnoDB 表加鎖時要註意,要將 AUTOCOMMIT 設為 0,否則MySQL 不會給表加鎖;
  • 事務結束前,不要用 UNLOCK TABLES 釋放表鎖,因為 UNLOCK TABLES會隱含地提交事務;
  • COMMIT 或 ROLLBACK 並不能釋放用 LOCK TABLES 加的表級鎖,必須用UNLOCK TABLES 釋放表鎖。

正確的方式見如下語句:

例如,如果需要寫表 t1 並從表 t 讀,可以按如下做:

SET AUTOCOMMIT=0; 
LOCK TABLES t1 WRITE, t2 READ, ...; 
[do something with tables t1 and t2 here]; 
COMMIT; 
UNLOCK TABLES;

使用LOCK TABLES的場景

給表顯示加表級鎖(InnoDB表和MyISAM都可以),一般是為了在一定程度模擬事務操作,實現對某一時間點多個表的一致性讀取。(與MyISAM默認的表鎖行為類似)

在用 LOCK TABLES 給表顯式加表鎖時,必須同時取得所有涉及到表的鎖,並且 MySQL 不支持鎖升級。也就是說,在執行 LOCK TABLES 後,只能訪問顯式加鎖的這些表,不能訪問未加鎖的表;同時,如果加的是讀鎖,那麽只能執行查詢操作,而不能執行更新操作。

其實,在MyISAM自動加鎖(表鎖)的情況下也大致如此,MyISAM 總是一次獲得 SQL 語句所需要的全部鎖,這也正是 MyISAM 表不會出現死鎖(Deadlock Free)的原因。

例如,有一個訂單表 orders,其中記錄有各訂單的總金額 total,同時還有一個 訂單明細表 order_detail,其中記錄有各訂單每一產品的金額小計 subtotal,假設我們需要檢 查這兩個表的金額合計是否相符,可能就需要執行如下兩條 SQL:

Select sum(total) from orders; 
Select sum(subtotal) from order_detail;


這時,如果不先給兩個表加鎖,就可能產生錯誤的結果,因為第一條語句執行過程中,
order_detail 表可能已經發生了改變。因此,正確的方法應該是:

Lock tables orders read local, order_detail read local; 
Select sum(total) from orders; 
Select sum(subtotal) from order_detail; 
Unlock tables;

(在 LOCK TABLES 時加了“local”選項,其作用就是允許當你持有表的讀鎖時,其他用戶可以在滿足 MyISAM 表並發插入條件的情況下,在表尾並發插入記錄(MyISAM 存儲引擎支持“並發插入”))

一些優化鎖性能的建議

  1. 盡量使用較低的隔離級別;
  2. 精心設計索引, 並盡量使用索引訪問數據, 使加鎖更精確, 從而減少鎖沖突的機會
  3. 選擇合理的事務大小,小事務發生鎖沖突的幾率也更小
  4. 給記錄集顯示加鎖時,最好一次性請求足夠級別的鎖。比如要修改數據的話,最好直接申請排他鎖,而不是先申請共享鎖,修改時再請求排他鎖,這樣容易產生死鎖
  5. 不同的程序訪問一組表時,應盡量約定以相同的順序訪問各表,對一個表而言,盡可能以固定的順序存取表中的行。這樣可以大大減少死鎖的機會
  6. 盡量用相等條件訪問數據,這樣可以避免間隙鎖對並發插入的影響
  7. 不要申請超過實際需要的鎖級別
  8. 除非必須,查詢時不要顯示加鎖。 MySQL的MVCC可以實現事務中的查詢不用加鎖,優化事務性能;MVCC只在COMMITTED READ(讀提交)和REPEATABLE READ(可重復讀)兩種隔離級別下工作
  9. 對於一些特定的事務,可以使用表鎖來提高處理速度或減少死鎖的可能

其他

來源:https://zhuanlan.zhihu.com/p/29150809

######################

MySQL表鎖和行鎖