切麵實現下單請求防重提交功能(自定義註釋@repeatSubmit)

来源:https://www.cnblogs.com/xietingwei/archive/2023/08/08/17615725.html
-Advertisement-
Play Games

##### 該切麵功能適用場景 - 下單請求多次提交,導致生成多個相同的訂單 ##### 解決方案 - 前端解決:限制點擊下單按鈕為1次後失效。不足:用戶體驗下降,能繞過前端 - 後端解決:防重提交切麵解決,自定義註釋實現該功能(如下) - 步驟: - 自定義註釋類RepeatSubmit - 創建 ...


該切麵功能適用場景
  • 下單請求多次提交,導致生成多個相同的訂單
解決方案
  • 前端解決:限制點擊下單按鈕為1次後失效。不足:用戶體驗下降,能繞過前端

  • 後端解決:防重提交切麵解決,自定義註釋實現該功能(如下)

    • 步驟:
      • 自定義註釋類RepeatSubmit
      • 創建切麵並有該註釋綁定,在切麵類實現防重提交功能:
        • 方式一:引入redission進行加鎖5秒,原理redis的setAbsent
        • 方式二:將token存入redis中,下單成功刪除token,下單前需要調用獲取token介面才能成功下單(類似於加鎖,和方式一原理相同)
  • RepeatSubmit

/**
 * 自定義防重提交
 */
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatSubmit {
    /**
     * 防重提交類型。  方法、令牌
     */
    enum Type {PARAM, TOKEN}

    /**
     * 預設防重提交,是方法參數
     * @return
     */
    Type limitType() default Type.PARAM;

    /**
     * 加鎖過期時間,預設5秒
     * @return
     */
    long lockTime() default 5;

}

  • 自定義切麵類
/**
 * 定義一個切麵類
 */
@Aspect
@Component
@Slf4j
public class RepeatSubmitAspect {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private RedissonClient redissonClient;

    /**
     * 定義 @Pointcut註解表達式,
     * 方式一:@annotation:當執行的方法上擁有指定的註解時生效(我們採用這)
     */
    @Pointcut("@annotation(repeatSubmit)")
    public void pointCutNoRepeatSubmit(RepeatSubmit repeatSubmit) {

    }

    /**
     * 環繞通知, 圍繞著方法執行
     *
     * @param joinPoint
     * @param repeatSubmit
     * @return
     * @throws Throwable
     * @Around 可以用來在調用一個具體方法前和調用後來完成一些具體的任務。
     */
    @Around("pointCutNoRepeatSubmit(repeatSubmit)")
    public Object around(ProceedingJoinPoint joinPoint, RepeatSubmit repeatSubmit) throws Throwable {


        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();

        Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();

        // 記錄成功或者失敗
        Boolean res = false;


        // 防重提交類型
        String type = repeatSubmit.limitType().name();
        if (type.equalsIgnoreCase(RepeatSubmit.Type.PARAM.name())) {
            //方式一,參數形式防重提交

            long lockTime = repeatSubmit.lockTime();

            String ipAddr = CommonUtil.getIpAddr(request);

            MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();

            Method method = methodSignature.getMethod();
            String className = method.getDeclaringClass().getName();
            String key = "order-server:repeat_submit"+CommonUtil.MD5(String.format("%s-%s-%s-%s", ipAddr, className, method, accountNo));

            // 加鎖
            //res = redisTemplate.opsForValue().setIfAbsent(key,"1",lockTime, TimeUnit.SECONDS);
            RLock lock = redissonClient.getLock(key);

            // 嘗試加鎖,最多等待2秒,上鎖以後5秒自動解鎖 [lockTime預設為5s, 可以自定義]
            res = lock.tryLock(2, lockTime, TimeUnit.SECONDS);

        } else if (type.equalsIgnoreCase(RepeatSubmit.Type.TOKEN.name())) {
            //方式二,令牌形式防重提交
            String requestToken = request.getHeader("request-token");
            if (StringUtils.isBlank(requestToken)) {
                throw new BizException(BizCodeEnum.ORDER_CONFIRM_TOKEN_EQUAL_FAIL);
            }
            String key = String.format(RedisKey.SUBMIT_ORDER_TOKEN_KEY, accountNo, requestToken);
            /**
             * 提交表單的token key
             * key是 order:submit:accountNo:token,然後直接刪除成功則完成
             */
            res = redisTemplate.delete(key);

        }
        if (!res) {
            log.error("訂單請求重覆提交");
            return null;
        }

        log.info("環繞通知執行前");

        Object obj = joinPoint.proceed();

        log.info("環繞通知執行後");
        return obj;
    }
}

  • RedissionConfiguration配置類(用於加鎖)
@Configuration
public class RedissionConfiguration {

    @Value("${spring.redis.host}")
    private String redisHost;

