1. 程式人生 > >mybatis和JPA實現樂觀鎖解決併發問題-阿里巴巴JAVA開發手冊詳細解讀

mybatis和JPA實現樂觀鎖解決併發問題-阿里巴巴JAVA開發手冊詳細解讀

在阿里巴巴近期發出的阿里巴巴JAVA開發手冊(終極版)中有這樣一條記錄。

【強制】併發修改同一記錄時,避免更新丟失,需要加鎖。要麼在應用層加鎖,要麼在快取加鎖,要麼在資料庫層使用樂觀鎖,使用version作為更新依據。

說明:如果每次訪問衝突概率小於20%,推薦使用樂觀鎖,否則使用悲觀鎖。樂觀鎖的重試次數不得小於3次。

那麼什麼是樂觀鎖呢?

樂觀鎖( Optimistic Locking ) 相對悲觀鎖而言,樂觀鎖假設認為資料一般情況下不會造成衝突,所以在資料進行提交更新的時候,才會正式對資料的衝突與否進行檢測,如果發現衝突了,則讓返回使用者錯誤的資訊,讓使用者決定如何去做。那麼我們如何實現樂觀鎖呢,一般來說有以下2種方式:

1.使用資料版本(Version)記錄機制實現,這是樂觀鎖最常用的一種實現方式。何謂資料版本?即為資料增加一個版本標識,一般是通過為資料庫表增加一個數字型別的 “version” 欄位來實現。當讀取資料時,將version欄位的值一同讀出,資料每更新一次,對此version值加一。當我們提交更新的時候,判斷資料庫表對應記錄的當前版本資訊與第一次取出來的version值進行比對,如果資料庫表當前版本號與第一次取出來的version值相等,則予以更新,否則認為是過期資料。

通過例項讓大家詳細理解

銀行兩操作員同時操作同一賬戶就是典型的例子。
比如A、B操作員同時讀取一餘額為1000元的賬戶,A操作員為該賬戶增加100元,B操作員同時為該賬戶扣除50元,A先提交,B後提交。最後實際賬戶餘額為1000-50=950元,但本該為1000+100-50=1050。這就是典型的併發問題。

對於上面修改使用者帳戶資訊的例子而言,假設資料庫中帳戶資訊表中有一個version欄位,當前值為1;而當前帳戶餘額欄位(balance)為1000元。假設操作員A先更新完,操作員B後更新。
a、操作員A此時將其讀出(version=1),並從其帳戶餘額中增加100(1000+100=1100)。
b、在操作員A操作的過程中,操作員B也讀入此使用者資訊(version=1),並從其帳戶餘額中扣除50(1000-50=950)。
c、操作員A完成了修改工作,將資料版本號加一(version=2),連同帳戶增加後餘額(balance=1100),提交至資料庫更新,此時由於提交資料版本大於資料庫記錄當前版本,資料被更新,資料庫記錄version更新為2。
d、操作員B完成了操作,也將版本號加一(version=2)試圖向資料庫提交資料(balance=950),但此時比對資料庫記錄版本時發現,操作員B提交的資料版本號為2,資料庫記錄當前版本也為2,不滿足 “提交版本必須大於記錄當前版本才能執行更新 “的樂觀鎖策略,因此,操作員B的提交被駁回。
這樣,就避免了操作員B用基於version=1的舊資料修改的結果覆蓋操作員A的操作結果的可能。

資料建表

t_goods表,增加一個version欄位,資料預設version值為0。

t_goods表初始資料如下:

Sql程式碼  收藏程式碼
  1. mysql> select * from t_goods;  
  2. +----+--------+------+---------+
  3. | id | status | name | version |  
  4. +----+--------+------+---------+
  5. |  1 |      1 | 道具 |       0 |  
  6. |  2 |      2 | 裝備 |       1 |  
  7. +----+--------+------+---------+
  8. rowsinset

mybatis實現

Goods實體類:

Java程式碼  收藏程式碼
  1. /** 
  2.  * ClassName: Goods <br/> 
  3.  * Function: 商品實體. <br/> 
  4.  * date: 2013-5-8 上午09:16:19 <br/> 
  5.  * @author [email protected] 
  6.  */
  7. publicclass Goods implements Serializable {  
  8.     /** 
  9.      * serialVersionUID:序列化ID. 
  10.      */
  11.     privatestaticfinallong serialVersionUID = 6803791908148880587L;  
  12.     /** 
  13.      * id:主鍵id. 
  14.      */
  15.     privateint id;  
  16.     /** 
  17.      * status:商品狀態:1未下單、2已下單. 
  18.      */
  19.     privateint status;  
  20.     /** 
  21.      * name:商品名稱. 
  22.      */
  23.     private String name;  
  24.     /** 
  25.      * version:商品資料版本號. 
  26.      */
  27.     privateint version;  
  28.     @Override
  29.     public String toString(){  
  30.         return"good id:"+id+",goods status:"+status+",goods name:"+name+",goods version:"+version;  
  31.     }  
  32.     //setter and getter
  33. }  

