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

SpringBoot實(shí)現(xiàn)接口防抖的實(shí)戰(zhàn)方案大全

 更新時(shí)間:2025年12月23日 08:54:55   作者:悟空碼字  
這篇文章主要介紹了四種在SpringBoot中實(shí)現(xiàn)接口防抖的方案,包括前端防抖+后端令牌鎖、數(shù)據(jù)庫(kù)唯一約束、本地Guava緩存和Token令牌機(jī)制,每種方案都有其優(yōu)缺點(diǎn),選擇時(shí)應(yīng)根據(jù)業(yè)務(wù)重要性和系統(tǒng)架構(gòu)來(lái)決定,需要的朋友可以參考下

一、什么是接口防抖?(又名:救救那個(gè)手抖的程序員)

想象一下這個(gè)場(chǎng)景:用戶(hù)小張?jiān)谔峤挥唵螘r(shí),因?yàn)榫W(wǎng)絡(luò)延遲,他以為沒(méi)點(diǎn)中那個(gè)“提交”按鈕,于是瘋狂連擊了10次!結(jié)果...10個(gè)一模一樣的訂單誕生了!

接口防抖 就像是給按鈕加上了一層“冷靜期”——“兄弟,你點(diǎn)太快了,先冷靜3秒再說(shuō)!”

防止重復(fù)提交 則是更嚴(yán)格的保安大哥——“同樣的身份證(請(qǐng)求)只能進(jìn)一次,想蒙混過(guò)關(guān)?沒(méi)門(mén)!”

下面我來(lái)教你在SpringBoot中布下天羅地網(wǎng),攔截這些“手抖攻擊”!

二、實(shí)戰(zhàn)方案大集合

方案1:前端防抖 + 后端令牌鎖(雙保險(xiǎn))

前端防抖代碼(JavaScript版):

// 給按鈕加個(gè)“冷靜debuff”
let isSubmitting = false;

function submitOrder() {
    if (isSubmitting) {
        alert("客官您點(diǎn)得太快了,喝口茶歇歇~");
        return;
    }
    
    isSubmitting = true;
    // 提交請(qǐng)求...
    
    // 3秒后才能再次點(diǎn)擊
    setTimeout(() => {
        isSubmitting = false;
    }, 3000);
}

后端令牌鎖實(shí)現(xiàn):

步驟1:創(chuàng)建防抖注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PreventDuplicateSubmit {
    /**
     * 防抖時(shí)間(秒),默認(rèn)3秒
     */
    int lockTime() default 3;
    
    /**
     * 鎖的key,支持SpEL表達(dá)式
     */
    String key() default "";
    
    /**
     * 提示信息
     */
    String message() default "請(qǐng)勿重復(fù)提交";
}

步驟2:實(shí)現(xiàn)AOP切面

@Aspect
@Component
@Slf4j
public class DuplicateSubmitAspect {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private HttpServletRequest request;
    
    @Pointcut("@annotation(preventDuplicateSubmit)")
    public void pointcut(PreventDuplicateSubmit preventDuplicateSubmit) {
    }
    
    @Around("pointcut(preventDuplicateSubmit)")
    public Object around(ProceedingJoinPoint joinPoint, 
                        PreventDuplicateSubmit preventDuplicateSubmit) throws Throwable {
        
        // 1. 構(gòu)造鎖的key
        String lockKey = buildLockKey(joinPoint, preventDuplicateSubmit);
        
        // 2. 嘗試加鎖(setnx操作)
        Boolean success = redisTemplate.opsForValue()
                .setIfAbsent(lockKey, "LOCKED", 
                           preventDuplicateSubmit.lockTime(), TimeUnit.SECONDS);
        
        if (Boolean.TRUE.equals(success)) {
            // 加鎖成功,執(zhí)行方法
            try {
                return joinPoint.proceed();
            } finally {
                // 可以根據(jù)業(yè)務(wù)決定是否立即刪除鎖
                // redisTemplate.delete(lockKey);
            }
        } else {
            // 加鎖失敗,說(shuō)明重復(fù)提交了
            throw new RuntimeException(preventDuplicateSubmit.message());
        }
    }
    
