1. 程式人生 > >SpringBoot實戰實現分散式鎖一之重現多執行緒高併發場景

SpringBoot實戰實現分散式鎖一之重現多執行緒高併發場景

實戰前言:上篇博文我總體介紹了我這套視訊課程:“SpringBoot實戰實現分散式鎖” 總體涉及的內容,從本篇文章開始,我將開始介紹其中涉及到的相關知識要點,感興趣的小夥伴可以關注關注學習學習!!工欲善其事,必先利其器,介紹分散式鎖使用的前因後果之前,得先想辦法說清楚為啥需要分散式鎖以及如何才需要將分散式鎖搬上用場!!

實戰概要:故而此文將介紹一下分散式鎖出現的背景以及如何才能將分散式鎖搬上用場(即如何重新多執行緒高併發的場景)。

實戰內容
1、“同一時刻多個執行緒高併發下訪問共享資源”的場景在當前網際網路產品或者專案下並不少見,這一場景隨之帶來的問題便顯而易見:這一共享資源在併發訪問的前後出現了資料不一致或者並非預期出現的結果的現象!!簡而言之,這種現象其實就是大夥熟悉的 “高併發多執行緒訪問共享資源時需要加同步程式碼塊”的口頭語(甚至可以說是面試時常見的對白了!)

2、單體應用時代加“同步鎖”常見的方式是利用jdk天然提供的類/元件:ReentrantLock或者Synchronized,但在分散式系統架構下專案一般以微服務的方式開發、獨立部署甚至叢集部署,當不同的服務或者叢集環境同一服務不同例項發生對共享資源的高併發訪問時,ReentrantLock或者Synchronized 的方式將很難解決 “高併發導致資料不一致或者併發預期出現的結果”的問題!!

3、於是乎,“分散式鎖”便出現了,“分散式鎖”其實只是一解決方案,並非一專有元件或者類,實現這一解決方案仍舊需要藉助額外的元件或者中介軟體來輔助,甚至某些情況下,需要藉助資料庫級別的方式來實現。總體來說,目前較為流行的解決方式還是有很多種,在我的視訊課程或者文章中,我將介紹一下幾種方式來實戰實現 “分散式鎖”
(1)資料庫級別鎖-樂觀悲觀鎖
(2)基於Redis的原子操作實現分散式鎖
(3)基於Zookeeper實戰實現分散式鎖
(4)基於Redisson實戰實現分散式鎖

4、既然我們知道分散式鎖出現的背景以及其相應的實戰實現方式,那我們回到本篇文章的核心內容:重現多執行緒高併發訪問共享資源的場景

5、下面我們以“商城系統/秒殺系統搶單”場景為例,藉助Jmeter測試工具,基於SpringBoot微服務專案重現高併發多執行緒訪問共享資源的場景!即:重現1秒內100執行緒、1000執行緒、10000執行緒等充當搶單請求對一商品進行搶單!!!

6、這一場景其實很像“搶微信紅包”、“某一商城如小米商城飢餓營銷時搶手機”等業務場景。下面我們大概模擬重現其中的核心邏輯-即搶單的過程:建庫-spring_boot_distribute,建一商品資訊表語句如下(mysql5.6版本):

