1. 程式人生 > 資料庫 >詳解mysql 中的鎖結構

詳解mysql 中的鎖結構

Mysql 支援3中鎖結構

  • 表級鎖,開銷小,加鎖快,不會出現死鎖,鎖定的粒度大,衝突概率高,併發度最低
  • 行級鎖,加鎖慢,會出現死鎖,鎖定粒度小,衝突概率最低,併發度最高
  • 頁面鎖,開銷和加鎖處於表鎖和行鎖之間,鎖粒度基於表和行之間,併發一般

InnoDB鎖問題

InnoDB與MyISAM的最大不同有兩點:一是支援事務(TRANSACTION);二是採用了行級鎖。
行級鎖和表級鎖本來就有許多不同之處,另外,事務的引入也帶來了一些新問題。

InnoDB的行鎖模式及加鎖方法

InnoDB實現了以下兩種型別的行鎖。

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

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

  • 意向共享鎖(IS):事務打算給資料行共享鎖,事務在給一個數據行加共享鎖前必須先取得該表的IS鎖。
  • 意向排他鎖(IX):事務打算給資料行加排他鎖,事務在給一個數據行加排他鎖前必須先取得該表的IX鎖。
當前鎖模式和請求鎖模式 X IX S IS
X 衝突 衝突
IX 衝突 相容 衝突 相容
S 衝突 相容 相容
IS 衝突 相容 相容 相容

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

Next-Key鎖

當我們用範圍條件而不是相等條件檢索資料,並請求共享或排他鎖時,InnoDB會給符合條件的已有資料的索引項加鎖;對於鍵值在條件範圍內但並不存在的記錄,叫做“間隙(GAP)”,InnoDB也會對這個“間隙”加鎖,這種鎖機制不是所謂的間隙鎖(Next-Key鎖)。
舉例來說,假如emp表中只有101條記錄,其empid的值分別是1,2,...,100,101,下面的SQL:

SELECT * FROM emp WHERE empid > 100 FOR UPDATE

是一個範圍條件的檢索,InnoDB不僅會對符合條件的empid值為101的記錄加鎖,也會對empid大於101(這些記錄並不存在)的“間隙”加鎖。
InnoDB使用間隙鎖的目的,一方面是為了防止幻讀,以滿足相關隔離級別的要求,對於上面的例子,要是不使用間隙鎖,如果其他事務插入了empid大於100的任何記錄,那麼本事務如果再次執行上述語句,就會發生幻讀;另一方面,是為了滿足其恢復和複製的需要。有關其恢復和複製對機制的影響,以及不同隔離級別下InnoDB使用間隙鎖的情況。
很顯然,在使用範圍條件檢索並鎖定記錄時,InnoDB這種加鎖機制會阻塞符合條件範圍內鍵值的併發插入,這往往會造成嚴重的鎖等待。因此,在實際開發中,尤其是併發插入比較多的應用,我們要儘量優化業務邏輯,儘量使用相等條件來訪問更新資料,避免使用範圍條件。

什麼時候使用表鎖?

對於InnoDB表,在絕大部分情況下都應該使用行級鎖,因為事務和行鎖往往是我們之所以選擇InnoDB表的理由。但在個另特殊事務中,也可以考慮使用表級鎖。

第一種情況是:事務需要更新大部分或全部資料,表又比較大,如果使用預設的行鎖,不僅這個事務執行效率低,而且可能造成其他事務長時間鎖等待和鎖衝突,這種情況下可以考慮使用表鎖來提高該事務的執行速度。

第二種情況是:事務涉及多個表,比較複雜,很可能引起死鎖,造成大量事務回滾。這種情況也可以考慮一次性鎖定事務涉及的表,從而避免死鎖、減少資料庫因事務回滾帶來的開銷。

當然,應用中這兩種事務不能太多,否則,就應該考慮使用MyISAM表。
在InnoDB下 ,使用表鎖要注意以下兩點。

(1)使用LOCK TALBES雖然可以給InnoDB加表級鎖,但必須說明的是,表鎖不是由InnoDB儲存引擎層管理的,而是由其上一層MySQL Server負責的,僅當autocommit=0、innodb_table_lock=1(預設設定)時,InnoDB層才能知道MySQL加的表鎖,MySQL Server才能感知InnoDB加的行鎖,這種情況下,InnoDB才能自動識別涉及表級鎖的死鎖;否則,InnoDB將無法自動檢測並處理這種死鎖。