    private String buildLockKey(ProceedingJoinPoint joinPoint, 
                               PreventDuplicateSubmit annotation) {
        StringBuilder keyBuilder = new StringBuilder("SUBMIT:LOCK:");
        
        // 如果有自定義key
        if (StringUtils.isNotBlank(annotation.key())) {
            keyBuilder.append(parseKey(joinPoint, annotation.key()));
        } else {
            // 默認(rèn)使用:方法名 + 用戶(hù)ID + 參數(shù)hash
            keyBuilder.append(joinPoint.getSignature().toShortString());
            
            // 加上用戶(hù)ID(如果有登錄)
            String userId = getCurrentUserId();
            if (userId != null) {
                keyBuilder.append(":").append(userId);
            }
            
            // 加上參數(shù)摘要
            Object[] args = joinPoint.getArgs();
            if (args.length > 0) {
                String argsHash = DigestUtils.md5DigestAsHex(
                    Arrays.deepToString(args).getBytes()
                ).substring(0, 8);
                keyBuilder.append(":").append(argsHash);
            }
        }
        
        return keyBuilder.toString();
    }
    
    private String getCurrentUserId() {
        // 從Token或Session中獲取用戶(hù)ID
        // 這里簡(jiǎn)化處理
        return (String) request.getSession().getAttribute("userId");
    }
}

步驟3:使用示例

@RestController
@RequestMapping("/order")
public class OrderController {
    
    @PostMapping("/create")
    @PreventDuplicateSubmit(lockTime = 5, message = "訂單正在處理中,請(qǐng)勿重復(fù)提交")
    public ApiResult createOrder(@RequestBody OrderDTO orderDTO) {
        // 業(yè)務(wù)邏輯
        orderService.create(orderDTO);
        return ApiResult.success("下單成功");
    }
    
    @PostMapping("/pay")
    @PreventDuplicateSubmit(
        key = "'PAY:' + #orderNo + ':' + T(com.example.util.UserUtil).getCurrentUserId()",
        lockTime = 10,
        message = "支付請(qǐng)求已提交,請(qǐng)勿重復(fù)操作"
    )
    public ApiResult payOrder(String orderNo) {
        // 支付邏輯
        return ApiResult.success("支付成功");
    }
}

方案2:數(shù)據(jù)庫(kù)唯一約束(最硬核的方案)

有時(shí)候,最簡(jiǎn)單的最有效!

@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    // 業(yè)務(wù)唯一號(hào):時(shí)間戳 + 用戶(hù)ID + 隨機(jī)數(shù)
    @Column(name = "order_no", unique = true, nullable = false)
    private String orderNo;
    
    // 或者使用請(qǐng)求ID作為防重
    @Column(name = "request_id", unique = true)
    private String requestId;
    
    // ...其他字段
}

@Service
@Slf4j
public class OrderService {
    
    @Transactional(rollbackFor = Exception.class)
    public void createOrder(OrderDTO dto) {
        // 生成唯一請(qǐng)求ID(前端傳遞或后端生成)
        String requestId = dto.getRequestId();
        if (StringUtils.isBlank(requestId)) {
            requestId = UUID.randomUUID().toString();
        }
        
        // 檢查是否已處理過(guò)該請(qǐng)求
        if (orderRepository.existsByRequestId(requestId)) {
            log.warn("重復(fù)請(qǐng)求被攔截:{}", requestId);
            throw new BusinessException("訂單已提交,請(qǐng)勿重復(fù)操作");
        }
        
        // 創(chuàng)建訂單
        Order order = new Order();
        order.setRequestId(requestId);
        order.setOrderNo(generateOrderNo());
        // ...設(shè)置其他字段
        
        try {
            orderRepository.save(order);
        } catch (DataIntegrityViolationException e) {
            // 捕獲唯一約束異常
            throw new BusinessException("訂單已存在,請(qǐng)勿重復(fù)提交");
        }
    }
}

方案3:本地Guava緩存(輕量級(jí)方案)

