SpringBoot實戰實現分佈式鎖一之重現多線程高併發場景

實戰前言:上篇博文我整體介紹了我這套視頻課程:「SpringBoot實戰實現分佈式鎖」 整體涉及的內容,從本篇文章開始,我將開始介紹其中涉及到的相關知識要點,感興趣的小夥伴能夠關注關注學習學習!!工欲善其事,必先利其器,介紹分佈式鎖使用的來龍去脈以前,得先想辦法說清楚爲啥須要分佈式鎖以及如何才須要將分佈式鎖搬上用場!!
其中,該課程的學習連接http://edu.51cto.com/course/15684.html
感興趣的童鞋能夠前往觀看學習!!!html

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

實戰內容
一、「同一時刻多個線程高併發下訪問共享資源」的場景在當前互聯網產品或者項目下並很多見,這一場景隨之帶來的問題便顯而易見:這一共享資源在併發訪問的先後出現了數據不一致或者並不是預期出現的結果的現象!!簡而言之,這種現象其實就是大夥熟悉的 「高併發多線程訪問共享資源時須要加同步代碼塊」的口頭語(甚至能夠說是面試時常見的對白了!)面試

二、單體應用時代加「同步鎖」常見的方式是利用jdk自然提供的類/組件:ReentrantLock或者Synchronized,但在分佈式系統架構下項目通常以微服務的方式開發、獨立部署甚至集羣部署,當不一樣的服務或者集羣環境同一服務不一樣實例發生對共享資源的高併發訪問時,ReentrantLock或者Synchronized 的方式將很難解決 「高併發致使數據不一致或者併發預期出現的結果」的問題!!spring

三、因而乎,「分佈式鎖」便出現了,「分佈式鎖」其實只是一解決方案,並不是一專有組件或者類,實現這一解決方案仍舊須要藉助額外的組件或者中間件來輔助,甚至某些狀況下,須要藉助數據庫級別的方式來實現。整體來講,目前較爲流行的解決方式仍是有不少種,在個人視頻課程或者文章中,我將介紹一下幾種方式來實戰實現 「分佈式鎖」
(1)數據庫級別鎖-樂觀悲觀鎖
(2)基於Redis的原子操做實現分佈式鎖
(3)基於Zookeeper實戰實現分佈式鎖
(4)基於Redisson實戰實現分佈式鎖sql

四、既然咱們知道分佈式鎖出現的背景以及其相應的實戰實現方式,那咱們回到本篇文章的核心內容:重現多線程高併發訪問共享資源的場景數據庫

五、下面咱們以「商城系統/秒殺系統搶單」場景爲例,藉助Jmeter測試工具,基於SpringBoot微服務項目重現高併發多線程訪問共享資源的場景!即:重現1秒內100線程、1000線程、10000線程等充當搶單請求對一商品進行搶單!!!json

六、這一場景其實很像「搶微信紅包」、「某一商城如小米商城飢餓營銷時搶手機」等業務場景。下面咱們大概模擬重現其中的核心邏輯-即搶單的過程:建庫-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多線程

七、接着採用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>

八、至此簡單的搶單系統/商城秒殺系統的搶單場景就大體模擬好了,下面咱們採用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

九、最後,咱們點擊這一按鈕,即開啓了 1s 內啓動100個併發線程對設定的產品進行 「搶」 的請求。
enter image description here

十、這個時候,咱們先對這一產品的庫存量在數據庫進行設置,咱們設置爲 100,即現有的庫存量爲100。理論狀況下,無論發生多少次的「哄搶」,「最終的庫存應當是被搶完並且應當是剛好被搶完,並且須要發送相應的短信/通知告知用戶搶到了!!」,而後,現實是很殘酷的(當你按下那一個start run的按鈕時,數據庫最終出現的結果卻不是咱們預期的那樣!!)

十一、下面是搶單接口的打印日誌以及數據庫最終對這一商品更新的結果:
enter image description here

enter image description here

1五、你會驚訝的看到,100個庫存在隨機產生的100個線程(每一個線程庫存2或者5-csv文件讀取的)更新以後居然變成了負數(按道理來講,咱們寫的數據庫更新邏輯以及代碼判斷邏輯沒有多大問題啊!!!)

實戰分析:「按道理來講,咱們寫的數據庫更新邏輯以及代碼判斷邏輯沒有多大問題啊!!!」,實則否則,其實問題正是出在這兩點:數據庫更新邏輯 跟 代碼判斷邏輯 。 欲知問題何在,請聽下回分解!!

實戰總結:本篇文章主要基於SpringBoot微服務項目重現了高併發多線程併發訪問同一共享資源時出現的問題,學習過程大夥如有相關問題能夠加我QQ:1974544863 進行技術交流!若須要該課程的學習,亦能夠加QQ進行諮詢!若是感興趣的童鞋,也能夠結合課程學習(掌握得更快哦):http://edu.51cto.com/course/15684.html

相關文章
相關標籤/搜索