1. 程式人生 > >SpringBoot實戰實現分布式鎖一之重現多線程高並發場景

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

-a 數據庫表 創建 book 前言 分解 bind result 上下

實戰前言:上篇博文我總體介紹了我這套視頻課程:“SpringBoot實戰實現分布式鎖” 總體涉及的內容,從本篇文章開始,我將開始介紹其中涉及到的相關知識要點,感興趣的小夥伴可以關註關註學習學習!!工欲善其事,必先利其器,介紹分布式鎖使用的前因後果之前,得先想辦法說清楚為啥需要分布式鎖以及如何才需要將分布式鎖搬上用場!!
其中,該課程的學習鏈接:http://edu.51cto.com/course/15684.html
感興趣的童鞋可以前往觀看學習!!!

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

實戰內容
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 的設置)!
技術分享圖片

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甚至更多個線程!
技術分享圖片

(2)接著我們設置 “HTTP信息頭管理器” ,因為我們的搶單接口接收的媒體類型是 json格式的post請求!
技術分享圖片

(3)接著我們創建 “HTTP請求” ,設置我們的項目上下文、端口以及我們的請求接口路徑跟方法體(ProductLockDto的字段:商品的id跟需要搶的量stock)
技術分享圖片

(4)最後我們設置stock字段來源於我們配置的CSV數據文件設置中讀取的變量stock 的值,即代表我們的用戶可以任意隨機的下單一定的量!!
技術分享圖片

(5)其中的csv文件是長這樣的:
技術分享圖片

9、最後,我們點擊這一按鈕,即開啟了 1s 內啟動100個並發線程對設定的產品進行 “搶” 的請求。
技術分享圖片

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

11、下面是搶單接口的打印日誌以及數據庫最終對這一商品更新的結果:
技術分享圖片

技術分享圖片

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

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

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

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