適合單機(jī)部署,簡(jiǎn)單快捷!

@Component
public class LocalDuplicateChecker {
    
    // Guava緩存,3秒自動(dòng)過(guò)期
    private final Cache<String, Boolean> submitCache = CacheBuilder.newBuilder()
            .expireAfterWrite(3, TimeUnit.SECONDS)
            .maximumSize(10000)
            .build();
    
    /**
     * 檢查是否重復(fù)提交
     * @param key 請(qǐng)求唯一標(biāo)識(shí)
     * @return true=重復(fù)提交, false=首次提交
     */
    public boolean isDuplicate(String key) {
        try {
            // 如果key不存在,則放入緩存并返回null
            // 如果key存在,則返回緩存的值
            return submitCache.get(key, () -> {
                // 這個(gè)lambda只在key不存在時(shí)執(zhí)行
                return false;
            });
        } catch (ExecutionException e) {
            return true;
        }
    }
    
    /**
     * 手動(dòng)放入緩存(用于防止并發(fā)時(shí)多次通過(guò)檢查)
     */
    public void markAsSubmitted(String key) {
        submitCache.put(key, true);
    }
}

// 使用方式
@RestController
public class ApiController {
    
    @Autowired
    private LocalDuplicateChecker duplicateChecker;
    
    @PostMapping("/api/submit")
    public ApiResult submitData(@RequestBody SubmitData data, 
                               HttpServletRequest request) {
        
        // 構(gòu)造唯一key:IP + 用戶(hù)ID + 數(shù)據(jù)摘要
        String clientIp = request.getRemoteAddr();
        String userId = getCurrentUserId();
        String dataHash = DigestUtils.md5DigestAsHex(
            JSON.toJSONString(data).getBytes()
        ).substring(0, 8);
        
        String lockKey = String.format("SUBMIT:%s:%s:%s", 
                                      clientIp, userId, dataHash);
        
        if (duplicateChecker.isDuplicate(lockKey)) {
            return ApiResult.error("請(qǐng)勿重復(fù)提交");
        }
        
        // 標(biāo)記為已提交
        duplicateChecker.markAsSubmitted(lockKey);
        
        // 執(zhí)行業(yè)務(wù)邏輯
        return processData(data);
    }
}

方案4:Token令牌機(jī)制(最經(jīng)典的方案)

這個(gè)方案就像發(fā)門(mén)票,一張票只能進(jìn)一個(gè)人!

步驟1:生成Token

@RestController
public class TokenController {
    
    @GetMapping("/api/getToken")
    public ApiResult getToken() {
        String token = UUID.randomUUID().toString();
        
        // 存入Redis,有效期5分鐘
        redisTemplate.opsForValue().set(
            "SUBMIT_TOKEN:" + token, 
            "VALID", 
            5, TimeUnit.MINUTES
        );
        
        return ApiResult.success(token);
    }
}

步驟2:驗(yàn)證Token

@Aspect
@Component
public class TokenCheckAspect {
    
    @Pointcut("@annotation(needTokenCheck)")
    public void pointcut(NeedTokenCheck needTokenCheck) {
    }
    
    @Around("pointcut(needTokenCheck)")
    public Object checkToken(ProceedingJoinPoint joinPoint, 
                            NeedTokenCheck needTokenCheck) throws Throwable {
        
        HttpServletRequest request = ((ServletRequestAttributes) 
            RequestContextHolder.getRequestAttributes()).getRequest();
        
        String token = request.getHeader("X-Submit-Token");
        if (StringUtils.isBlank(token)) {
            throw new RuntimeException("提交令牌缺失");
        }
        
        String redisKey = "SUBMIT_TOKEN:" + token;
        String value = (String) redisTemplate.opsForValue().get(redisKey);
        
        if (!"VALID".equals(value)) {
            throw new RuntimeException("無(wú)效的提交令牌");
        }
        
        // 刪除令牌(一次性使用)
        redisTemplate.delete(redisKey);
        
        return joinPoint.proceed();
    }
}

步驟3:前端配合

