【轉】MySQL樂觀鎖在分布式情境下的實踐,mysql情境
背景
在電商購物的情境下,當我們點擊購物時,後端服務就會對相應的商品進行減庫存操作。在單一實例部署的情況,我們可以簡單地使用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類如下:
@Servicepublic 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)@SpringBootTestpublic 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)); }}
輸出結果:
Goods{id=1, name='手機', remainingNumber=10, version=9}Goods{id=1, name='手機', remainingNumber=10, version=9}successfailGoods{id=1, name='手機', remainingNumber=8, version=10}
代碼說明:
在updateGoodCASTest()的測試方法中,使用者1和使用者2同時查出id=1的商品的同一個版本資訊,然後分別對商品進行庫存減1和減2的操作。從輸出的結果可以看出使用者2的減庫存操作成功了,商品庫存成功減去2;而使用者1提交減庫存操作時,資料版本號碼已經改變,所以資料變更失敗。
這樣,我們就可以通過MySQL的樂觀鎖機制保證在分布式情境下的資料一致性。
以上。
原文連結
https://segmentfault.com/a/11...