1. 程式人生 > >MySQL樂觀鎖在分散式場景下的實踐

MySQL樂觀鎖在分散式場景下的實踐

640

java葵花寶典

堅持更新java綜合性知識,自助領取海量java資料

640?640

作者:達兔哥

原文連結:https://segmentfault.com/a/1190000008935924

背景

在電商購物的場景下,當我們點選購物時,後端服務就會對相應的商品進行減庫存操作。在單例項部署的情況,我們可以簡單地使用JVM提供的鎖機制對減庫存操作進行加鎖,防止多個使用者同時點選購買後導致的庫存不一致問題。

但在實踐中,為了提高系統的可用性,我們一般都會進行多例項部署。而不同例項有各自的JVM,被負載均衡到不同例項上的使用者請求不能通過JVM的鎖機制實現互斥。

因此,為了保證在分散式場景下的資料一致性,我們一般有兩種實踐方式:一、使用MySQL樂觀鎖;二、使用分散式鎖。

本文主要介紹MySQL樂觀鎖,關於分散式鎖我在下一篇部落格中介紹。

樂觀鎖簡介

樂觀鎖(Optimistic Locking)與悲觀鎖相對應,我們在使用樂觀鎖時會假設資料在極大多數情況下不會形成衝突,因此只有在資料提交的時候,才會對資料是否產生衝突進行檢驗。如果產生資料衝突了,則返回錯誤資訊,進行相應的處理。

那我們如何來實現樂觀鎖呢?一般採用以下方式:使用版本號(version)機制來實現,這是樂觀鎖最常用的實現方式。

版本號

那什麼是版本號呢?版本號就是為資料新增一個版本標誌,通常我會為資料庫中的表新增一個int型別的"version"欄位。當我們將資料讀出時,我們會將version欄位一併讀出;當資料進行更新時,會對這條資料的version值加1。當我們提交資料的時候,會判斷資料庫中的當前版本號和第一次取資料時的版本號是否一致,如果兩個版本號相等,則更新,否則就認為資料過期,返回錯誤資訊。我們可以用下圖來說明問題:

如圖所示,如果更新操作如第一個圖中一樣順序執行,則資料的版本號會依次遞增,不會有衝突出現。但是像第二個圖中一樣,不同的使用者操作讀取到資料的同一個版本,再分別對資料進行更新操作,則使用者的A的更新操作可以成功,使用者B更新時,資料的版本號已經變化,所以更新失敗。

程式碼實踐

我們對某個商品減庫存時,具體操作分為以下3個步驟:

  • 查詢出商品的具體資訊

  • 根據具體的減庫存數量,生成相應的更新物件

  • 修改商品的庫存數量

為了使用MySQL的樂觀鎖,我們需要為商品表goods加一個版本號欄位version,具體的表結構如下:

CREATE TABLE `goods` (

 `id` int(11) NOT NULL AUTO_INCREMENT,

 `name`
varchar(64) NOT NULL DEFAULT '',

 `remaining_number` int(11) NOT NULL,

 `version` int(11) NOT NULL,

 PRIMARY KEY (`id`)

) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

Goods類的Java程式碼:

/* 商品名字
    */

   private String name;
   /**
    * 庫存數量
    */

   private Integer remainingNumber;
   /**
    * 版本號
    */

   private Integer version;

   @Override
   public String toString() {
       return "Goods{" +
               "id=" + id +
               ", name='" + name + '\'' +
               ", remainingNumber=" + remainingNumber +
               ", version=" + version +
               '}';
   }

}

GoodsMapper.java:

public interface GoodsMapper {
   Integer updateGoodCAS(Goods good);
}

GoodsMapper.xml如下:

<update id="updateGoodCAS" parameterType="com.ztl.domain.Goods">

       <![CDATA[
         update goods
         set `name`=#{name},
         remaining_number=#{remainingNumber},
         version=version+1
         where id=#{id} and version=#{version}
       ]]>

   </update>

GoodsService.java 介面如下:

public interface GoodsService {

   @Transactional
   Boolean updateGoodCAS(Integer id, Integer decreaseNum);

}

GoodsServiceImpl.java類如下:

@Service

public class GoodsServiceImpl implements GoodsService {

   @Autowired
   private GoodsMapper goodsMapper;

   @Override
   public Boolean updateGoodCAS(Integer id, Integer decreaseNum) {
       Goods good = goodsMapper.selectGoodById(id);
       System.out.println(good);
       try {
           Thread.sleep(3000);    
       } catch (InterruptedException e) {
           e.printStackTrace();
       }

       good.setRemainingNumber(good.getRemainingNumber() - decreaseNum);
       int result = goodsMapper.updateGoodCAS(good);
       System.out.println(result == 1 ? "success" : "fail");
       return result == 1;
   }

}

GoodsServiceImplTest.java測試類

@RunWith(SpringRunner.class)

@SpringBootTest

public class GoodsServiceImplTest {

   @Autowired
   private GoodsService goodsService;

   @Test
   public void updateGoodCASTest() {

       final Integer id = 1;
       Thread thread = new Thread(new Runnable() {
           @Override
           public void run() {
               goodsService.updateGoodCAS(id, 1);  
           }
       });

       thread.start();
       goodsService.updateGoodCAS(id, 2);          
       System.out.println(goodsService.selectGoodById(id));

   }

}

輸出結果:

Goods{id=1, name='手機', remainingNumber=10, version=9}
Goods{id=1, name='手機', remainingNumber=10, version=9}
success
fail
Goods{id=1, name='手機', remainingNumber=8, version=10}
程式碼說明:

在updateGoodCASTest()的測試方法中,使用者1和使用者2同時查出id=1的商品的同一個版本資訊,然後分別對商品進行庫存減1和減2的操作。從輸出的結果可以看出使用者2的減庫存操作成功了,商品庫存成功減去2;而使用者1提交減庫存操作時,資料版本號已經改變,所以資料變更失敗。這樣,我們就可以通過MySQL的樂觀鎖機制保證在分散式場景下的資料一致性。

640

640