使用mysql樂觀鎖解決並發問題

來源:互聯網
上載者:User

標籤:大於   oid   src   lock   sdn   產生   使用   nod   相對   

案例說明:

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

樂觀鎖機制在一定程度上解決了這個問題。樂觀鎖,大多是基於資料版本(Version)記錄機制實現。何謂資料版本?即為資料增加一個版本標識,在基於資料庫表的版本解決方案中,一般是通過為資料庫表增加一個 “version” 欄位來實現。

讀取出資料時,將此版本號碼一同讀出,之後更新時,對此版本號碼加一。此時,將提交資料的版本資料與資料庫表對應記錄的目前的版本資訊進行比對,如果提交的資料版本號碼大於資料庫表目前的版本號,則予以更新,否則認為是到期資料。

對於上面修改使用者帳戶資訊的例子而言,假設資料庫中帳戶資訊表中有一個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的操作結果的可能。

樂觀鎖介紹:

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

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

如所示,如果更新操作順序執行,則資料的版本(version)依次遞增,不會產生衝突。但是如果發生有不同的業務操作對同一版本的資料進行修改,那麼,先提交的操作(圖中B)會把資料version更新為2,當A在B之後提交更新時探索資料的version已經被修改了,那麼A的更新操作會失敗。

 

2.樂觀鎖定的第二種實現方式和第一種差不多,同樣是在需要樂觀鎖控制的table中增加一個欄位,名稱無所謂,欄位類型使用時間戳(timestamp), 和上面的version類似,也是在更新提交的時候檢查當前資料庫中資料的時間戳記和自己更新前取到的時間戳記進行對比,如果一致則OK,否則就是版本衝突。

使用舉例:以MySQL InnoDB為例

還是拿之前的執行個體來舉:商品goods表中有一個欄位status,status為1代表商品未被下單,status為2代表商品已經被下單,那麼我們對某個商品下單時必須確保該商品status為1。假設商品的id為1。

 

下單操作包括3步驟:

1.查詢出商品資訊

select (status,status,version) from t_goods where id=#{id}

2.根據商品資訊產生訂單

3.修改商品status為2

update t_goods 

set status=2,version=version+1

where id=#{id} and version=#{version};

 

那麼為了使用樂觀鎖,我們首先修改t_goods表,增加一個version欄位,資料預設version值為1。

t_goods表初始資料如下:

mysql> select * from t_goods;  +----+--------+------+---------+  | id | status | name | version |  +----+--------+------+---------+  |  1 |      1 | 道具 |       1 |  |  2 |      2 | 裝備 |       2 |  +----+--------+------+---------+  2 rows in set    mysql>  

對於樂觀鎖的實現,我使用MyBatis來進行實踐,具體如下:

Goods實體類:

/**  * ClassName: Goods <br/>  * Function: 商品實體. <br/>  * date: 2013-5-8 上午09:16:19 <br/>  * @author [email protected]  */  public class Goods implements Serializable {        /**      * serialVersionUID:序列化ID.      */      private static final long serialVersionUID = 6803791908148880587L;            /**      * id:主鍵id.      */      private int id;            /**      * status:商品狀態:1未下單、2已下單.      */      private int status;            /**      * name:商品名稱.      */      private String name;            /**      * version:商品資料版本號碼.      */      private int version;            @Override      public String toString(){          return "good id:"+id+",goods status:"+status+",goods name:"+name+",goods version:"+version;      }        //setter and getter    }  

GoodsDao

/**  * updateGoodsUseCAS:使用CAS(Compare and set)更新商品資訊. <br/>  *  * @author [email protected]  * @param goods 商品對象  * @return 影響的行數  */  int updateGoodsUseCAS(Goods goods);  

mapper.xml

<update id="updateGoodsUseCAS" parameterType="Goods">      <![CDATA[         update t_goods         set status=#{status},name=#{name},version=version+1         where id=#{id} and version=#{version}     ]]>  </update>  

GoodsDaoTest測試類別

@Test  public void goodsDaoTest(){      int goodsId = 1;      //根據相同的id查詢出商品資訊,賦給2個對象      Goods goods1 = this.goodsDao.getGoodsById(goodsId);      Goods goods2 = this.goodsDao.getGoodsById(goodsId);            //列印當前商品資訊      System.out.println(goods1);      System.out.println(goods2);            //更新商品資訊1      goods1.setStatus(2);//修改status為2      int updateResult1 = this.goodsDao.updateGoodsUseCAS(goods1);      System.out.println("修改商品資訊1"+(updateResult1==1?"成功":"失敗"));            //更新商品資訊2      goods1.setStatus(2);//修改status為2      int updateResult2 = this.goodsDao.updateGoodsUseCAS(goods2);      System.out.println("修改商品資訊2"+(updateResult2==1?"成功":"失敗"));  }  

輸出結果:

good id:1,goods status:1,goods name:道具,goods version:1  good id:1,goods status:1,goods name:道具,goods version:1  修改商品資訊1成功  修改商品資訊2失敗  

說明:

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

mysql> select * from t_goods;  +----+--------+------+---------+  | id | status | name | version |  +----+--------+------+---------+  |  1 |      2 | 道具 |       2 |  |  2 |      2 | 裝備 |       2 |  +----+--------+------+---------+  2 rows in set    mysql>  

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

update t_goods   set status=2,version=version+1  where id=#{id} and version=#{version};  

這樣我們就實現了樂觀鎖

 

以上就是我對MySQL樂觀鎖的總結和實踐,寫得比較淺顯,有不對的地方歡迎拍磚

 參考來源:

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

http://blog.csdn.net/liyantianmin/article/details/50752102

使用mysql樂觀鎖解決並發問題

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.