// 提交前先獲取令牌
async function submitWithToken(data) {
    // 1. 獲取令牌
    const token = await fetch('/api/getToken').then(r => r.json());
    
    // 2. 攜帶令牌提交
    const result = await fetch('/api/submit', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-Submit-Token': token
        },
        body: JSON.stringify(data)
    });
    
    return result;
}

三、方案對(duì)比總結(jié)

方案優(yōu)點(diǎn)缺點(diǎn)適用場(chǎng)景
AOP + Redis鎖靈活可控,支持復(fù)雜規(guī)則依賴(lài)Redis,增加系統(tǒng)復(fù)雜度分布式系統(tǒng),需要精細(xì)控制
數(shù)據(jù)庫(kù)唯一約束絕對(duì)可靠,永不漏網(wǎng)對(duì)數(shù)據(jù)庫(kù)有壓力,需要設(shè)計(jì)唯一鍵核心業(yè)務(wù)(如支付、訂單)
本地緩存性能極高,零延遲僅限單機(jī),集群無(wú)效單體應(yīng)用,高頻但非核心接口
Token機(jī)制安全性高,前端可控需要兩次請(qǐng)求,增加交互表單提交,需要嚴(yán)格防重

四、防抖策略選擇指南

根據(jù)業(yè)務(wù)重要性選擇

  • 金融支付 → 數(shù)據(jù)庫(kù)唯一約束 + Redis鎖(雙重保險(xiǎn))
  • 普通表單 → Token機(jī)制或AOP鎖
  • 查詢(xún)接口 → 本地緩存防抖

根據(jù)系統(tǒng)架構(gòu)選擇

  • 單機(jī)應(yīng)用 → 本地緩存最香
  • 分布式集群 → Redis是王道
  • 微服務(wù) → 考慮分布式鎖服務(wù)

實(shí)用小貼士

// 最佳實(shí)踐:組合拳!
@PostMapping("/important/submit")
@PreventDuplicateSubmit(lockTime = 5)
@Transactional(rollbackFor = Exception.class)
public ApiResult importantSubmit(@RequestBody @Valid RequestDTO dto) {
    // 1. 檢查請(qǐng)求ID是否重復(fù)
    checkRequestId(dto.getRequestId());
    
    // 2. 執(zhí)行業(yè)務(wù)
    // 3. 數(shù)據(jù)庫(kù)唯一約束兜底
    
    return ApiResult.success();
}

五、最后

  1. 不要過(guò)度設(shè)計(jì):簡(jiǎn)單的業(yè)務(wù)用簡(jiǎn)單的方案,殺雞不要用牛刀
  2. 用戶(hù)體驗(yàn)很重要:防抖提示要友好,別讓用戶(hù)一臉懵逼
  3. 監(jiān)控不能少:記錄被攔截的請(qǐng)求,分析用戶(hù)行為
  4. 前端也要防:前后端雙重防護(hù)才是王道

防抖的目的不是為難用戶(hù),而是保護(hù)系統(tǒng)和數(shù)據(jù)的安全。就像給你的接口穿上防彈衣,既能抵擋"手抖攻擊",又能讓正常請(qǐng)求暢通無(wú)阻!

程序員防抖口訣

前端防抖先出手,后端加鎖不能少。

令牌機(jī)制來(lái)幫忙,唯一約束最可靠。

根據(jù)場(chǎng)景選方案,系統(tǒng)穩(wěn)定沒(méi)煩惱。

用戶(hù)手抖不可怕,我有妙招來(lái)護(hù)駕!