    @Value("${spring.redis.port}")
    private String redisPort;


    @Value("${spring.redis.password}")
    private String redisPwd;


    /**
     * 配置分散式鎖的redisson
     * @return
     */
    @Bean
    public RedissonClient redissonClient(){
        Config config = new Config();

        //單機方式
        config.useSingleServer().setPassword(redisPwd).setAddress("redis://"+redisHost+":"+redisPort);

        //集群
        //config.useClusterServers().addNodeAddress("redis://192.31.21.1:6379","redis://192.31.21.2:6379")

        RedissonClient redissonClient = Redisson.create(config);
        return redissonClient;
    }

    /**
     * 集群模式
     * 備註:可以用"rediss://"來啟用SSL連接
     */
    /*@Bean
    public RedissonClient redissonClusterClient() {
        Config config = new Config();
        config.useClusterServers().setScanInterval(2000) // 集群狀態掃描間隔時間,單位是毫秒
              .addNodeAddress("redis://127.0.0.1:7000")
              .addNodeAddress("redis://127.0.0.1:7002");
        RedissonClient redisson = Redisson.create(config);
        return redisson;
    }*/

}

  • 使用說明:在下單介面標註@RepeatSubmit(limitType = RepeatSubmit.Type.TOKEN)
  • 或者@RepeatSubmit(limitType = RepeatSubmit.Type.PARAM)
    /**
     * 下單前獲取令牌,用於防重提交
     * @return
     */
    @GetMapping("token")
    public JsonData getOrderToken() {

        Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();

        String token = CommonUtil.getStringNumRandom(32);

        String key = String.format(RedisKey.SUBMIT_ORDER_TOKEN_KEY, accountNo, token);
        // token 過期時間30分鐘
        redisTemplate.opsForValue().set(key, String.valueOf(Thread.currentThread().getId()), 30, TimeUnit.MINUTES);

        return JsonData.buildSuccess(token);
    } 	


	@PostMapping("confirm")
    @RepeatSubmit(limitType = RepeatSubmit.Type.TOKEN)
    public void confirmOrder(@RequestBody ConfirmOrderRequest orderRequest, HttpServletResponse response) {
        // TODO 下單業務
    }

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 作為開發公司,我們開發完APP,如何發給客戶下載測試呢?安卓APP可以通過QQ直接發送給客戶,客戶可以在QQ內直接點擊安裝。 但是現在很多客戶都不用QQ,用微信的居多。而通過微信直接發送安卓APP安裝包(apk)的話,是不可以像QQ那樣直接點擊安裝的。這就需要我們把APP生成二維碼提供給客戶下載安裝 ...
  • > 本文首發於[掘金](https://juejin.cn/post/7264128388288708664),未經許可禁止轉載 Vuex4 是 Vue 的狀態管理工具,Vuex 和單純的全局對象有以下兩點不同: 1. Vuex 的狀態存儲是響應式的 2. 不能直接改變 store 中的狀態。改變 ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 我們經常會遇到一個場景,比如在一個列表中批量獲取用戶的信息。 如果我們一次性往後端發送幾十條請求是非常愚蠢的事情。此時我們就要學會如何使用批量獲取的邏輯。 但是批量獲取有一個問題就是,我需要在用戶列表項的上層去獲取,然後再把結果分發給下層 ...
  • 分享的 WebStorm 2023.2 最新激活註冊碼,可免費永久激活,親測有效,下麵是詳細文檔哦~ 申明:本教程 WebStorm 激活碼收集於網路,請勿商用,僅供個人學習使用,如有侵權,請聯繫作者刪除。若條件允許,希望大家購買正版 ! PS: 本教程最新更新時間: 2023年08月08日~ 前言 ...
  • 最近,群友分享了一個很有意思的效果: ![](https://img2023.cnblogs.com/blog/608782/202308/608782-20230808101320920-311621134.gif) 原效果的網址:[frosted-glass](https://frosted-g ...
  • 說到 Hybrid App(混合應用)大家都不陌生,因為這種開發模式大行其道發展的這些年取代了很多原生和 Web 應用,為什麼大家對這種「Native + HTML5」的開發模式額外偏愛呢? 因為一方面在一定程度上兼顧了原生應用的優質體驗,另一方面又兼顧到了 HTML5 靈活的開發模式。 這種模式的 ...
  • 為了降低啟動時間,quarkus下的常規作用域bean遵循懶載入規則,但有時我們希望bean可以更早實例化,本篇,咱們一起來瞭解懶載入規則和改變規則的方法 ...
  • 由於垃圾收集演算法的實現涉及大量的程式細節,而且各個平臺的虛擬機操作記憶體的方法又各不相同,因此本節不打算過多地討論演算法的實現,只是介紹幾種演算法的思想及其發展過程。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...