CREATE TABLE `product_lock` (  
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`product_no` varchar(255) DEFAULT NULL COMMENT '產品編號',
`stock` int(11) DEFAULT NULL COMMENT '庫存量',
`version` int(11) DEFAULT NULL COMMENT '版本號',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_unique` (`id`) USING BTREE
ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='產品資訊表';

並在其中錄入一個商品的資訊(主要是庫存 stock 的設定)!
enter image description here

7、接著採用IDEA的SpringBoot Initializr元件構建多模組的SpringBoot微服務專案,並開發一Controller跟一Service,採用Mybatis逆向工程生成上面那張資料庫表對應的entity、mapper、mapper.xml,相關程式碼以及截圖如下:

@Service
public class DataLockService {

private static final Logger log= LoggerFactory.getLogger(DataLockService.class);

@Autowired
private ProductLockMapper lockMapper;

/**
 * 正常更新商品庫存 - 重現了高併發的場景
 * @param dto
 * @return
 * @throws Exception
 */
@Transactional(rollbackFor = Exception.class)
public int updateStock(ProductLockDto dto) throws Exception{
    int res=0;

    ProductLock entity=lockMapper.selectByPrimaryKey(dto.getId());
    if (entity!=null && entity.getStock().compareTo(dto.getStock())>=0){
        entity.setStock(dto.getStock());
        return lockMapper.updateStock(entity);
    }

    return res;
}}

DataLockController程式碼如下:

@RestController
public class DataLockController {

private static final Logger log= LoggerFactory.getLogger(DataLockController.class);

private static final String prefix="lock";

@Autowired
private DataLockService dataLockService;

/**
 * 更新商品庫存-1
 * @param dto
 * @param bindingResult
 * @return
 */
@RequestMapping(value = prefix+"/data/base/positive/update",method = RequestMethod.POST,consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
public BaseResponse dataBasePositive(@RequestBody @Validated ProductLockDto dto, BindingResult bindingResult){
    if (bindingResult.hasErrors()){
        return new BaseResponse(StatusCode.InvalidParam);
    }

    BaseResponse response=new BaseResponse(StatusCode.Ok);
    try {
        log.debug("當前請求資料:{} ",dto);

        int res=dataLockService.updateStock(dto);
        if (res<=0) {
            return new BaseResponse(StatusCode.Fail);
        }
    }catch (Exception e){
        log.error("發生異常:",e.fillInStackTrace());
        response=new BaseResponse(StatusCode.Fail);
    }
    return response;
}}

ProductLockDto 程式碼如下:

@Data
@ToString
public class ProductLockDto {
@NotNull
private Integer id;

@NotNull
private Integer stock=1;}

Mybatis逆向工程生成的那三個元件就不貼了,在這裡貼一下 DataLockService呼叫的ProductLockMapper更新庫存的方法以及動態sql的寫法:

ProductLockMapper類的方法: int updateStock(ProductLock lock);

ProductLockMapper.xml的動態sql:

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

8、至此簡單的搶單系統/商城秒殺系統的搶單場景就大致模擬好了,下面我們採用Jmeter測試工具來模擬這一高併發場景,Jmeter的相關設定如下:
(1)首先我們設定1s併發100個執行緒,後面你可以在這裡設定1000、10000甚至更多個執行緒!
enter image description here

(2)接著我們設定 “HTTP資訊頭管理器” ,因為我們的搶單介面接收的媒體型別是 json格式的post請求!
enter image description here

(3)接著我們建立 “HTTP請求” ,設定我們的專案上下文、埠以及我們的請求介面路徑跟方法體(ProductLockDto的欄位:商品的id跟需要搶的量stock)
enter image description here

(4)最後我們設定stock欄位來源於我們配置的CSV資料檔案設定中讀取的變數stock 的值,即代表我們的使用者可以任意隨機的下單一定的量!!
enter image description here

(5)其中的csv檔案是長這樣的:
enter image description here

9、最後,我們點選這一按鈕,即開啟了 1s 內啟動100個併發執行緒對設定的產品進行 “搶” 的請求。
enter image description here

10、這個時候,我們先對這一產品的庫存量在資料庫進行設定,我們設定為 100,即現有的庫存量為100。理論情況下,不管發生多少次的“哄搶”,“最終的庫存應當是被搶完而且應當是恰好被搶完,而且需要傳送相應的簡訊/通知告知使用者搶到了!!”,然後,現實是很殘酷的(當你按下那一個start run的按鈕時,資料庫最終出現的結果卻不是我們預期的那樣!!)

11、下面是搶單介面的列印日誌以及資料庫最終對這一商品更新的結果:
enter image description here

enter image description here

15、你會驚訝的看到,100個庫存在隨機產生的100個執行緒(每個執行緒庫存2或者5-csv檔案讀取的)更新之後竟然變成了負數(按道理來說,我們寫的資料庫更新邏輯以及程式碼判斷邏輯沒有多大問題啊!!!)

實戰分析:“按道理來說,我們寫的資料庫更新邏輯以及程式碼判斷邏輯沒有多大問題啊!!!”,實則不然,其實問題正是出在這兩點:資料庫更新邏輯 跟 程式碼判斷邏輯 。 欲知問題何在,請聽下回分解!!

實戰總結:本篇文章主要基於SpringBoot微服務專案重現了高併發多執行緒併發訪問同一共享資源時出現的問題,學習過程大夥若有相關問題可以加我QQ:1974544863 進行技術交流!若需要該課程的學習,亦可以加QQ進行諮詢!如果感興趣的童鞋,也可以關注關注我的公眾號哦!!