以上就是SpringBoot實(shí)現(xiàn)接口防抖的實(shí)戰(zhàn)方案大全的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot實(shí)現(xiàn)接口防抖的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • 詳解SpringBoot異常處理流程及原理

    詳解SpringBoot異常處理流程及原理

    今天給大家?guī)?lái)的是關(guān)于Java的相關(guān)知識(shí),文章圍繞著SpringBoot異常處理流程及原理展開(kāi),文中有非常詳細(xì)的介紹及代碼示例,需要的朋友可以參考下
    2021-06-06
  • 詳解Java語(yǔ)言中的抽象類(lèi)與繼承

    詳解Java語(yǔ)言中的抽象類(lèi)與繼承

    這篇文章主要為大家詳細(xì)介紹了Java語(yǔ)言中的抽象類(lèi)與繼承的相關(guān)資料,文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)Java有一定的幫助,感興趣的小伙伴快跟隨小編一起了解一下
    2022-10-10
  • Java指令重排在多線(xiàn)程環(huán)境下的解決方式

    Java指令重排在多線(xiàn)程環(huán)境下的解決方式

    這篇文章介紹了Java指令重排在多線(xiàn)程環(huán)境下的解決方式,文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2022-04-04
  • 2021年最新Redis面試題匯總(1)

    2021年最新Redis面試題匯總(1)

    在程序員面試過(guò)程中redis相關(guān)的知識(shí)是常被問(wèn)到的話(huà)題。這篇文章主要介紹了幾道Redis面試題,整理一下分享給大家,感興趣的小伙伴們可以參考一下
    2021-07-07
  • Java 無(wú)符號(hào)右移與右移運(yùn)算符的使用介紹

    Java 無(wú)符號(hào)右移與右移運(yùn)算符的使用介紹

    這篇文章主要介紹了Java 無(wú)符號(hào)右移與右移運(yùn)算符的使用介紹,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2020-06-06
  • java中ThreadLocalRandom的使用詳解

    java中ThreadLocalRandom的使用詳解

    這篇文章主要介紹了java中ThreadLocalRandom的使用詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2020-03-03
  • Spring Boot 約定大于配置之如何實(shí)現(xiàn)自定義配置

    Spring Boot 約定大于配置之如何實(shí)現(xiàn)自定義配置

    本文介紹了SpringBoot的“約定大于配置”理念以及如何實(shí)現(xiàn)自定義配置,通過(guò)實(shí)現(xiàn)接口和添加@Configuration注解,開(kāi)發(fā)者可以靈活地?cái)U(kuò)展和定制SpringBoot的默認(rèn)行為,感興趣的朋友一起看看吧
    2025-03-03
  • Mac下用Java調(diào)用c/c++的思路詳解

    Mac下用Java調(diào)用c/c++的思路詳解

    這篇文章主要介紹了Mac下用Java調(diào)用c/c++的方法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2021-08-08
  • 關(guān)于CommandLineRunner的使用詳解

    關(guān)于CommandLineRunner的使用詳解

    本文介紹了如何在SpringBoot項(xiàng)目啟動(dòng)時(shí)使用CommandLineRunner和ApplicationRunner接口進(jìn)行數(shù)據(jù)預(yù)加載或操作,通過(guò)實(shí)現(xiàn)這兩個(gè)接口,可以在項(xiàng)目啟動(dòng)時(shí)執(zhí)行特定的任務(wù),同時(shí),還展示了如何使用@Order注解來(lái)控制多個(gè)實(shí)現(xiàn)類(lèi)的加載順序
    2024-12-12
  • JVM調(diào)優(yōu)總結(jié) -Xms -Xmx -Xmn -Xss的使用

    JVM調(diào)優(yōu)總結(jié) -Xms -Xmx -Xmn -Xss的使用

    這篇文章主要介紹了JVM調(diào)優(yōu)總結(jié) -Xms -Xmx -Xmn -Xss的使用方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2025-07-07

最新評(píng)論

泽库县| 湛江市| 海城市| 京山县| 台江县| 仙游县| 海宁市| 栖霞市| 额济纳旗| 社旗县| 赞皇县| 弥渡县| 平湖市| 宾川县| 河北省| 余姚市| 青海省| 通山县| 甘谷县| 日照市| 华坪县| 长沙县| 务川| 崇义县| 汤原县| 平阴县| 若羌县| 平潭县| 灵石县| 桂东县| 宜州市| 建昌县| 姜堰市| 蒙城县| 宜阳县| 梅州市| 北海市| 泸定县| 灵山县| 咸宁市| 冷水江市|