(2)在用LOCAK TABLES對InnoDB鎖時要注意,要將AUTOCOMMIT設為0,否則MySQL不會給表加鎖;事務結束前,不要用UNLOCAK TABLES釋放表鎖,因為UNLOCK TABLES會隱含地提交事務;COMMIT或ROLLBACK產不能釋放用LOCAK TABLES加的表級鎖,必須用UNLOCK TABLES釋放表鎖,正確的方式見如下語句。
例如,如果需要寫表t1並從表t讀,可以按如下做:

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

死鎖

在InnoDB中,除單個SQL組成的事務外,鎖是逐步獲得的,這就決定了InnoDB發生死鎖是可能的。
發生死鎖後,InnoDB一般都能自動檢測到,並使一個事務釋放鎖並退回,另一個事務獲得鎖,繼續完成事務。但在涉及外部鎖,或涉及鎖的情況下,InnoDB並不能完全自動檢測到死鎖,這需要通過設定鎖等待超時引數innodb_lock_wait_timeout來解決。需要說明的是,這個引數並不是只用來解決死鎖問題,在併發訪問比較高的情況下,如果大量事務因無法立即獲取所需的鎖而掛起,會佔用大量計算機資源,造成嚴重效能問題,甚至拖垮資料庫。我們通過設定合適的鎖等待超時閾值,可以避免這種情況發生。

通常來說,死鎖都是應用設計的問題,通過調整業務流程、資料庫物件設計、事務大小、以及訪問資料庫的SQL語句,絕大部分都可以避免。下面就通過例項來介紹幾種死鎖的常用方法。

(1)在應用中,如果不同的程式會併發存取多個表,應儘量約定以相同的順序為訪問表,這樣可以大大降低產生死鎖的機會。如果兩個session訪問兩個表的順序不同,發生死鎖的機會就非常高!但如果以相同的順序來訪問,死鎖就可能避免。

(2)在程式以批量方式處理資料的時候,如果事先對資料排序,保證每個執行緒按固定的順序來處理記錄,也可以大大降低死鎖的可能。

(3)在事務中,如果要更新記錄,應該直接申請足夠級別的鎖,即排他鎖,而不應該先申請共享鎖,更新時再申請排他鎖,甚至死鎖。

(4)在REPEATEABLE-READ隔離級別下,如果兩個執行緒同時對相同條件記錄用SELECT...ROR UPDATE加排他鎖,在沒有符合該記錄情況下,兩個執行緒都會加鎖成功。程式發現記錄尚不存在,就試圖插入一條新記錄,如果兩個執行緒都這麼做,就會出現死鎖。這種情況下,將隔離級別改成READ COMMITTED,就可以避免問題。

(5)當隔離級別為READ COMMITED時,如果兩個執行緒都先執行SELECT...FOR UPDATE,判斷是否存在符合條件的記錄,如果沒有,就插入記錄。此時,只有一個執行緒能插入成功,另一個執行緒會出現鎖等待,當第1個執行緒提交後,第2個執行緒會因主鍵重出錯,但雖然這個執行緒出錯了,卻會獲得一個排他鎖!這時如果有第3個執行緒又來申請排他鎖,也會出現死鎖。對於這種情況,可以直接做插入操作,然後再捕獲主鍵重異常,或者在遇到主鍵重錯誤時,總是執行ROLLBACK釋放獲得的排他鎖。

MyISAM 和 InnoDB 區別

對於MyISAM的表鎖,主要有以下幾點

(1)共享讀鎖(S)之間是相容的,但共享讀鎖(S)和排他寫鎖(X)之間,以及排他寫鎖之間(X)是互斥的,也就是說讀和寫是序列的。
(2)在一定條件下,MyISAM允許查詢和插入併發執行,我們可以利用這一點來解決應用中對同一表和插入的鎖爭用問題。
(3)MyISAM預設的鎖排程機制是寫優先,這並不一定適合所有應用,使用者可以通過設定LOW_PRIPORITY_UPDATES引數,或在INSERT、UPDATE、DELETE語句中指定LOW_PRIORITY選項來調節讀寫鎖的爭用。
(4)由於表鎖的鎖定粒度大,讀寫之間又是序列的,因此,如果更新操作較多,MyISAM表可能會出現嚴重的鎖等待,可以考慮採用InnoDB表來減少鎖衝突。

對於InnoDB表,主要有以下幾點

