1. 程式人生 > 實用技巧 >資料庫-鎖/樂觀/悲觀鎖/讀寫鎖/死鎖

資料庫-鎖/樂觀/悲觀鎖/讀寫鎖/死鎖

1. 鎖的定義

鎖是網路資料庫中的一個非常重要的概念,當多個使用者同時對資料庫併發操作時,會帶來資料不一致的問題,所以,鎖主要用於多使用者環境下保證資料庫完整性和一致性。

資料庫鎖出現的目的:處理併發問題

併發控制的主要採用的技術手段:樂觀鎖、悲觀鎖和時間戳

鎖分類

從資料庫系統角度分為三種:排他鎖、共享鎖、更新鎖。

從程式設計師角度分為兩種:一種是悲觀鎖,一種樂觀鎖

2. 樂觀鎖(Optimistic Lock)

顧名思義,就是很樂觀,每次去拿資料的時候都認為別人不會修改,所以,不會上鎖。但是在更新的時候會判斷一下在此期間別人有沒有更新這個資料,可以使用版本號等機制。

樂觀鎖( Optimistic Locking ): 相對悲觀鎖而言,樂觀鎖機制採取了更加寬鬆的加鎖機制。
悲觀鎖大多數情況下依靠資料庫的鎖機制實現,以保證操作最大程度的獨佔性。但隨之而來的就是資料庫效能的大量開銷,特別是對長事務而言,這樣的開銷往往無法承受。而樂觀鎖機制在一定程度上解決了這個問題。
樂觀鎖,大多是基於資料版本( Version )記錄機制實現。
資料版本:為資料增加一個版本標識,在基於資料庫表的版本解決方案中,一般是通過為資料庫表增加一個 “version” 欄位來實現。讀取出資料時,將此版本號一同讀出,之後更新時,對此版本號加一。此時,將提交資料的版本資料與資料庫表對應記錄的當前版本資訊進行比對,如果提交的資料版本號大於資料庫表當前版本號,則予以更新,否則認為是過期資料。

2.1 實現方式

CAS(比較與交換,Compare and swap) 是一種有名的無鎖演算法。無鎖程式設計,即不使用鎖的情況下實現多執行緒之間的變數同步,也就是在沒有執行緒被阻塞的情況下實現變數的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。實現非阻塞同步的方案稱為“無鎖程式設計演算法”( Non-blocking algorithm)。

樂觀鎖基本都是基於 CAS(Compare and swap)演算法來實現的。

CAS有3個運算元,記憶體值V,舊的預期值A,要修改的新值B。當且僅當預期值A和記憶體值V相同時,將記憶體值V修改為B,否則什麼都不做。整個CAS操作是一個原子操作,是不可分割的。

  • 版本號(version)

    在表中新增一個欄位:version,用於儲存版本號。獲取資料的時候同時獲取版本號,然後更新資料的時候用以下命令:updatexxx set version=version+1,… where … version="old version" and ....。這時候通過判斷返回結果的影響行數是否為0來判斷是否更新成功,更新失敗則說明有其他請求已經更新了資料了

  • 時間戳(使用資料庫的時間戳)

    和版本號一樣,只是通過時間戳來判斷。一般來說很多資料表都會有更新時間這一個欄位,通過這個欄位來判斷就不用再新增一個欄位了。

  • 待更新欄位

    如果沒有時間戳欄位,而且不想新增欄位,那可以考慮用待更新欄位來判斷,因為更新資料一般都會發生變化,那更新前可以拿要更新的欄位的舊值和資料庫的現值進行比對,沒有變化則更新。

  • 所有欄位

    資料表所有欄位都用來判斷。這種相當於就、不僅僅對某幾個欄位做加鎖了,而是對整個資料行加鎖,只要本行資料發生變化,就不進行更新。

2.3 優缺點

優點:

樂觀併發控制沒有實際加鎖,所以沒有額外開銷,也不錯出現死鎖問題,適用於讀多寫少的併發場景,因為沒有額外開銷,所以能極大提高資料庫的效能。

缺點:

樂觀併發控制不適合於寫多讀少的併發場景下,因為會出現很多的寫衝突,導致資料寫入要多次等待重試,在這種情況下,其開銷實際上是比悲觀鎖更高的。而且樂觀鎖的業務邏輯比悲觀鎖要更為複雜,業務邏輯上要考慮到失敗,等待重試的情況,而且也無法避免其他第三方系統對資料庫的直接修改的情況。

2.4 案例

通過增加version欄位進行版本控制實現樂觀鎖

Session1

Session2

  1. 兩個會話假設同時查詢資料庫記錄;
  2. 會話2還未更新資料,會話1先更新了資料結果;
  3. 會話2根據之前查詢出的version更新資料,會話1更新後version已更新,導致會話2 無法更新;

3. 悲觀鎖(Pessimistic Lock)

