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