(1)InnoDB的行銷是基於索引實現的,如果不通過索引訪問資料,InnoDB會使用表鎖。
(2)InnoDB間隙鎖機制,以及InnoDB使用間隙鎖的原因。
(3)在不同的隔離級別下,InnoDB的鎖機制和一致性讀策略不同。
(4)MySQL的恢復和複製對InnoDB鎖機制和一致性讀策略也有較大影響。
(5)鎖衝突甚至死鎖很難完全避免。

在瞭解InnoDB的鎖特性後,使用者可以通過設計和SQL調整等措施減少鎖衝突和死鎖,包括:

  • 儘量使用較低的隔離級別
  • 精心設計索引,並儘量使用索引訪問資料,使加鎖更精確,從而減少鎖衝突的機會。
  • 選擇合理的事務大小,小事務發生鎖衝突的機率也更小。
  • 給記錄集顯示加鎖時,最好一次性請求足夠級別的鎖。比如要修改資料的話,最好直接申請排他鎖,而不是先申請共享鎖,修改時再請求排他鎖,這樣容易產生死鎖。
  • 不同的程式訪問一組表時,應儘量約定以相同的順序訪問各表,對一個表而言,儘可能以固定的順序存取表中的行。這樣可以大減少死鎖的機會。
  • 儘量用相等條件訪問資料,這樣可以避免間隙鎖對併發插入的影響。
  • 不要申請超過實際需要的鎖級別;除非必須,查詢時不要顯示加鎖。
  • 對於一些特定的事務,可以使用表鎖來提高處理速度或減少死鎖的可能

MySql樂觀鎖悲觀鎖

悲觀鎖

悲觀鎖的特點是先獲取鎖,再進行業務操作,即“悲觀”的認為獲取鎖是非常有可能失敗的,因此要先確保獲取鎖成功再進行業務操作。通常所說的“一鎖二查三更新”即指的是使用悲觀鎖。通常來講在資料庫上的悲觀鎖需要資料庫本身提供支援,即通過常用的select … for update操作來實現悲觀鎖。當資料庫執行select for update時會獲取被select中的資料行的行鎖,因此其他併發執行的select for update如果試圖選中同一行則會發生排斥(需要等待行鎖被釋放),因此達到鎖的效果。select for update獲取的行鎖會在當前事務結束時自動釋放,因此必須在事務中使用。

這裡需要注意的一點是不同的資料庫對select for update的實現和支援都是有所區別的,例如oracle支援select for update no wait,表示如果拿不到鎖立刻報錯,而不是等待,mysql就沒有no wait這個選項。另外mysql還有個問題是select for update語句執行中所有掃描過的行都會被鎖上,這一點很容易造成問題。因此如果在mysql中用悲觀鎖務必要確定走了索引,而不是全表掃描。

樂觀鎖

樂觀鎖的特點先進行業務操作,不到萬不得已不去拿鎖。即“樂觀”的認為拿鎖多半是會成功的,因此在進行完業務操作需要實際更新資料的最後一步再去拿一下鎖就好。

樂觀鎖在資料庫上的實現完全是邏輯的,不需要資料庫提供特殊的支援。一般的做法是在需要鎖的資料上增加一個版本號,或者時間戳,然後按照如下方式實現:

1. SELECT data AS old_data,version AS old_version FROM …;
2. 根據獲取的資料進行業務操作,得到new_data和new_version
3. UPDATE SET data = new_data,version = new_version WHERE version = old_version
if (updated row > 0) {
  // 樂觀鎖獲取成功,操作完成
} else {
  // 樂觀鎖獲取失敗,回滾並重試
}

在資料庫內部update同一行的時候是不允許併發的,即資料庫每次執行一條update語句時會獲取被update行的寫鎖,直到這一行被成功更新後才釋放。因此在業務操作進行前獲取需要鎖的資料的當前版本號,然後實際更新資料時再次對比版本號確認與之前獲取的相同,並更新版本號,即可確認這之間沒有發生併發的修改。如果更新失敗即可認為老版本的資料已經被併發修改掉而不存在了,此時認為獲取鎖失敗,需要回滾整個業務操作並可根據需要重試整個過程。

樂觀鎖在不發生取鎖失敗的情況下開銷比悲觀鎖小,但是一旦發生失敗回滾開銷則比較大,因此適合用在取鎖失敗概率比較小的場景,可以提升系統併發效能
樂觀鎖還適用於一些比較特殊的場景,例如在業務操作過程中無法和資料庫保持連線等悲觀鎖無法適用的地方

以上就是詳解mysql 中的鎖結構的詳細內容,更多關於MySQL 鎖結構的資料請關注我們其它相關文章!