顧名思義,很悲觀,每次去拿資料的時候都認為別人會修改,所以每次在拿資料的時候都會上鎖,這樣別人拿這個資料就會block(阻塞),直到它拿鎖。

悲觀鎖(Pessimistic Lock):正如其名,具有強烈的獨佔和排他特性。它指的是對資料被外界(包括本系統當前的其他事務,以及來自外部系統的事務處理)修改持保守態度,因此,在整個資料處理過程中,將資料處於鎖定狀態。悲觀鎖的實現,往往依靠資料庫提供的鎖機制(也只有資料庫層提供的鎖機制才能真正保證資料訪問的排他性,否則,即使在本系統中實現了加鎖機制,也無法保證外部系統不會修改資料)。

傳統的關係資料庫裡用到了很多這種鎖機制,比如行鎖、表鎖、讀鎖、寫鎖等,都是在操作之前先上鎖

3.1 悲觀鎖按使用性質劃分

讀寫角度,分共享鎖(S鎖,Shared Lock)和排他鎖(X鎖,Exclusive Lock),也叫讀鎖(Read Lock)和寫鎖(Write Lock)。 持有S鎖的事務只讀不可寫。如果事務A對資料D加上S鎖後,其它事務只能對D加上S鎖而不能加X鎖。 持有X鎖的事務可讀可寫。如果事務A對資料D加上X鎖後,其它事務不能再對D加鎖,直到A對D的鎖解除。

3.1.1共享鎖(Share Lock)

共享鎖又稱為讀鎖,簡稱S鎖。

顧名思義,共享鎖就是多個事務對於同一資料可以共享一把鎖,都能訪問到資料,但是隻能讀不能修改。

示例:

SELECT * FROM user WHERE id = 2 LOCK IN SHARE MODE;

-- postgresql: SELECT ... FOR SHARE;

3.1.2 排他鎖(Exclusive Lock)

排他鎖又稱為寫鎖,簡稱X鎖。

顧名思義,排他鎖就是不能與其他所並存,如一個事務獲取了一個數據行的排他鎖,其他事務就不能再獲取該行的其他鎖,包括共享鎖和排他鎖,但是獲取排他鎖的事務是可以對資料就行讀取和修改。

示例:

session1:
-- 1、開啟事務 或使用begin
mysql> set autocommit = 0;
Query OK, 0 rows affected (0.00 sec)

-- 2、查詢資料並新增排他鎖
mysql> select * from user where id = 2 for update;
+----+------+---------+
| id | name | version |
+----+------+---------+
|  2 | b    |       4 |
+----+------+---------+
1 row in set (0.00 sec)

-- 2、或者是 更新資料
mysql> update user set name = 'c' where id = 2;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

-- 4、提交事務
mysql> commit;
Query OK, 0 rows affected (0.01 sec)

session2:
-- 3、查詢資料並新增排他鎖
mysql> select * from user where id = 2 for update;
-- 5、session1 提交事務後 查詢結果出來
+----+------+---------+
| id | name | version |
+----+------+---------+
|  2 | c    |       4 |
+----+------+---------+
1 row in set (44.71 sec)

3.2 悲觀鎖按作用範圍劃分

鎖的粒度角度,主要分為表級鎖(Table Lock)和行級鎖(Row Lock)。

表級鎖將整個表加鎖,效能開銷最小。使用者可以同時進行讀操作。當一個使用者對錶進行寫操作時,使用者可以獲得一個寫鎖,寫鎖禁止其他的使用者讀寫操作。寫鎖比讀鎖的優先順序更高,即使有讀操作已排在佇列中,一個被申請的寫鎖仍可以排在所佇列的前列。 行級鎖僅對指定的記錄進行加鎖,這樣其它程序可以對同一個表中的其它記錄進行讀寫操作。行級鎖粒度最小,開銷大,能夠支援高併發,可能會出現死鎖。

3.2.1 行鎖(row lock)

顧名思義,行鎖就是針對資料表中行記錄的鎖。這很好理解,比如事務A更新了一行,而這時侯事務B也要更新同一行,則必須等事務A的操作完成後才能進行更新。

顯示執行行鎖的兩種方式:

  • FOR UPDATE:使用FOR UPDATE子句持有的任何鎖都不允許其他事務讀取(使用FOR UPDATE子句)、更新或刪除行,直到事務被提交或回滾,釋放鎖為止。這基本上是一個排他/寫鎖。這基本上是一個排他/寫鎖。
  • LOCK IN SHARE MODE:使用lock IN SHARE MODE子句持有的任何鎖將允許其他事務讀取鎖定的行,但在事務提交或回滾並釋放鎖之前,不允許其他事務在該行上寫操作。這基本上是一個共享/讀鎖。

mysql中行鎖有以下特點:

    1. 行鎖必須有索引才能實現,否則會自動鎖全表,那麼就不是行鎖了。
    1. 兩個事務不能鎖同一個索引
    1. insert ,delete , update在事務中都會自動預設加上排它鎖。

