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


 

背景

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

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

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

本文主要介紹MySQL樂觀鎖,關於分布式鎖我在下一篇博客中介紹。

樂觀鎖簡介

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

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

版本號

那什么是版本號呢?版本號就是為數據添加一個版本標志,通常我會為數據庫中的表添加一個int類型的"version"字段。當我們將數據讀出時,我們會將version字段一並讀出;當數據進行更新時,會對這條數據的version值加1。當我們提交數據的時候,會判斷數據庫中的當前版本號和第一次取數據時的版本號是否一致,如果兩個版本號相等,則更新,否則就認為數據過期,返回錯誤信息。我們可以用下圖來說明問題:

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

代碼實踐

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

  1. 查詢出商品的具體信息

  2. 根據具體的減庫存數量,生成相應的更新對象

  3. 修改商品的庫存數量

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

1
2
3
4
5
6
7
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代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
* 商品名字
      */
     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:

1
2
3
4
5
public  interface  GoodsMapper {
 
     Integer updateGoodCAS(Goods good);
 
}

 

 

GoodsMapper.xml如下:

1
2
3
4
5
6
7
8
9
<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 接口如下:

1
2
3
4
5
public  interface  GoodsService {
 
     @Transactional
     Boolean updateGoodCAS(Integer id, Integer decreaseNum);
}

 

 

GoodsServiceImpl.java類如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@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測試類

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@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 );     //用戶1的請求
             }
         });
         thread.start();
         goodsService.updateGoodCAS(id,  2 );             //用戶2的請求
 
         System.out.println(goodsService.selectGoodById(id));
     }
}

 

 

輸出結果:

1
2
3
4
5
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的樂觀鎖機制保證在分布式場景下的數據一致性。

以上。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM