1. 程式人生 > 資料庫 >分散式鎖(二)——基於資料庫實現分散式鎖

分散式鎖(二)——基於資料庫實現分散式鎖

上一篇部落格中簡單說了說什麼是分散式鎖,搭建了基本的環境(非常簡單)這篇部落格就需要開始正式體驗分散式鎖 了,由於是在單機上開發,沒有做叢集,但是程式碼方法的具體實現與叢集方面沒有二異,只能通過JMeter模擬多執行緒達到高併發的效果。

模擬業務場景

1、模擬資料庫中商品庫存銷售的SQL

  <!--更新庫存-->
  <update id="updateStock" parameterType="com.learn.lockmodel.entity.ProductLock">
    update product_lock
    set stock = stock - #{stock,jdbcType=INTEGER}
    where id = #{id,jdbcType=INTEGER}
  </update>

2、商品銷售的實體

其中的BindingResult就是校驗結果,之前介紹過,傳送門:spring boot中的引數校驗 其中的BaseResponse是封裝的統一訊息處理物件,具體程式碼如下:

@Data
public class BaseResponse<T> {

    private Integer code;
    private String msg;
    private T data;

    public BaseResponse(StatusCode statusCode) {
        this.code = statusCode.getCode();
        this.msg = statusCode.getMsg();
    }

    public BaseResponse(StatusCode statusCode,T data) {
        this.code = statusCode.getCode();
        this.msg = statusCode.getMsg();
        this.data = data;
    }

    public BaseResponse(Integer code,String msg) {
        this.code = code;
        this.msg = msg;
    }

    public BaseResponse(Integer code,String msg,T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }
}

5、postman進行驗證

商品id:10010,商品庫存:1000
在這裡插入圖片描述
利用postman構建如下請求
在這裡插入圖片描述
請求傳送成功之後會看到庫存正常減少
在這裡插入圖片描述

JMeter模擬高併發

JMeter在我看來能模擬我們開發中用到的所有場景,功能似乎比postman強大的多,Apache JMeter 介紹 下載地址:JMeter的各個版本下載地址

安裝

安裝就是一個Easy到爆的東西,解壓完成之後,進入到bin目錄下,點選jmeter.bat批處理指令碼(windows環境下,linux環境下點選jmeter.sh),就可以啟動jmeter
在這裡插入圖片描述

加入HTTP資訊頭管理器

滑鼠反鍵->新增->配置元件->HTTP資訊頭管理器。

在這裡插入圖片描述
之後需要在HTTP頭中增加HTTP的資料型別
在這裡插入圖片描述
這一點與postman不同,新增更加方便。

加入HTTP請求頭

滑鼠右鍵->新增->取樣器->HTTP請求
在這裡插入圖片描述
加入HTTP請求之後,需要配置url,埠等引數。
在這裡插入圖片描述
這裡的請求引數,我們採用的動態配置,JMeter可以通過動態配置變數,自動獲取指定檔案中的資料,這個就需要新增相應的資料檔案了。

加入測試資料檔案

滑鼠右鍵->新增->配置元件在這裡插入圖片描述
新增檔案之後,我們需要指定CSV檔案,並指定變數名稱:
在這裡插入圖片描述

這也就是為什麼在HTTP請求中指定${stock}變數的原因。這裡的資料CSV檔案中我們就指定了兩個資料——2,4。

設定執行緒組

忘了提一下,在開啟JMeter的時候,預設就會有一個測試計劃,在給測試計劃中添加了執行緒組之後,才可以新增HTTP資訊管理頭,HTTP請求頭和測試資料檔案。
在這裡插入圖片描述
設定相關執行緒組:
在這裡插入圖片描述

發起請求

發起請求,1000個執行緒處理完成之後,我們會發現如下情況
在這裡插入圖片描述
庫存變成負數,這是不能忍的(雖然一定程度下這隻能算是多執行緒的一種簡單模擬,但是如果這些程式碼放在多個伺服器上,多個JVM中執行,還是會出現庫存負數的情況)。

樂觀鎖與悲觀鎖

關於樂觀鎖和悲觀鎖的介紹這裡也不再贅述,樂觀鎖引入了一個版本號的概念,如果版本號不一致則表示已經有其他的操作對其進行了修改,資料就變成了髒資料。悲觀鎖無非就是讓所有的請求進行排隊。這些基本概念已經有大牛總結的非常到位了,百度一搜一大堆,這裡就直接附上一個連結即可。樂觀鎖與悲觀鎖簡介

基於資料庫的實現

資料庫是所用應用程式的資料來源,在資料庫階段完成資料的訪問限制,自然能實現分散式鎖的操作。

樂觀鎖的實現

引入版本號的欄位之後,在更新是匹配版本號與本執行緒是否一致,如果一致則更新資料,如果不一致則放棄更新。在基礎的程式碼基礎上,我們只需要修改SQL就可以實現。

<!--更新庫存-樂觀鎖v1-->
<update id="updateStockV1" parameterType="com.learn.lockmodel.entity.ProductLock">
  update product_lock set stock = stock - #{stock,jdbcType=INTEGER},version=version+1
  where id = #{id,jdbcType=INTEGER} and version = #{version,jdbcType=INTEGER} and stock > 0
</update>

where 條件中加入了version的判斷,在更新的時候同時讓版本號+1。

業務程式碼無需大的更改,只需要呼叫指定的資料訪問程式碼即可。

@RequestMapping(value=prefix+"/db/update/optimistic",method = RequestMethod.POST,consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
public BaseResponse dataBaseOptimisticLock(@RequestBody @Validated ProductLockDto productLockDto,BindingResult bindingResult){
    if(bindingResult.hasErrors()){
        return new BaseResponse(StatusCode.InvalidParam);
    }
    BaseResponse result = new BaseResponse(StatusCode.Ok);
    try{
        log.debug("當前請求資料:{}",productLockDto);
        int res = dataLockService.updateStockWithOptimisticLock(productLockDto);
        if(res<=0){//如果資料庫層面更新失敗,則直接購買失敗
            return new BaseResponse(StatusCode.Fail);
        }
    }catch (Exception e){
        log.error("更新商品庫存失敗,異常資訊為:{}",e.fillInStackTrace());
        result = new BaseResponse(StatusCode.Fail);
    }
    return result;
}

測試

為了便於計算,我們將所有的購買個數變成1,然後發起多個執行緒,進行壓力測試。

併發2000個執行緒,初始化庫存50000,啟動JMeter測試,如果版本號+庫存=50000則表示資料正常(只有在購買數都為1的時候才可以用這個公式進行驗證,任何時候公式都成立才表示資料正確)
在這裡插入圖片描述
測試結果如下圖
在這裡插入圖片描述

悲觀鎖的實現

我們回本溯源一下,出現數據不符合邏輯的情況,其實就是資料出現了髒讀讀取情況,如果用悲觀鎖實現,就需要在讀取資料的時候,加上X 鎖,關於資料庫的X鎖和S鎖可以參見這篇部落格——MySql(三)——事務和鎖

加入X鎖

利用for update語句,給資料庫讀取的時候增加上X鎖,這樣就能避免資料出現髒讀的情況。

  <!--根據主鍵查詢for update 悲觀鎖-->
  <select id="selectByPKForNegative" resultType="com.learn.lockmodel.entity.ProductLock">
    SELECT <include refid="Base_Column_List"/> FROM product_lock
    WHERE id=#{id} FOR UPDATE
  </select>

加入for update語句,用於加入X鎖。

業務程式碼層面需要做的變更依舊很少,只是需要變更資料查詢的邏輯就可以了。如下所示:

/*
* 悲觀鎖的更新操作
 * @param dto
 * @return
 */
@Transactional(rollbackFor = Exception.class)
public int updateStockNegativeLock(ProductLockDto dto){
    int res = 0;
    //獲取庫存資料的時候,加上X鎖
    ProductLock negativeLockEntity = lockMapper.selectByPKForNegative(dto.getId());
    if(negativeLockEntity!=null && negativeLockEntity.getStock().compareTo(dto.getStock())>=0){
        negativeLockEntity.setStock(dto.getStock());
        res = lockMapper.updateStockForNegative(negativeLockEntity);
        if(res>0){//搶購成功
            log.info("下單搶購商品成功,stock{}",negativeLockEntity.getStock());
        }else{
            log.error("搶購失敗");
        }
        return res;
    }
    return res;
}

controller的例項:

 /*
 * 悲觀鎖更新資料庫
 * @param productLockDto
 * @param bindingResult
 * @return
 */
@RequestMapping(value=prefix+"/db/update/negative",consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
public BaseResponse dataBaseNegativeLock(@RequestBody @Validated ProductLockDto productLockDto,productLockDto);
        int res = dataLockService.updateStockNegativeLock(productLockDto);
        if(res<=0){//如果資料庫層面更新失敗,則直接購買失敗
            return new BaseResponse(StatusCode.Fail);
        }
    }catch (Exception e){
        log.error("更新商品庫存失敗,異常資訊為:{}",e.fillInStackTrace());
        result = new BaseResponse(StatusCode.Fail);
    }
    return result;
}

**邏輯上這種鎖是直接載入MySQL層面,每一個請求無法更新資料的時候會等待,知道資料更新成功,因此只要資料庫的連線數是充足的,則並不會像樂觀鎖那樣出現很多更新失敗的情況。**測試結果如下所示:

在這裡插入圖片描述

總結

本篇部落格從例項出發,介紹了資料庫層面的樂觀鎖和悲觀鎖。