3.2.2 表鎖(table lock)

表鎖就是表示對當前操作的整張表加鎖

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

給MyISAM表顯示加鎖,一般是為了一定程度模擬事務操作,實現對某一時間點多個表的一致性讀取。
例如,有一個訂單表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表併發插入條件的情況下,允許其他使用者在表尾插入記錄
  • 在用LOCKTABLES給表顯式加表鎖是時,必須同時取得所有涉及表的鎖,並且MySQL支援鎖升級。也就是說,在執行LOCK TABLES後,只能訪問顯式加鎖的這些表,不能訪問未加鎖的表;同時,如果加的是讀鎖,那麼只能執行查詢操作,而不能執行更新操作。其實,在自動加鎖的情況下也基本如此,MySQL問題一次獲得SQL語句所需要的全部鎖。這也正是MyISAM表不會出現死鎖(Deadlock Free)的原因

一個session使用LOCK TABLE 命令給表film_text加了讀鎖,這個session可以查詢鎖定表中的記錄,但更新或訪問其他表都會提示錯誤;同時,另外一個session可以查詢表中的記錄,但更新就會出現鎖等待。

當使用LOCK TABLE時,不僅需要一次鎖定用到的所有表,而且,同一個表在SQL語句中出現多少次,就要通過與SQL語句中相同的別名鎖多少次,否則也會出錯!

4. 死鎖

MyISAM表鎖是deadlock free的,這是因為MyISAM總是一次性獲得所需的全部鎖,要麼全部滿足,要麼等待,因此不會出現死鎖。但是在InnoDB中,除單個SQL組成的事務外,鎖是逐步獲得的,這就決定了InnoDB發生死鎖是可能的。

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

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

    1. 在應用中,如果不同的程式會併發存取多個表,應儘量約定以相同的順序為訪問表,這樣可以大大降低產生死鎖的機會。如果兩個session訪問兩個表的順序不同,發生死鎖的機會就非常高!但如果以相同的順序來訪問,死鎖就可能避免。
    1. 在程式以批量方式處理資料的時候,如果事先對資料排序,保證每個執行緒按固定的順序來處理記錄,也可以大大降低死鎖的可能。
    1. 在事務中,如果要更新記錄,應該直接申請足夠級別的鎖,即排他鎖,而不應該先申請共享鎖,更新時再申請排他鎖,甚至死鎖。
    1. 在REPEATEABLE-READ隔離級別下,如果兩個執行緒同時對相同條件記錄用SELECT...ROR UPDATE加排他鎖,在沒有符合該記錄情況下,兩個執行緒都會加鎖成功。程式發現記錄尚不存在,就試圖插入一條新記錄,如果兩個執行緒都這麼做,就會出現死鎖。這種情況下,將隔離級別改成READ COMMITTED,就可以避免問題。
    1. 當隔離級別為READ COMMITED時,如果兩個執行緒都先執行SELECT...FOR UPDATE,判斷是否存在符合條件的記錄,如果沒有,就插入記錄。此時,只有一個執行緒能插入成功,另一個執行緒會出現鎖等待,當第1個執行緒提交後,第2個執行緒會因主鍵重出錯,但雖然這個執行緒出錯了,卻會獲得一個排他鎖!這時如果有第3個執行緒又來申請排他鎖,也會出現死鎖。對於這種情況,可以直接做插入操作,然後再捕獲主鍵重異常,或者在遇到主鍵重錯誤時,總是執行ROLLBACK釋放獲得的排他鎖。

儘管通過上面的設計和優化等措施,可以大減少死鎖,但死鎖很難完全避免。因此,在程式設計中總是捕獲並處理死鎖異常是一個很好的程式設計習慣。

如果出現死鎖,可以用SHOW INNODB STATUS命令來確定最後一個死鎖產生的原因和改進措施。

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

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

什麼是死鎖,簡述死鎖發生的四個必要條件,如何避免死鎖

產生死鎖的原因主要是:
(1) 因為系統資源不足。
(2) 程序執行推進的順序不合適。
(3) 資源分配不當等。
如果系統資源充足,程序的資源請求都能夠得到滿足,死鎖出現的可能性就很低,否則
就會因爭奪有限的資源而陷入死鎖。其次,程序執行推進順序與速度不同,也可能產生死鎖。

產生死鎖的四個必要條件:
(1) 互斥條件:一個資源每次只能被一個程序使用。
(2) 請求與保持條件:一個程序因請求資源而阻塞時,對已獲得的資源保持不放。
(3) 不剝奪條件:程序已獲得的資源,在末使用完之前,不能強行剝奪。
(4) 迴圈等待條件:若干程序之間形成一種頭尾相接的迴圈等待資源關係。
這四個條件是死鎖的必要條件,只要系統發生死鎖,這些條件必然成立,而只要上述條件之
一不滿足,就不會發生死鎖。