最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Spring?Retry?實(shí)現(xiàn)樂觀鎖重試實(shí)踐記錄

 更新時(shí)間:2025年03月01日 11:34:13   作者:matrixlzp  
本文介紹了在秒殺商品SKU表中使用樂觀鎖和MybatisPlus配置樂觀鎖的方法,并分析了測試環(huán)境和生產(chǎn)環(huán)境的隔離級別對樂觀鎖的影響,通過簡單驗(yàn)證,展示了在可重復(fù)讀和讀已提交隔離級別下的不同行為,感興趣的朋友一起看看吧

一、場景分析

假設(shè)有這么一張表:

create table pms_sec_kill_sku
(
    id             int auto_increment comment '主鍵ID'
        primary key,
    spec_detail    varchar(50)              not null comment '規(guī)格描述',
    purchase_price decimal(10, 2)           not null comment '采購價(jià)格',
    sale_price     decimal(10, 2)           not null comment '銷售價(jià)格',
    origin_stock   int unsigned default '0' not null comment '初始庫存',
    sold_stock     int unsigned default '0' not null comment '已售庫存',
    stock          int unsigned default '0' not null comment '實(shí)時(shí)庫存',
    occupy_stock   int unsigned default '0' not null comment '訂單占用庫存',
    version        int          default 0   not null comment '樂觀鎖版本號',
    created_time   datetime                 not null comment '創(chuàng)建時(shí)間',
    updated_time   datetime                 not null comment '更新時(shí)間'
)
    comment '促銷管理服務(wù)-秒殺商品SKU';

一張簡單的秒殺商品SKU表。使用 version 字段做樂觀鎖。使用 unsigned 關(guān)鍵字,限制 int 類型非負(fù),防止庫存超賣。

 使用 MybatisPlus 來配置樂觀鎖:

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@EnableTransactionManagement
@Configuration
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 分頁插件
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        // 樂觀鎖插件
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return interceptor;
    }
    @Bean
    public DefaultDBFieldHandler defaultDBFieldHandler() {
        DefaultDBFieldHandler defaultDBFieldHandler = new DefaultDBFieldHandler();
        return defaultDBFieldHandler;
    }
}
@Data
@TableName("pms_sec_kill_sku")
@ApiModel(value = "PmsSecKillSku對象", description = "促銷管理-秒殺商品SKU")
public class PmsSecKillSku implements Serializable {
    private static final long serialVersionUID = 1L;
    // @Version 注解不能遺漏
    @Version
    @ApiModelProperty("樂觀鎖版本號")
    private Integer version;
    // other properties ......
}

 現(xiàn)在有這么一個(gè)支付回調(diào)接口:

/**
 * 促銷管理-秒殺商品SKU 服務(wù)實(shí)現(xiàn)類
 * @since 2025-02-26 14:21:42
 */