GoodsDao

Java程式碼  收藏程式碼
  1. /** 
  2.  * updateGoodsUseCAS:使用CAS(Compare and set)更新商品資訊. <br/> 
  3.  * 
  4.  * @author [email protected] 
  5.  * @param goods 商品物件 
  6.  * @return 影響的行數 
  7.  */
  8. int updateGoodsUseCAS(Goods goods);  

mapper.xml

Xml程式碼  收藏程式碼
  1. <updateid="updateGoodsUseCAS"parameterType="Goods">
  2.     <![CDATA[ 
  3.         update t_goods 
  4.         set status=#{status},name=#{name},version=version+1 
  5.         where id=#{id} and version=#{version} 
  6.     ]]>
  7. </update>

GoodsDaoTest測試類

Java程式碼  收藏程式碼
  1. @Test
  2. publicvoid goodsDaoTest(){  
  3.     int goodsId = 1;  
  4.     //根據相同的id查詢出商品資訊,賦給2個物件
  5.     Goods goods1 = this.goodsDao.getGoodsById(goodsId);  
  6.     Goods goods2 = this.goodsDao.getGoodsById(goodsId);  
  7.     //列印當前商品資訊
  8.     System.out.println(goods1);  
  9.     System.out.println(goods2);  
  10.     //更新商品資訊1
  11.     goods1.setStatus(2);//修改status為2
  12.     int updateResult1 = this.goodsDao.updateGoodsUseCAS(goods1);  
  13.     System.out.println("修改商品資訊1"+(updateResult1==1?"成功":"失敗"));  
  14.     //更新商品資訊2
  15.     goods1.setStatus(2);//修改status為2
  16.     int updateResult2 = this.goodsDao.updateGoodsUseCAS(goods1);  
  17.     System.out.println("修改商品資訊2"+(updateResult2==1?"成功":"失敗"));  
  18. }  

輸出結果:

Shell程式碼  收藏程式碼
  1. good id:1,goods status:1,goods name:道具,goods version:1
  2. good id:1,goods status:1,goods name:道具,goods version:1
  3. 修改商品資訊1成功  
  4. 修改商品資訊2失敗  

說明:

在GoodsDaoTest測試方法中,我們同時查出同一個版本的資料,賦給不同的goods物件,然後先修改good1物件然後執行更新操作,執行成功。然後我們修改goods2,執行更新操作時提示操作失敗。此時t_goods表中資料如下:

Sql程式碼  收藏程式碼
  1. mysql> select * from t_goods;  
  2. +----+--------+------+---------+
  3. | id | status | name | version |  
  4. +----+--------+------+---------+
  5. |  1 |      2 | 道具 |       2 |  
  6. |  2 |      2 | 裝備 |       2 |  
  7. +----+--------+------+---------+
  8. rowsinset
  9. mysql>   

我們可以看到 id為1的資料version已經在第一次更新時修改為2了。所以我們更新good2時update where條件已經不匹配了,所以更新不會成功,具體sql如下:

Sql程式碼  收藏程式碼
  1. update t_goods   
  2. set status=2,version=version+1  
  3. where id=#{id} and version=#{version};  

這樣我們就在Mybatis實現了樂觀鎖

JPA實現

持久層使用jpa時比較簡單,JPA預設提供了一個註解@Version,我們只需要在剛才的實體類中做相應的註解就可以。

Goods實體類:

Java程式碼  收藏程式碼
  1. /** 
  2.  * ClassName: Goods <br/> 
  3.  * Function: 商品實體. <br/> 
  4.  * date: 2013-5-8 上午09:16:19 <br/> 
  5.  * @author [email protected] 
  6.  */
  7. publicclass Goods implements Serializable {  
  8.     /** 
  9.      * serialVersionUID:序列化ID. 
  10.      */
  11.     privatestaticfinallong serialVersionUID = 6803791908148880587L;  
  12.     /** 
  13.      * id:主鍵id. 
  14.      */
  15.     @Id
        @GenericGenerator(name = "PKUUID", strategy = "uuid2")
        @GeneratedValue(generator = "PKUUID")
        @Column(length = 36)
  16.     privateInteger id;  
  17.     /** 
  18.      * status:商品狀態:1未下單、2已下單. 
  19.      */
  20.   @Column(name = "status") 
  21.     private Integer status;  
  22.     /** 
  23.      * name:商品名稱. 
  24.      */
  25.   @Column(name = "name")
  26.     private String name;  
  27.     /** 
  28.      * version:商品資料版本號. 
  29.      */
  30.   @Version 
  31.     privateInteger version;  
  32.     @Override
  33.     public String toString(){  
  34.         return"good id:"+id+",goods status:"+status+",goods name:"+name+",goods version:"+version;  
  35.     }  
  36.     //setter and getter
  37. }  
