JPA使用樂觀鎖應(yīng)對(duì)高并發(fā)方式
JPA使用樂觀鎖應(yīng)對(duì)高并發(fā)
高并發(fā)系統(tǒng)的挑戰(zhàn)
在部署分布式系統(tǒng)時(shí),我們通常把多個(gè)微服務(wù)部署在內(nèi)網(wǎng)集群中,再用API網(wǎng)關(guān)聚合起來(lái)對(duì)外提供。為了做負(fù)載均衡,通常會(huì)對(duì)每個(gè)微服務(wù)都啟動(dòng)多個(gè)運(yùn)行實(shí)例,通過(guò)注冊(cè)中心去調(diào)用。
那么問(wèn)題來(lái)了,因?yàn)橛卸鄠€(gè)實(shí)例運(yùn)行都是同一個(gè)應(yīng)用,雖然微服務(wù)網(wǎng)關(guān)會(huì)把每一個(gè)請(qǐng)求只轉(zhuǎn)發(fā)給一個(gè)實(shí)例,但當(dāng)面對(duì)高并發(fā)時(shí),但它們?nèi)匀豢赡芡瑫r(shí)操作同一個(gè)數(shù)據(jù)庫(kù)表,這會(huì)不會(huì)引發(fā)什么問(wèn)題呢?
悲觀鎖的問(wèn)題
比如電商中常見的商品秒殺系統(tǒng),在用戶搶購(gòu)商品過(guò)程中,會(huì)有大量并發(fā)請(qǐng)求,很可能同時(shí)讀寫一個(gè)包含商品剩余數(shù)量的表,這種一般要給數(shù)據(jù)庫(kù)加鎖,否則很容易出現(xiàn)商品超賣錯(cuò)賣的情況。
如果使用數(shù)據(jù)庫(kù)自帶的鎖機(jī)制,也就是悲觀鎖,在寫入的時(shí)候鎖定數(shù)據(jù)庫(kù),其他修改請(qǐng)求到來(lái)時(shí)就必須等待鎖釋放,但這就使效率降下來(lái)了,而且高并發(fā)場(chǎng)景下可能有的請(qǐng)求一直搶不到鎖,就會(huì)長(zhǎng)時(shí)間卡在那導(dǎo)致請(qǐng)求失敗,然后有大量重試,系統(tǒng)可能會(huì)發(fā)生連接數(shù)耗盡等異常。
樂觀鎖是個(gè)好東西
查閱資料發(fā)現(xiàn)樂觀鎖是個(gè)好東西,它是為數(shù)據(jù)庫(kù)表增加一個(gè)標(biāo)識(shí)數(shù)據(jù)版本的version字段來(lái)實(shí)現(xiàn)的,讀取數(shù)據(jù)時(shí)把version字段一同讀出,寫入數(shù)據(jù)庫(kù)時(shí)比對(duì)version字段就知道數(shù)據(jù)是否被更改過(guò),如果version不相等就說(shuō)明持有的是過(guò)期數(shù)據(jù),不能寫入,如果相等就可以寫入,并把version加一。
樂觀鎖在寫入數(shù)據(jù)庫(kù)的時(shí)候,才會(huì)檢查數(shù)據(jù)是否沖突,如果發(fā)現(xiàn)沖突了,就放棄寫入,返回寫入失敗的信息,相比于悲觀鎖,這是一種輕量級(jí)的對(duì)數(shù)據(jù)的鎖定方式,能夠應(yīng)對(duì)高并發(fā)需求。
給數(shù)據(jù)庫(kù)添加樂觀鎖
說(shuō)樂觀鎖是個(gè)好東西,首先得說(shuō) JPA 是個(gè)好東西,因?yàn)镾pring Data JPA已經(jīng)內(nèi)置了樂觀鎖的實(shí)現(xiàn),給數(shù)據(jù)庫(kù)表添加樂觀鎖很簡(jiǎn)單,添加一個(gè)整型字段,并加入@Version注解就可以了,每次提交數(shù)據(jù)時(shí)JPA會(huì)自動(dòng)檢查版本。
@Entity @Table(name = "m_order") public class Order { ... @Version private int version; ... }
樂觀鎖 -業(yè)務(wù)判斷 解決高并發(fā)
在解決高并發(fā)問(wèn)題時(shí),如果是分布式系統(tǒng)顯然我們只能夠使用數(shù)據(jù)庫(kù)端加鎖機(jī)制來(lái)解決這個(gè)問(wèn)題,但是這種同步機(jī)制或者數(shù)據(jù)庫(kù)物理鎖機(jī)制會(huì)犧牲一部分的性能,所以常常以另外一種方式來(lái)解決這個(gè)問(wèn)題 就是樂觀鎖模式
銀行兩操作員同時(shí)操作同一賬戶就是典型的樂觀鎖模式。
比如A、B操作員同時(shí)讀取一余額為1000元的賬戶,A操作員為該賬戶增加100元,B操作員同時(shí)為該賬戶扣除50元,A先提交,B后提交。最后實(shí)際賬戶余額為1000-50=950元,但本該為1000+100-50=1050。這就是典型的并發(fā)問(wèn)題。
樂觀鎖機(jī)制在一定程度上解決了這個(gè)問(wèn)題。樂觀鎖,大多是基于數(shù)據(jù)版本(Version)記錄機(jī)制實(shí)現(xiàn)。何謂數(shù)據(jù)版本?即為數(shù)據(jù)增加一個(gè)版本標(biāo)識(shí),在基于數(shù)據(jù)庫(kù)表的版本解決方案中,一般是通過(guò)為數(shù)據(jù)庫(kù)表增加一個(gè) “version” 字段來(lái)實(shí)現(xiàn)。
讀取出數(shù)據(jù)時(shí),將此版本號(hào)一同讀出,之后更新時(shí),對(duì)此版本號(hào)加一。此時(shí),將提交數(shù)據(jù)的版本數(shù)據(jù)與數(shù)據(jù)庫(kù)表對(duì)應(yīng)記錄的當(dāng)前版本信息進(jìn)行比對(duì),如果提交的數(shù)據(jù)版本號(hào)大于數(shù)據(jù)庫(kù)表當(dāng)前版本號(hào),則予以更新,否則認(rèn)為是過(guò)期數(shù)據(jù)。
對(duì)于上面修改用戶帳戶信息的例子而言,假設(shè)數(shù)據(jù)庫(kù)中帳戶信息表中有一個(gè)version字段,當(dāng)前值為1;而當(dāng)前帳戶余額字段(balance)為1000元。假設(shè)操作員A先更新完,操作員B后更新。
- a、操作員A此時(shí)將其讀出(version=1),并從其帳戶余額中增加100(1000+100=1100)。
- b、在操作員A操作的過(guò)程中,操作員B也讀入此用戶信息(version=1),并從其帳戶余額中扣除50(1000-50=950)。
- c、操作員A完成了修改工作,將數(shù)據(jù)版本號(hào)加一(version=2),連同帳戶增加后余額(balance=1100),提交至數(shù)據(jù)庫(kù)更新,此時(shí)由于提交數(shù)據(jù)版本大于數(shù)據(jù)庫(kù)記錄當(dāng)前版本,數(shù)據(jù)被更新,數(shù)據(jù)庫(kù)記錄version更新為2。
- d、操作員B完成了操作,也將版本號(hào)加一(version=2)試圖向數(shù)據(jù)庫(kù)提交數(shù)據(jù)(balance=950),但此時(shí)比對(duì)數(shù)據(jù)庫(kù)記錄版本時(shí)發(fā)現(xiàn),操作員B提交的數(shù)據(jù)版本號(hào)為2,數(shù)據(jù)庫(kù)記錄當(dāng)前版本也為2,不滿足 “提交版本必須大于記錄當(dāng)前版本才能執(zhí)行更新 “的樂觀鎖策略,因此,操作員B的提交被駁回。
- 這樣,就避免了操作員B用基于version=1的舊數(shù)據(jù)修改的結(jié)果覆蓋操作員A的操作結(jié)果的可能。
操作員A操作如下:
select id, balance, version from account where id="1"; 查詢結(jié)果:id=1, balance=1000, version=1 update account set balance=balance+100, version=version+1 where id="1" and version=1 select id, balance, version from account where id="1"; 查詢結(jié)果:id=1, balance=1100, version=2
操作員B操作如下:
select id, balance, version from account where id="1"; 查詢結(jié)果:id=1, balance=1000, version=1 #操作員A已修改成功,實(shí)際account.balance=1100、account.version=2,操作員B也將版本號(hào)加一(version=2)試圖向數(shù)據(jù)庫(kù)提交數(shù)據(jù)(balance=950),但此時(shí)比對(duì)數(shù)據(jù)庫(kù)記錄版本時(shí)發(fā)現(xiàn),操作員B提交的數(shù)據(jù)版本號(hào)為2,數(shù)據(jù)庫(kù)記錄當(dāng)前版本也為2,不滿足 “提交版本必須大于記錄當(dāng)前版本才能執(zhí)行更新 “的樂觀鎖策略,因此,操作員B的提交被駁回。 update account set balance=balance-50, version=version+1 where id="1" and version=1 select id, balance, version from account where id="1"; 查詢結(jié)果:id=1, balance=1100, version=2
Hibernate、JPA等ORM框架或者實(shí)現(xiàn),是使用版本號(hào),再判斷UPDATE后返回的數(shù)值
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
java鏈表數(shù)據(jù)結(jié)構(gòu)LinkedList插入刪除元素時(shí)間復(fù)雜度面試精講
這篇文章主要為大家介紹了java LinkedList插入和刪除元素的時(shí)間復(fù)雜度面試精講,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10當(dāng)Transactional遇上synchronized的解決方法分享
前些時(shí)間剛好刷到了有關(guān)于“# 【事務(wù)與鎖】當(dāng)Transactional遇上synchronized”這一類的文章,感覺這也是工作中經(jīng)常會(huì)遇到的一類問(wèn)題了。所以就針對(duì)這個(gè)話題進(jìn)行了分析并整理了常用的解決方法,希望對(duì)大家有所幫助2023-05-05使用spring框架實(shí)現(xiàn)數(shù)據(jù)庫(kù)事務(wù)處理方式
這篇文章主要介紹了使用spring框架實(shí)現(xiàn)數(shù)據(jù)庫(kù)事務(wù)處理方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-10-10手?jǐn)]一個(gè)Spring?Boot?Starter并上傳到Maven中央倉(cāng)庫(kù)
本文主要介紹了手?jǐn)]一個(gè)Spring?Boot?Starter并上傳到Maven中央倉(cāng)庫(kù),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-05-05Idea中Springboot熱部署無(wú)效問(wèn)題解決
這篇文章主要介紹了Idea中Springboot熱部署無(wú)效問(wèn)題解決,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-11-11mybatis-plus通用枚舉@JsonValue接收參數(shù)報(bào)錯(cuò)No enum constant
最近在使用mybatis-plus時(shí)用到了通用枚舉,遇到了問(wèn)題,本文主要介紹了mybatis-plus通用枚舉@JsonValue接收參數(shù)報(bào)錯(cuò)No enum constant,具有一定的參考價(jià)值,感興趣的可以了解一下2023-09-09Spring JdbcTemplate實(shí)現(xiàn)添加與查詢方法詳解
JdbcTemplate是Spring框架自帶的對(duì)JDBC操作的封裝,目的是提供統(tǒng)一的模板方法使對(duì)數(shù)據(jù)庫(kù)的操作更加方便、友好,效率也不錯(cuò),這篇文章主要介紹了Spring?JdbcTemplate執(zhí)行數(shù)據(jù)庫(kù)操作,需要的朋友可以參考下2022-11-11SpringBoot和Swagger結(jié)合提高API開發(fā)效率
這篇文章主要介紹了SpringBoot和Swagger結(jié)合提高API開發(fā)效率的相關(guān)資料,需要的朋友可以參考下2017-09-09