悲觀鎖
當我們要對數據庫中的一條數據進行修改的時候,為了避免同時被其他人修改,最好的辦法就是直接對該數據進行加鎖以防止並發。這種借助數據庫鎖機制在修改數據之前鎖定,再修改的方式被稱為悲觀並發控制(PCC)。
之所以叫做悲觀鎖,是因為抱有悲觀的態度去修改數據的並發控制方式,認為數據並發修改的概率比較大,所以需要在修改之前先加鎖。
悲觀並發控制實際上是 “先取鎖,再訪問” 的保守策略,為數據處理的安全提供了保證。
在效率上,處理加鎖的機制會讓數據庫產生額外的開銷,還會有死鎖的可能性。降低並行性,一個事務如果鎖定了某行數據,其他事務就必須等待該事務處理完才可以處理那行數據。
悲觀鎖的實現方式:悲觀鎖的實現,依靠數據庫提供的鎖機制。在數據庫中,悲觀鎖的流程如下:
- 在對數據修改前,嘗試增加排他鎖。
- 加鎖失敗,意味着數據正在被修改,進行等待或者拋出異常。
- 加鎖成功,對數據進行修改,提交事務,鎖釋放。
- 如果我們加鎖成功,有其他線程對該數據進操作或者加排他鎖的操作,只能等待或者拋出異常。
樂觀鎖
樂觀鎖是相對悲觀鎖而言的,樂觀鎖假設數據一般情況下不會造成沖突,所以在數據進行提交更新的時候,才會正式對數據的沖突與否進行檢測。
相對於悲觀鎖,在數據庫進行處理的時候,樂觀鎖不會使用數據庫提供的鎖機制,一般是增加 version 參數,記錄數據版本
樂觀並發控制相信事務之間的數據競爭概率非常小,因此盡可能直接操作,提交的時候才去鎖定,不會產生任何鎖和死鎖。
上手試一試
基於 MySQL InnoDB 引擎
使用悲觀鎖
begin; select quantity from products where id = 1 for update; update products set quanntity = 2 where id = 1; commit;
以上,對 id 為 1 的產品進行修改,先通過 for update 的方式進行加鎖,然后再修改。典型的悲觀鎖策略。
如果修改庫存的邏輯發生並發,同一時間只有一個線程可以開啟事務並獲得 id = 1 的鎖,其他事務必須等本次提交之后才能執行,這樣可以保證數據不被其他事務修改。
使用排他鎖會把數據鎖住,不過需要注意一些基本的鎖級別,MySQL InnoDB 默認行級鎖。行級鎖是基於索引的,如果一條 SQL 語句用不到索引是不會使用行級鎖,會使用表級鎖把整張表鎖住。
使用樂觀鎖
select quantity from products where id = 1 update products set quantity = 2 where id = 1 and quantity = 3
先查詢庫存表當前庫存數,然后更新的時候判斷數據表對應數據的 quantity 與第一次取出來的是否一致,一致則更新,否則認為是過期數據。
這樣實現有一個問題,線程 1 從數據庫取出 quantity 為 3,線程 2 也取出同一條數據的 quantity,進行操作,變成了 2,然后又進行某些操作 變成了 3,此時線程 1 進行更新操作成功。但是這個過程有問題。
引入 version 參數,樂觀鎖每次在執行數據修改的操作,都會帶上版本號,一旦版本號和數據的版本號一致就可以執行修改操作並對 version 執行 +1 操作,否則就執行失敗。
這樣實現也有一個問題,如果真的有高並發的時候,就只有一個線程可以修改成功,就會存在大量的失敗。
如果你的應用存在超高並發,這樣解決也不好,因為會總讓用戶感知到失敗。
嘗試減小樂觀鎖力度,最大程度提高吞吐。
update products set quantity = quantity - 1 where id = 1 and quantity - 1 > 0
使用這條 SQL 語句,在執行過程中,會在一次原子操作中查詢一遍 quantity 的值,並且減去 1。
簡述區別
- 樂觀鎖不是真的加鎖,效率高,但是要控制好鎖的力度。
- 悲觀鎖依賴數據庫鎖,效率低。
總結
無論是悲觀鎖還是樂觀鎖,都是人們定義出來的概念,可以認為是一種思想。
大家要記住鎖機制一定要在事務中才能生效哦。
以上是我對樂觀鎖與悲觀鎖一點基礎實踐,希望能和大家再深入了解了解。