java中樂觀鎖與悲觀鎖區(qū)別及使用場景分析
一、什么是樂觀鎖?什么是悲觀鎖
個人理解一句話概括就是,在應用層面會造成線程阻塞的是悲觀鎖,而不會造成線程阻塞的是樂觀鎖,為什么這么說會在后續(xù)的內(nèi)容中做詳細介紹。
1.1、悲觀鎖
悲觀鎖是一種基于悲觀態(tài)度的數(shù)據(jù)并發(fā)控制機制,用于防止數(shù)據(jù)沖突。它采取預防性的措施,在修改數(shù)據(jù)之前將其鎖定,并在操作完成后釋放鎖定,以確保數(shù)據(jù)的一致性和完整性。悲觀鎖通常用于并發(fā)環(huán)境下的數(shù)據(jù)庫系統(tǒng),是數(shù)據(jù)庫本身實現(xiàn)鎖機制的一種方式。
在悲觀鎖的機制下,當一個使用者要修改某個數(shù)據(jù)時,首先會嘗試獲取該數(shù)據(jù)的鎖。如果鎖已經(jīng)被其他使用者持有,則當前使用者會被阻塞,直到對應的鎖被釋放。這種悲觀的態(tài)度認為數(shù)據(jù)沖突是不可避免的,因此在修改數(shù)據(jù)之前先鎖定數(shù)據(jù),以防止沖突的發(fā)生。
在Java中,常見的悲觀鎖實現(xiàn)是使用
synchronized
關鍵字或ReentrantLock
類。這些鎖能夠確保同一時刻只有一個線程可以訪問被鎖定的代碼塊或資源,其他線程必須等待鎖釋放后才能繼續(xù)執(zhí)行。
1.2、樂觀鎖
樂觀鎖是一種基于版本控制的并發(fā)控制機制。在樂觀鎖的思想中,認為數(shù)據(jù)訪問沖突的概率很低,因此不加鎖直接進行操作,但在更新數(shù)據(jù)時會進行版本比對,以確保數(shù)據(jù)的一致性。
樂觀鎖的原理主要基于版本號或時間戳來實現(xiàn)。在每次更新數(shù)據(jù)時,先獲取當前數(shù)據(jù)的版本號或時間戳,然后在更新時比對版本號或時間戳是否一致,若一致則更新成功,否則表示數(shù)據(jù)已被其他線程修改,更新失敗。
在Java中,常見的樂觀鎖實現(xiàn)是使用
Atomic
類,例如AtomicInteger
、AtomicLong
等。這些類提供了原子操作,可以確保對共享資源的更新操作是原子性的,從而避免了鎖的開銷和線程等待,另外,CAS(Compare-And-Swap)
是實現(xiàn)樂觀鎖的核心算法,它通過比較內(nèi)存中的值是否和預期的值相等來判斷是否存在沖突。如果存在,則返回失?。蝗绻淮嬖?,則執(zhí)行更新操作。Java中提供了AtomicInteger
、AtomicLong
、AtomicReference
等原子類來支持CAS操作。
二、樂觀鎖與悲觀鎖分別適用于什么場景
2.1、悲觀鎖適用場景
- 高并發(fā)且數(shù)據(jù)競爭激烈的場景:當多個事務需要同時訪問和修改同一份數(shù)據(jù)時,使用悲觀鎖可以確保數(shù)據(jù)在任一時刻只被一個事務訪問和修改,從而避免數(shù)據(jù)的不一致性和臟讀。
- 數(shù)據(jù)一致性要求極高的場景:在金融、醫(yī)療等行業(yè)中,對數(shù)據(jù)的一致性要求非常高,不允許出現(xiàn)任何的數(shù)據(jù)不一致或臟讀現(xiàn)象。在這些場景中,使用悲觀鎖可以確保數(shù)據(jù)在任一時刻只被一個事務訪問和修改,從而滿足數(shù)據(jù)一致性的要求。
- 寫操作頻繁的場景:如果系統(tǒng)中寫操作(如更新、刪除等)遠多于讀操作(如查詢),那么使用悲觀鎖可以更有效地保護數(shù)據(jù),避免在寫操作時被其他事務干擾。
- 事務執(zhí)行時間較長的場景:當事務的執(zhí)行時間較長時,使用悲觀鎖可以確保在該事務執(zhí)行期間,數(shù)據(jù)不會被其他事務修改,從而避免數(shù)據(jù)的不一致性和臟讀。
2.2、樂觀鎖適用場景
- 寫操作較少:在這種場景下,多個事務或線程大部分時間都在讀取數(shù)據(jù),而寫操作的頻率相對較低。樂觀鎖能夠減少鎖的持有時間,允許多個事務或線程同時讀取數(shù)據(jù),而不會相互阻塞。
- 數(shù)據(jù)沖突較少:如果數(shù)據(jù)更新操作之間的沖突較少,即多個事務或線程同時更新同一份數(shù)據(jù)的概率較低,那么樂觀鎖能夠發(fā)揮很好的性能。因為即使偶爾出現(xiàn)沖突,也只是在更新數(shù)據(jù)時才會被檢測到,而不需要在整個數(shù)據(jù)處理過程中都鎖定資源。
- 重試成本較低:樂觀鎖在檢測到?jīng)_突時會回滾事務或提示沖突,需要客戶端重新嘗試更新操作。因此,如果重試的成本較低(例如,重試不會導致大量計算或I/O操作),那么使用樂觀鎖是合適的。
- 系統(tǒng)能夠容忍一定程度的失敗:由于樂觀鎖在更新數(shù)據(jù)時可能會因為版本沖突而失敗,因此系統(tǒng)需要能夠處理這種失敗情況。如果系統(tǒng)能夠容忍一定程度的失敗(例如,通過重試或其他補償機制來恢復),那么使用樂觀鎖是可行的。
三、樂觀鎖與悲觀鎖各自優(yōu)缺點
3.1、悲觀鎖
優(yōu)點:
- 數(shù)據(jù)一致性高:悲觀鎖認為沖突一定會發(fā)生,因此在數(shù)據(jù)處理前會先加鎖,這樣可以確保數(shù)據(jù)在任一時刻只被一個事務訪問和修改,從而避免數(shù)據(jù)的不一致性和臟讀。
- 簡單易用:悲觀鎖的實現(xiàn)相對簡單,只需要在操作數(shù)據(jù)前獲取鎖即可。
缺點:
- 性能開銷大:悲觀鎖在操作數(shù)據(jù)前需要獲取鎖,如果有大量的并發(fā)操作,可能會導致性能問題,因為其他事務需要等待鎖釋放。
- 容易造成死鎖:如果多個事務相互等待對方釋放鎖,可能會導致死鎖的發(fā)生,影響系統(tǒng)的穩(wěn)定性和可用性。
- 可能導致資源浪費:如果獲取鎖后長時間不釋放,可能會導致其他事務無法操作數(shù)據(jù),從而造成資源浪費。
3.2、樂觀鎖
優(yōu)點:
- 高并發(fā)高吞吐:樂觀鎖不會阻塞其他事務的讀取操作,只在提交時檢查數(shù)據(jù)是否被修改,因此可以提供更好的并發(fā)性能。
- 無鎖操作:樂觀鎖不需要顯式地獲取和釋放鎖,減少了鎖競爭和上下文切換的開銷。
- 無死鎖風險:由于樂觀鎖不會阻塞其他事務的訪問,因此不會出現(xiàn)死鎖的情況。
缺點:
- 沖突處理復雜:由于樂觀鎖不會阻塞其他事務,因此在提交時需要檢查數(shù)據(jù)是否被其他事務修改,如果發(fā)現(xiàn)沖突,需要回滾事務或重新嘗試操作,這增加了沖突處理的復雜性。
- 數(shù)據(jù)一致性風險:樂觀鎖假設并發(fā)沖突較少,因此可能存在數(shù)據(jù)一致性的風險。如果多個事務同時對同一數(shù)據(jù)進行修改,可能會導致數(shù)據(jù)不一致的情況。
- 需要額外字段:為了實現(xiàn)樂觀鎖,通常需要在數(shù)據(jù)表中添加額外的版本號或時間戳字段,這增加了存儲空間的需求。
- 處理不當造成死循環(huán)風險:在大多數(shù)業(yè)務中樂觀鎖更新失敗都會進行自旋,如果沒有控制好自旋退出邏輯可能會造成遞歸死循環(huán)問題。
四、樂觀鎖與悲觀鎖使用示例
這里舉例會以操作數(shù)據(jù)庫實現(xiàn)樂觀鎖與悲觀鎖示例,在實際開發(fā)中一般在操作數(shù)據(jù)庫時經(jīng)常會使用到樂觀鎖與悲觀鎖的實現(xiàn)思路來確保數(shù)據(jù)一致性問題,這里會以一個更新用戶錢包舉例。
用戶錢包表
CREATE TABLE `customer_wallet` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `customer_id` bigint(20) DEFAULT NULL COMMENT '客戶ID', `balance_amount` bigint(20) DEFAULT NULL COMMENT '剩余金額', `version` bigint(20) DEFAULT '1' COMMENT '版本鎖', PRIMARY KEY (`id`) USING BTREE, KEY `idx_customer_id` (`customer_id`) ) COMMENT='客戶錢包信息';
4.1、悲觀鎖實現(xiàn)更新用戶錢包示例
這里提供一個使用悲觀鎖扣減錢包余額的示例,在第一次查詢時添加for update
操作,那么其它線程進入該方法時則會阻塞等待上一個方法事務提交才能繼續(xù)執(zhí)行,在這整個方法中都是線程安全的,這就是常見的結(jié)合數(shù)據(jù)庫實現(xiàn)悲觀鎖更新數(shù)據(jù)的示例,所有線程都必須排隊串行更新數(shù)據(jù)。
@Transactional(rollbackFor = Exception.class) public boolean pessimisticLockSubAmount(Long customerId, Long happenAmount) { // 1、查詢用戶錢包 - 并且添加for update 鎖,這里customer_id字段添加了索引最終鎖定的還是索引定義行的ID,和直接使用ID區(qū)別不大 // 這段代碼相當于 select * from customer_wallet where customer_id = ? for update CustomerWallet customerWallet = lambdaQuery() .eq(CustomerWallet::getCustomerId, customerId) .last("for update") .one(); if(customerWallet == null){ throw new RuntimeException("用戶錢包不存在"); } // 2、校驗用戶余額是否足夠 Long balanceAmount = customerWallet.getBalanceAmount() - happenAmount; if(balanceAmount < 0){ throw new RuntimeException("用戶余額不足"); } // 3、更新錢包余額 update customer_wallet set balance_amount = ? where id = ? boolean update = lambdaUpdate() .eq(CustomerWallet::getId, customerWallet.getId()) .set(CustomerWallet::getBalanceAmount, balanceAmount) .update(); if(!update){ throw new RuntimeException("錢包更新失敗"); } // 4、添加余額明細 addWalletDetail(customerWallet.getId(),2,happenAmount,balanceAmount); return update; }
4.2、樂觀鎖實現(xiàn)更新用戶錢包示例1
使用樂觀鎖更新數(shù)據(jù)時,執(zhí)行更新語句時通過判斷version
是否有變動來確認數(shù)據(jù)是否有過變更,如果數(shù)據(jù)庫當前version
值和查詢出來的version
值相等則代表數(shù)據(jù)沒有變更可以更新,因為數(shù)據(jù)庫指定ID更新某一行數(shù)據(jù)時是在數(shù)據(jù)庫層面會添加行鎖,確保只能有一個事務進行這行數(shù)據(jù)更新,這樣就保證了數(shù)據(jù)的一致性。
@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class) public boolean subAmount(Long customerId, Long happenAmount) { // 1、獲取用戶錢包 CustomerWallet customerWallet = lambdaQuery().eq(CustomerWallet::getCustomerId, customerId).one(); if (customerWallet == null) { throw new RuntimeException("用戶錢包不存在"); } // 2、判斷用戶余額是否足夠 Long balanceAmount = customerWallet.getBalanceAmount() - happenAmount; if(balanceAmount < 0){ throw new RuntimeException("用戶余額不足"); } // 3、進行樂觀鎖更新 // 這段代碼相當于 update customer_wallet set balance_amount = ?, version = ? where id = ? and version = ? boolean update = lambdaUpdate() .eq(CustomerWallet::getId, customerWallet.getId()) .eq(CustomerWallet::getVersion, customerWallet.getVersion()) .set(CustomerWallet::getBalanceAmount, balanceAmount) .set(CustomerWallet::getVersion, customerWallet.getVersion() + 1) .update(); if(!update){ log.info("樂觀鎖更新失敗,開始自旋"); return subAmount(customerId,happenAmount); } // 4、添加余額明細 addWalletDetail(customerWallet.getId(),2,happenAmount,balanceAmount); return update; }
PS:注意,在使用樂觀鎖更新數(shù)據(jù)時,事務隔離級別必須設置為READ_COMMITTED,在最后注意事項中會進行分析。
4.3、樂觀鎖實現(xiàn)更新用戶錢包示例2
在樂觀鎖實現(xiàn)更新用戶錢包示例1中使用了一個version
字段來作為樂觀鎖更新的標記,其實對于這種更新錢包的業(yè)務想使用樂觀鎖完全沒有必要單獨加一個version
字段,可以直接使用余額字段作為這個樂觀鎖的比較字段,因為我們這里擬定的是用戶余額需要足夠才能支付,那么在更新錢包時判斷一下當前余額是否大于等于所需金額,如果滿足調(diào)整則
@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class) public boolean subAmountV2(Long customerId, Long happenAmount) { // 1、獲取用戶錢包 CustomerWallet customerWallet = lambdaQuery().eq(CustomerWallet::getCustomerId, customerId).one(); if (customerWallet == null) { throw new RuntimeException("用戶錢包不存在"); } // 2、直接使用余額作為樂觀鎖更新依據(jù)進行樂觀鎖更新 // 這段代碼相當于 update customer_wallet set balance_amount = balance_amount + ? where id = ? and balance_amount >= ? boolean update = lambdaUpdate() .eq(CustomerWallet::getId, customerWallet.getId()) .ge(CustomerWallet::getBalanceAmount, happenAmount) .setSql("balance_amount = balance_amount - "+happenAmount) .update(); if(!update){ log.info("樂觀鎖更新失敗,用戶余額不足"); throw new RuntimeException("用戶余額不足"); } // 3、添加余額明細 注意這里需要從新查詢一次數(shù)據(jù) CustomerWallet customerWalletNew = lambdaQuery().eq(CustomerWallet::getCustomerId, customerId).one(); addWalletDetail(customerWallet.getId(),2,happenAmount,customerWalletNew.getBalanceAmount()); return update; }
PS:注意,在使用樂觀鎖更新數(shù)據(jù)時,事務隔離級別必須設置為READ_COMMITTED,在最后注意事項中會進行分析。
五、注意事項
在使用樂觀鎖更新數(shù)據(jù)時要注意一個事務隔離級別的問題,我這里使用的是READ COMMITTED
(讀已提交),如果使用的是REPEATABLE READ
(可重復讀)會存在兩個問題,分別對應4.2 和 4.3中的示例。
- 在4.2示例中通過version值來實現(xiàn)樂觀鎖更新,如果這里使用的事務隔離級別為
REPEATABLE READ
(可重復讀),那么在樂觀鎖沖突更新失敗自旋時因為MVCC機制查詢到的數(shù)據(jù)會是一個副本值,就算別的事務更新成功了讀取到的version都是歷史值,這樣會導致死循環(huán)遞歸最后棧溢出。 - 在4.3示例中采用余額這樣的字段進行判斷更新,因為MySQL的更新數(shù)據(jù)采用的是當前讀,這里其實無論使用
REPEATABLE READ
(可重復讀)還是READ COMMITTED
(讀已提交)事務隔離級別都不會存在死循環(huán)問題,但是如果鎖沖突頻繁在使用REPEATABLE READ
(可重復讀)事務隔離級別時可能會出現(xiàn)鎖持有時間過長問題,因為在REPEATABLE READ
事務隔離級別下,在一個事務中執(zhí)行一個更新語句,就算where id=1 and balance_amount >= 100
這樣的條件不成立,也會將這一行數(shù)據(jù)進行鎖定,需要等待事務提交或回滾才會釋放鎖,也就是說在自旋時其它事務想要更新數(shù)據(jù)等待時間會變長影響系統(tǒng)吞吐量,而使用READ COMMITTED
事務隔離級別當where
中條件不成立更新失敗時不會持有鎖,也就是說事務A在更新失敗自旋時事務B也是可以進行更新的,而不需要等待事務A自旋更新成功后才能進行更新,這樣能提高系統(tǒng)吞吐量。
到此這篇關于java中樂觀鎖與悲觀鎖區(qū)別及使用場景分析的文章就介紹到這了,更多相關java 樂觀鎖與悲觀鎖內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
在Linux系統(tǒng)上升級Java版本的兩種方法步驟
由于項目升級,需要將JDK7升級到JDK8,升級JDK的同時也要升級一些其他的版本,下面這篇文章主要給大家介紹了關于在Linux系統(tǒng)上升級Java版本的兩種方法步驟,需要的朋友可以參考下2024-09-09Intellij?IDEA根據(jù)maven依賴名查找它是哪個pom.xml引入的(圖文詳解)
這篇文章主要介紹了Intellij?IDEA根據(jù)maven依賴名查找它是哪個pom.xml引入的,本文通過圖文并茂的形式給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-08-08