在JPA中對於添加了@Version註解的欄位,我們不需要像mybatis中一樣手動去控制,每一次save操作會在原來的基礎上+1,如果初始為null,則springdata自動設定其為0。

總結

樂觀鎖,用在一些敏感業務資料上,而其本身的修飾:樂觀,代表的含義便是相信大多數場景下version是一致的。但是從業務角度出發又要保證資料的嚴格一致性,避免髒讀等問題,使用的場景需要斟酌。記得前面一片博文簡單介紹了一下行級鎖的概念,其實本質上和樂觀鎖都是想要再資料庫層面加鎖控制併發,那麼什麼時候該用樂觀鎖,行級鎖,什麼時候得在程式級別加同步鎖,又要根據具體的業務場景去判斷。找到能夠滿足自己專案需求的方案,找到效能和可靠性的平衡點,才是一個程式設計師的價值所在。

歡迎大家關注、轉載、評論。

大家有什麼問題需要討論的也歡迎在評論下留言,我會隨時關注,大家一起進步。

參考部落格地址:

http://www.cnblogs.com/linjiqin/p/5096206.html

http://chenzhou123520.iteye.com/blog/1863407

http://blog.csdn.net/u013815546/article/details/54784025

http://blog.csdn.net/tzdwsy/article/details/47976919

相關推薦

mybatisJPA實現樂觀解決併發問題-阿里巴巴JAVA開發手冊詳細解讀

在阿里巴巴近期發出的阿里巴巴JAVA開發手冊(終極版)中有這樣一條記錄。 【強制】併發修改同一記錄時,避免更新丟失,需要加鎖。要麼在應用層加鎖,要麼在快取加鎖,要麼在資料庫層使用樂觀鎖,使用version作為更新依據。 說明:如果每次訪問衝突概率小於20%,推薦使用樂觀鎖

阿里巴巴Java開發手冊--程式碼抒寫規範注意

Java中 VO、 PO、DO、DTO、 BO、 QO、DAO、POJO的概念 PO(persistant object) 持久物件 在 o/r 對映的時候出現的概念,如果沒有 o/r 對映,沒有這個概念存在了。通常對應資料模型 ( 資料庫 ), 本身還有部分業務邏輯的

阿里巴巴Java開發手冊———個人追加的見解補充(一)

先上乾貨,《阿里巴巴Java開發手冊》的下載地址https://yq.aliyun.com/articles/69327下面分幾個部分對這個手冊進行說明,都是個人的見解,本人技術一般,如果有錯誤或者不妥,請評論指出,虛心接受,提前感謝了。 建議邊看手冊,邊食用以下說明,效果比較好。 前言首先當我第一次看見

為什麼阿里巴巴Java開發手冊中強制要求不要在foreach迴圈裡進行元素的removeadd操作?

在閱讀《阿里巴巴Java開發手冊》時,發現有一條關於在 foreach 迴圈裡進行元素的 remove/add 操作的規約,具體內容如下: 錯誤演示 我們首先在 IDEA 中編寫一個在 foreach 迴圈裡進行 remove 操作的程式碼: import java.util.ArrayList; imp

利用Redis實現分散式 使用mysql樂觀解決併發問題

寫在最前面 我在之前總結冪等性的時候,寫過一種分散式鎖的實現,可惜當時沒有真正應用過,著實的心虛啊。正好這段時間對這部分實踐了一下,也算是對之前填坑了。 分散式鎖按照網上的結論,大致分為三種:1、資料庫樂觀鎖; 2、基於Redis的分散式鎖;3.、基於ZooKeeper的分散式鎖; 關於樂觀鎖的實現其實

【Spring】27、JPA 實現樂觀@Version註解的使用