@Service
public class PmsSecKillSkuServiceImpl extends ServiceImpl<PmsSecKillSkuMapper, PmsSecKillSku> implements PmsSecKillSkuService {
    // 最大重試次數(shù)
    private static final int MAX_RETRIES = 3;
    /**
     * 訂單支付成功回調(diào)
     * 假設(shè)每次只能秒殺一個(gè)數(shù)量的SKU
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void paySucCallback(Integer skuId) {
        // 持久化庫存
        int count = 0, retries = 0;
        while (count == 0 && retries < MAX_RETRIES) {
            PmsSecKillSkuVo pmsSkuVo = this.baseMapper.findDetailById(skuId);
            PmsSecKillSku wt = new PmsSecKillSku();
            wt.setId(pmsSkuVo.getId());
            wt.setVersion(pmsSkuVo.getVersion());
            // 占用庫存減1
            wt.setOccupyStock( pmsSkuVo.getOccupyStock()-1 );
            // 已售庫存加1
            wt.setSoldStock( pmsSkuVo.getSoldStock()+1 );
            // 實(shí)時(shí)庫存減1
            wt.setStock( pmsSkuVo.getStock()-1 );
            count = this.baseMapper.updateById(wt);
            retries++;
            if (count == 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new BusinessException(e.getMessage());
                }
            }
        }
        if (count == 0) {
            throw new BusinessException("請刷新后重新取消!");
        }
    }
}

該方法的目的,是為了進(jìn)行庫存更新,當(dāng)樂觀鎖版本號有沖突時(shí),對方法進(jìn)行休眠重試。

該方法在測試環(huán)境還能正常跑,到了生產(chǎn)環(huán)境,卻頻繁報(bào) "請刷新后重新取消!"

仔細(xì)分析后發(fā)現(xiàn),測試環(huán)境的MYSQL數(shù)據(jù)庫全局隔離級別是,READ-COMMITTED(讀已提交)。而生產(chǎn)環(huán)境是 REPEATABLE_READ(可重復(fù)讀)。

SHOW GLOBAL VARIABLES LIKE 'transaction_isolation';
  • 在讀已提交隔離級別下,該方法每次重試,都能讀取到別的事務(wù)提交的最新的 version,相當(dāng)于拿到樂觀鎖。
  • 在可重復(fù)讀隔離級別下,因?yàn)橛?MVCC 多版本并發(fā)控制,該方法每次重試,讀取到的都是同一個(gè)結(jié)果,相當(dāng)于一直拿不到樂觀鎖。所以多次循環(huán)之后,count 還是等于 0,程序拋出異常。

 二、簡單驗(yàn)證

假設(shè)現(xiàn)在表里面有這么一條記錄:

INSERT INTO `pms_sec_kill_sku` 
(`id`, `spec_detail`, `purchase_price`, `sale_price`, 
`origin_stock`, `sold_stock`, `stock`, `occupy_stock`, 
`version`, `created_time`, `updated_time`) VALUES 
(1, '尺碼:M1', 100.00, 1.00, 
2, 0, 2, 2, 
2, '2025-02-26 15:51:22', '2025-02-26 15:51:24');

 2.1、可重復(fù)讀

 修改服務(wù)程序:

  • 增加會話的隔離級別為 isolation = Isolation.REPEATABLE_READ 可以重復(fù)讀。
  • 增加記錄日志。
  • 在方法更新前阻塞當(dāng)前線程,模擬另一個(gè)事務(wù)先提交。
@Api(value = "促銷管理-秒殺商品SKU", tags = {"促銷管理-秒殺商品SKU接口"})
@RestController
@RequiredArgsConstructor
@RequestMapping("pmsSecKillSku")
public class PmsSecKillSkuController {
    private final PmsSecKillSkuService pmsSecKillSkuService;
    @ApiOperation(value = "支付成功", notes = "支付成功")
    @PostMapping("/pay")
    public R<Void> pay(Integer id) {
        pmsSecKillSkuService.paySucCallback(id);
        return R.success();
    }
}

訪問接口:

###
POST http://localhost:5910/pmsSecKillSku/pay?id=1
Content-Type: application/json
token: 123

在第0次查詢的時(shí)候,執(zhí)行更新:

UPDATE `pms_sec_kill_sku`
SET sold_stock = sold_stock + 1, stock = stock - 1, 
occupy_stock = occupy_stock - 1, version = version + 1
WHERE id = 1;

可以看到,三次查詢,返回結(jié)果都是一樣的:

數(shù)據(jù)庫的版本號只有3:

 2.2、讀已提交

修改會話的隔離級別為 isolation = Isolation.READ_COMMITTED 讀已提交。

恢復(fù)數(shù)據(jù):

訪問接口:

###
POST http://localhost:5910/pmsSecKillSku/pay?id=1
Content-Type: application/json
token: 123

 在第0次查詢的時(shí)候,執(zhí)行更新:

UPDATE `pms_sec_kill_sku`
SET sold_stock = sold_stock + 1, stock = stock - 1, 
occupy_stock = occupy_stock - 1, version = version + 1
WHERE id = 1;

 可以看到,第0次查詢的時(shí)候,version=2;執(zhí)行完 SQL語句,第1次查詢的時(shí)候,version=3;拿到了樂觀鎖,更新成功。

 三、最佳實(shí)踐

可以看到,使用 Thread.sleep 配合循環(huán)來進(jìn)行獲取樂觀鎖的重試,存在一些問題:

  • 依賴事務(wù)隔離級別的正確設(shè)置。
  • 休眠的時(shí)間不好把控。
  • 代碼復(fù)用性差。

Spring Retry 提供了一種更優(yōu)雅的方式,來進(jìn)行樂觀鎖的重試。

 恢復(fù)數(shù)據(jù):

 3.1、配置重試模板

import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.retry.backoff.FixedBackOffPolicy;
import org.springframework.retry.support.RetryTemplate;
@Configuration
@EnableRetry
public class RetryConfig {
    @Bean
    public RetryTemplate retryTemplate() {
        RetryTemplate retryTemplate = new RetryTemplate();
        // 設(shè)置重試策略,這里設(shè)置最大重試次數(shù)為3次
        SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(3);
        retryTemplate.setRetryPolicy(retryPolicy);
        // 設(shè)置重試間隔時(shí)間,這里設(shè)置為固定的500毫秒
        // 可以根據(jù)系統(tǒng)的并發(fā)度,來設(shè)置
        // 并發(fā)度高,設(shè)置長一點(diǎn),并發(fā)度低,設(shè)置短一點(diǎn)
        FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
        backOffPolicy.setBackOffPeriod(500);
        retryTemplate.setBackOffPolicy(backOffPolicy);
        return retryTemplate;
    }
}

 3.2、使用 Spring 的@Retryable注解

@Override
    @Retryable(value = OptimisticLockingFailureException.class)
    @Transactional(rollbackFor = Exception.class, 
                    isolation = Isolation.REPEATABLE_READ)
    public void paySucRetry(Integer skuId) {
        PmsSecKillSkuVo pmsSkuVo = this.baseMapper.findDetailById(skuId);
        log.info("===============查詢結(jié)果為{}", pmsSkuVo);
        PmsSecKillSku wt = new PmsSecKillSku();
        wt.setId(pmsSkuVo.getId());
        wt.setVersion(pmsSkuVo.getVersion());
        // 占用庫存減1
        wt.setOccupyStock( pmsSkuVo.getOccupyStock()-1 );
        // 已售庫存加1
        wt.setSoldStock( pmsSkuVo.getSoldStock()+1 );
        // 實(shí)時(shí)庫存減1
        wt.setStock( pmsSkuVo.getStock()-1 );
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            throw new BusinessException(e.getMessage());
        }
        int count = this.baseMapper.updateById(wt);
        if (count == 0) {
            throw new OptimisticLockingFailureException("樂觀鎖沖突");
        }
    }

當(dāng)樂觀鎖沖突的時(shí)候,拋出異常, OptimisticLockingFailureException。

這里特意設(shè)置事務(wù)隔離級別為 REPEATABLE_READ

3.3、測試

 訪問接口:

@Api(value = "促銷管理-秒殺商品SKU", tags = {"促銷管理-秒殺商品SKU接口"})
@RestController
@RequiredArgsConstructor
@RequestMapping("pmsSecKillSku")
public class PmsSecKillSkuController {
    private final PmsSecKillSkuService pmsSecKillSkuService;
    @ApiOperation(value = "可重試", notes = "可重試")
    @PostMapping("/retry")
    public R<Void> retry(Integer id) {
        pmsSecKillSkuService.paySucRetry(id);
        return R.success();
    }
}
###
POST http://localhost:5910/pmsSecKillSku/retry?id=1
Content-Type: application/json
token: 123

 在第0次查詢的時(shí)候,執(zhí)行更新:

UPDATE `pms_sec_kill_sku`
SET sold_stock = sold_stock + 1, stock = stock - 1, 
occupy_stock = occupy_stock - 1, version = version + 1
WHERE id = 1;

可以看到,在第二次查詢的時(shí)候,就獲取到鎖,并成功執(zhí)行更新。

到此這篇關(guān)于Spring Retry 實(shí)現(xiàn)樂觀鎖重試的文章就介紹到這了,更多相關(guān)Spring Retry 樂觀鎖重試內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Spring Boot整合Seata的過程詳解(AT 模式)

    Spring Boot整合Seata的過程詳解(AT 模式)

    文章介紹了如何在SpringBoot項(xiàng)目中整合Seata框架來管理跨服務(wù)、跨數(shù)據(jù)庫的分布式事務(wù),本文結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧
    2025-10-10
  • 詳解Java中KMP算法的圖解與實(shí)現(xiàn)

    詳解Java中KMP算法的圖解與實(shí)現(xiàn)

    KMP算法是一種神奇的字符串匹配算法,在對超長字符串進(jìn)行模板匹配的時(shí)候比暴力匹配法的效率會高不少。本文將利用圖解為大家詳細(xì)講解KMP算法的實(shí)現(xiàn),需要的可以參考一下
    2022-05-05
  • Tomcat數(shù)據(jù)源配置方法_JBuilder中

    Tomcat數(shù)據(jù)源配置方法_JBuilder中

    今天幫一同事配置一個(gè)數(shù)據(jù)源,采用tomcat5.5.9,本來是個(gè)很簡單的事,以前也配過,但由于很長時(shí)間沒用過容器提供的數(shù)據(jù)源了(IOC用慣了),也只記的個(gè)大概了,所以剛開始一配就出錯(cuò)了,google了一下,有很多資料,照著試試卻都不好使(到不是別人說的不對,只是大家用的版本不同)。
    2008-10-10
  • Spring實(shí)戰(zhàn)之緩存使用key操作示例

    Spring實(shí)戰(zhàn)之緩存使用key操作示例

    這篇文章主要介紹了Spring實(shí)戰(zhàn)之緩存使用key操作,結(jié)合實(shí)例形式分析了Spring緩存使用key具體配置、屬性、領(lǐng)域模型等相關(guān)操作技巧,需要的朋友可以參考下
    2020-01-01
  • 三種SpringBoot中實(shí)現(xiàn)異步調(diào)用的方法總結(jié)

    三種SpringBoot中實(shí)現(xiàn)異步調(diào)用的方法總結(jié)

    Spring Boot 提供了多種方式來實(shí)現(xiàn)異步任務(wù),這篇文章主要為大家介紹了常用的三種實(shí)現(xiàn)方式,文中的示例代碼講解詳細(xì),需要的可以參考一下
    2023-05-05
  • java實(shí)現(xiàn)的DES加密算法詳解

    java實(shí)現(xiàn)的DES加密算法詳解

    這篇文章主要介紹了java實(shí)現(xiàn)的DES加密算法,結(jié)合實(shí)例形式詳細(xì)分析了java實(shí)現(xiàn)DES加密操作的原理、實(shí)現(xiàn)技巧與相關(guān)注意事項(xiàng),需要的朋友可以參考下
    2017-06-06
  • java8學(xué)習(xí)教程之lambda表達(dá)式的使用方法

    java8學(xué)習(xí)教程之lambda表達(dá)式的使用方法

    Java8最值得學(xué)習(xí)的特性就是Lambda表達(dá)式,下面這篇文章主要給大家介紹了關(guān)于java8學(xué)習(xí)教程之lambda表達(dá)式使用的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。
    2017-09-09
  • SpringBoot實(shí)現(xiàn)圖片上傳及本地訪問

    SpringBoot實(shí)現(xiàn)圖片上傳及本地訪問

    在SpringBoot項(xiàng)目中,處理靜態(tài)文件訪問尤其是實(shí)時(shí)更新的文件如商品圖片,可通過配置WebMvcConfig將本地文件映射到URL路徑上,以解決重啟項(xiàng)目才能訪問文件的問題,本文詳解如何保存和訪問這些文件,幫助開發(fā)者優(yōu)化項(xiàng)目文件管理
    2022-09-09
  • Java實(shí)現(xiàn)排球比賽計(jì)分系統(tǒng)

    Java實(shí)現(xiàn)排球比賽計(jì)分系統(tǒng)

    這篇文章主要為大家詳細(xì)介紹了Java實(shí)現(xiàn)排球比賽計(jì)分系統(tǒng),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2017-06-06
  • Java弱鍵集合WeakHashMap及ConcurrentCache原理詳解

    Java弱鍵集合WeakHashMap及ConcurrentCache原理詳解

    這篇文章主要介紹了Java弱鍵集合WeakHashMap及ConcurrentCache原理詳解,基于哈希表的Map接口實(shí)現(xiàn),支持null鍵和值,但是WeakHashMap具有弱鍵,可用來實(shí)現(xiàn)緩存存儲,在進(jìn)行GC的時(shí)候會自動回收鍵值對,需要的朋友可以參考下
    2023-09-09

最新評論

左云县| 政和县| 图木舒克市| 琼海市| 和田市| 峨边| 德保县| 农安县| 牟定县| 开远市| 无锡市| 大城县| 苗栗市| 崇信县| 河北省| 长宁区| 方山县| 青神县| 铜陵市| 师宗县| 农安县| 德钦县| 固镇县| 黄大仙区| 盐池县| 宽甸| 岗巴县| 时尚| 陵水| 临澧县| 胶南市| 汪清县| 苍溪县| 临城县| 嵊州市| 安徽省| 乌拉特中旗| 舒城县| 平陆县| 略阳县| 广东省|