【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_version3. UPDATE SET data = new_data, version = new_version WHERE version = old_versionif (updated row > 0) { // 樂觀鎖獲取成功,操作完成} else { // 樂觀鎖獲取失敗,回滾并重試}
樂觀鎖是否在事務中其實都是無所謂的,其底層機制是這樣:在數據庫內部update同一行的時候是不允許并發的,即數據庫每次執行一條update語句時會獲取被update行的寫鎖,直到這一行被成功更新后才釋放。因此在業務操作進行前獲取需要鎖的數據的當前版本號,然后實際更新數據時再次對比版本號確認與之前獲取的相同,并更新版本號,即可確認這之間沒有發生并發的修改。如果更新失敗即可認為老版本的數據已經被并發修改掉而不存在了,此時認為獲取鎖失敗,需要回滾整個業務操作并可根據需要重試整個過程。
總結
樂觀鎖在不發生取鎖失敗的情況下開銷比悲觀鎖小,但是一旦發生失敗回滾開銷則比較大,因此適合用在取鎖失敗概率比較小的場景,可以提升系統并發性能
樂觀鎖還適用于一些比較特殊的場景,例如在業務操作過程中無法和數據庫保持連接等悲觀鎖無法適用的地方
參考資料 Internal Locking Methods
樂觀鎖 優缺點:
樂觀鎖:
樂觀鎖( Optimistic Locking ) 相對悲觀鎖而言,樂觀鎖機制采取了更加寬松的加鎖機制。悲觀鎖大多數情況下依靠數據庫的鎖機制實現,以保證操作最大程度的獨占性。但隨之而來的就是數據庫性能的大量開銷,特別是對長事務而言,這樣的開銷往往無法承受。而樂觀鎖機制在一定程度上解決了這個問題。樂觀鎖,大多是基于數據版本( Version )記錄機制實現。何謂數據版本?即為數據增加一個版本標識,在基于數據庫表的版本解決方案中,一般是通過為數據庫表增加一個 “version” 字段來實現。讀取出數據時,將此版本號一同讀出,之后更新時,對此版本號加一。更新數據時將讀出的版本號作為一個條件,如果數據庫表中版本號與此版本號相同則能更新成功,否則更新失敗。
例如一個金融系統,當某個操作員讀取用戶的數據,并在讀出的用戶數據的基礎上進行修改時(如更改用戶帳戶余額),如果采用悲觀鎖機制,也就意味著整個操作過 程中(從操作員讀出數據、開始修改直至提交修改結果的全過程,甚至還包括操作 員中途去煮咖啡的時間),數據庫記錄始終處于加鎖狀態,可以想見,如果面對幾百上千個并發,這樣的情況將導致怎樣的后果。
樂觀鎖機制在一定程度上解決了這個問題。樂觀鎖,大多是基于數據版本 ( Version )記錄機制實現。何謂數據版本?即為數據增加一個版本標識,在基于數據庫表的版本解決方案中,一般是通過為數據庫表增加一個 “version” 字段來實現(也可以采用另一種方式,同樣是在需要樂觀鎖控制的table中增加一個字段,名稱無所謂,字段類型使用時間戳timestamp, 和上面的version類似,也是在更新的時候檢查當前數據庫中數據的時間戳和自己更新前取到的時間戳進行對比,如果一致則OK,否則就是版本沖突)。
優點:
從上面的例子可以看出,樂觀鎖機制避免了長事務中的數據庫加鎖開銷,大大提升了大并發量下的系統整體性能表現。
缺點:
樂觀鎖機制往往基于系統中的數據存儲邏輯,因此也具備一定的局限性,如在上例中,由于樂觀鎖機制是在我們的系統中實現,來自外部系統的更新操作不受我們系統的控制,因此可能會造成臟數據被更新到數據庫中。在系統設計階段,應該充分考慮到這些情況出現的可能性,并進行相應調整(如將樂觀鎖策略在數據庫存儲過程中實現,對外只開放基于此存儲過程的數據更新途徑,而不是將數據庫表直接對外公開)。
使用實例:
MySQL中的隔離級別和悲觀鎖及樂觀鎖示例
1,MySQL的事務支持 MySQL的事務支持不是綁定在MySQL服務器本身,而是與存儲引擎相關:
MyISAM:不支持事務,用于只讀程序提高性能 InnoDB:支持ACID事務、行級鎖、并發 Berkeley DB:支持事務 2,隔離級別隔離級別決定了一個session中的事務可能對另一個session的影響、并發session對數據庫的操作、一個session中所見數據的一致性 ANSI標準定義了4個隔離級別,MySQL的InnoDB都支持:
java代碼
READ UNCOMMITTED:最低級別的隔離,通常又稱為dirty read,它允許一個事務讀取還沒commit的數據,這樣可能會提高性能,但是dirty read可能不是我們想要的 READ COMMITTED:在一個事務中只允許已經commit的記錄可見,如果session中select還在查詢中,另一session此時insert一條記錄,則新添加的數據不可見 REPEATABLE READ:在一個事務開始后,其他session對數據庫的修改在本事務中不可見,直到本事務commit或rollback。在一個事務中重復select的結果一樣,除非本事務中update數據庫。 SERIALIZABLE:最高級別的隔離,只允許事務串行執行。為了達到此目的,數據庫會鎖住每行已經讀取的記錄,其他session不能修改數據直到前一事務結束,事務commit或取消時才釋放鎖。 可以使用如下語句設置MySQL的session隔離級別:
[c-sharp] view plain copy1. SET TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE} MySQL默認的隔離級別是REPEATABLE READ,在設置隔離級別為READ UNCOMMITTED或SERIALIZABLE時要小心,READ UNCOMMITTED會導致數據完整性的嚴重問題,而SERIALIZABLE會導致性能問題并增加死鎖的機率
3,隔離級別樂觀所和悲觀鎖策略: 悲觀鎖:在讀取數據時鎖住那幾行,其他對這幾行的更新需要等到悲觀鎖結束時才能繼續 樂觀所:讀取數據時不鎖,更新時檢查是否數據已經被更新過,如果是則取消當前更新 一般在悲觀鎖的等待時間過長而不能接受時我們才會選擇樂觀鎖 悲觀鎖的例子:
[c-sharp] view plain copyCREATE 樂觀鎖的例子:
[c-sharp] view plain copyCREATE PROCEDURE tfer_funds (from_account INT, to_account INT, tfer_amount NUMERIC(10,2), OUT status INT, OUT message VARCHAR(30) ) BEGIN DECLARE from_account_balance NUMERIC(8,2); DECLARE from_account_balance2 NUMERIC(8,2); DECLARE from_account_timestamp1 TIMESTAMP; DECLARE from_account_timestamp2 TIMESTAMP; SELECT account_timestamp,balance INTO from_account_timestamp1,from_account_balance FROM account_balance WHERE account_id=from_account; IF (from_account_balance>=tfer_amount) THEN -- Here we perform some long running validation that -- might take a few minutes */ CALL long_running_validation(from_account); START TRANSACTION; -- Make sure the account row has not been updated since -- our initial check SELECT account_timestamp, balance INTO from_account_timestamp2,from_account_balance2 FROM account_balance WHERE account_id=from_account FOR UPDATE; IF (from_account_timestamp1 <> from_account_timestamp2 OR from_account_balance <> from_account_balance2) THEN ROLLBACK; SET status=-1; SET message=CONCAT("Transaction cancelled due to concurrent update", " of account" ,from_account); ELSE UPDATE account_balance SET balance=balance-tfer_amount WHERE account_id=from_account; UPDATE account_balance SET balance=balance+tfer_amount WHERE account_id=to_account; COMMIT; SET status=0; SET message="OK"; END IF; ELSE ROLLBACK; SET status=-1; SET message="Insufficient funds"; END IF; END$$
新聞熱點
疑難解答