線程並發 基礎上 nal where 本質 項目需求 得到 業務 -s 持久層使用jpa時,默認提供了一個註解@Version來實現樂觀鎖 簡單來說就是用一個version字段來充當樂觀鎖的作用。先來設計實體類 /** * Created by xujingfeng on

MP(MyBatis-Plus)實現樂觀更新功能

## 實現步驟 **step1:新增樂觀鎖攔截器** MP的其他攔截器功能可以參考[官網](https://mp.baomidou.com/guide/interceptor.html) ```java @Bean public MybatisPlusInterceptor mybatisPlusInt

阿里巴巴Java開發規約外掛---------安裝使用(健康之家)

1、 Eclispe安裝, Help -> Install New Software   2、輸入阿里規約外掛的網址https://p3c.alibaba.com/plugin/eclipse/update,Name隨便取一個(叫阿里巴巴都行),然後按照常規方

阿里巴巴Java開發規約-外掛使用[IdeaEclipse]

       阿里巴巴基於手冊內容,研發了一套自動化的IDE檢測外掛(IDEA、Eclipse)。該外掛在掃描程式碼後,將不符合規約的程式碼按Blocker(命名不符合規範)/Critical/Maj

分散式解決併發的三種實現方式

分散式鎖解決併發的三種實現方式 在很多場景中,我們為了保證資料的最終一致性,需要很多的技術方案來支援,比如分散式事務、分散式鎖等。有的時候,我們需要保證一個方法在同 一時間內只能被同一個執行緒執行。在單機環境中,Java中其實提供了很多併發處理相關的API,但是這些API在分散式場景中就無能

你們部署伺服器是幾臺,併發量是多大;怎麼進行模擬搶購的同一時間請求量是多少;怎麼防止帶刷(黃牛)如果說部署兩臺伺服器 不同的程序 怎麼實現樂觀

Django專案用到5臺伺服器。部署在2臺上面,因為使用者量比較少。 模擬搶購主要解決2個問題: 1.高併發對資料庫產生的壓力 2.競爭狀態下如何解決庫存的正確減少("超賣"問題) 對於第一個問題可以使用redis解決,避免對資料庫的直接操作較少資料防護的查詢壓力。 對於“超賣”專案

悲觀樂觀解決事務丟失跟新問題

事務丟失更新:A,B兩個事務通過id獲取資料,name:lisi,money:1000 首先,A事務修改name,把lisi變為zhangsan,然後提交事務。此時,B事務中不受A事務的影響,即B事務中的name還是lisi,此時如果B事務改變money為2000,然後提交事務。最後資料庫的資料

JPA使用樂觀應對高併發

高併發系統的挑戰 在部署分散式系統時,我們通常把多個微服務部署在內網叢集中,再用API閘道器聚合起來對外提供。為了做負載均衡,通常會對每個微服務都啟動多個執行例項,通過註冊中心去呼叫。 那麼問題來了,因為有多個例項執行都是同一個應用,雖然微服務閘道器會把每一

Redis:多線程修改同一個Key使用watch+事務(mutil)實現樂觀

width uno ... ack spool 場景 .html 高並發 遇到的問題 本篇文章是通過watch(監控)+mutil(事務)實現應用於在分布式高並發處理等相關場景。下邊先通過redis-cli.exe來測試多個線程修改時,遇到問題及解決問題。 高並發下修改同

elasticsearch 筆記七: es樂觀併發控制

1.併發控制 es 的併發控制是通過多version來實現的(不清楚樂觀鎖的自己提升去) 2.例項 //建立索引 PUT /test_index/test_type/7 { "test_field": "test test" } //返回建立結果 GET test_index

redis 實現樂觀

轉載務必說明出處:https://blog.csdn.net/LiaoHongHB/article/details/83410650 1、redis通過事務機制中watch命令可以實現Java樂觀鎖機制 public void watch() { try {

利用Redis解決併發問題

轉發 https://blog.csdn.net/fuyifang/article/details/83008884 用redis處理高併發是個很常見的方式,因為redis的訪問效率很高(直接訪問記憶體),一般我們會用來處理網站一瞬間的併發量。 那如果要使用redis來進行高併發問

Redis實現樂觀

樂觀鎖       大多數是基於資料版本(version)的記錄機制實現的。即為資料增加一個版本標識,在基於資料庫表的版本解決方案中,一般是通過為資料庫表增加一個”version”欄位來實現讀取出資料時,將此版本號一同讀出,之後更新時,對此版本號

使用rediszookeeper實現分散式

1.分散式鎖   分散式鎖一般用在分散式系統或者多個應用中,用來控制同一任務是否執行或者任務的執行順序。在專案中,部署了多個tomcat應用,在執行定時任務時就會遇到同一任務可能執行多次的情況,我們可以藉助分散式鎖,保證在同一時間只有一個tomcat應用執行了定時任務。 &nb

springboot-通過註解aop實現分散式

一、原因 1、在分散式專案中,使用者觸發插入、更新等操作,我們只需要其中一個服務執行,如果不加分散式鎖,後果很嚴重 二、方法 1、分佈鎖一般通過redis實現,主要通過setnx函式向redis儲存一個key,value等於儲存時的時間戳,並設定過期時間,然後返回true; 2、