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

Java中防止重復提交的八種解決方案(最后一種很優(yōu)雅)

 更新時間:2026年03月24日 10:48:27   作者:小卜NPE  
重復提交是指用戶在短時間內(nèi)多次發(fā)送相同請求到服務端,導致數(shù)據(jù)被多次處理的現(xiàn)象,下面這篇文章主要介紹了Java中防止重復提交的八種解決方案的相關資料,文中通過代碼介紹的非常詳細,需要的朋友可以參考下

在Web開發(fā)中,防止重復提交是一個常見且重要的需求。本文將詳細介紹Java中防止重復提交的8種解決方案,并分析各自的優(yōu)缺點。

1. 什么是重復提交?為什么要防止?

1.1 重復提交的定義

重復提交是指用戶在短時間內(nèi)對同一業(yè)務請求進行多次提交的行為。常見場景包括:

  • 網(wǎng)絡延遲:用戶點擊提交后頁面無響應,多次點擊

  • 誤操作:用戶雙擊提交按鈕

  • 惡意請求:攻擊者故意重復提交

1.2 重復提交的危害

  • 數(shù)據(jù)不一致:創(chuàng)建重復訂單、重復扣款等

  • 系統(tǒng)資源浪費:增加數(shù)據(jù)庫和服務器壓力

  • 業(yè)務邏輯錯誤:影響統(tǒng)計數(shù)據(jù)和業(yè)務流程

2. 前端解決方案

2.1 按鈕禁用(最基礎)

// 提交后禁用按鈕
function submitForm() {
    const submitBtn = document.getElementById('submitBtn');
    submitBtn.disabled = true;
    // 執(zhí)行提交邏輯
    document.forms[0].submit();
}

2.2 加載狀態(tài)提示

// 顯示加載狀態(tài)
function submitForm() {
    const submitBtn = document.getElementById('submitBtn');
    submitBtn.innerHTML = '<i class="loading"></i> 提交中...';
    submitBtn.disabled = true;
}

前端方案的局限性:無法防止惡意請求和瀏覽器刷新重復提交。

3. 后端解決方案

3.1 同步鎖(不推薦)

public class OrderService {
    private final Object lock = new Object();
    
    public Result createOrder(OrderDTO orderDTO) {
        synchronized(lock) {
            // 業(yè)務邏輯
            return processOrder(orderDTO);
        }
    }
}

缺點:集群環(huán)境下無效,性能差。

3.2 數(shù)據(jù)庫唯一索引

-- 為訂單號添加唯一索引
ALTER TABLE orders ADD UNIQUE INDEX uk_order_no (order_no);

-- 或者為業(yè)務關鍵字段添加聯(lián)合唯一索引
ALTER TABLE orders ADD UNIQUE INDEX uk_business_key (user_id, product_id, create_date);

優(yōu)點:最可靠的防重方案
缺點:數(shù)據(jù)庫壓力大,不友好的錯誤提示

3.3 數(shù)據(jù)庫樂觀鎖

@Mapper
public interface OrderMapper {
    // 通過版本號控制
    @Update("UPDATE orders SET status = #{status}, version = version + 1 " +
            "WHERE id = #{id} AND version = #{version}")
    int updateWithVersion(Order order);
}

@Service
@Transactional
public class OrderService {
    public Result createOrder(OrderDTO orderDTO) {
        // 1. 查詢當前版本號
        Order order = orderMapper.selectById(orderDTO.getId());
        
        // 2. 業(yè)務處理...
        
        // 3. 更新時校驗版本號
        int count = orderMapper.updateWithVersion(order);
        if (count == 0) {
            throw new RuntimeException("訂單已處理,請勿重復提交");
        }
        return Result.success();
    }
}

4. Token令牌方案(推薦)

4.1 實現(xiàn)原理

  1. 頁面加載時向后端請求Token

  2. 提交時攜帶Token

  3. 后端校驗Token并刪除

4.2 具體實現(xiàn)

Token生成工具類

@Component
public class TokenUtil {
    
    private static final String TOKEN_PREFIX = "submit_token:";
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    /**
     * 生成Token
     */
    public String generateToken(String key) {
        String token = UUID.randomUUID().toString();
        String redisKey = TOKEN_PREFIX + key + ":" + token;
        redisTemplate.opsForValue().set(redisKey, "1", Duration.ofMinutes(5));
        return token;
    }
    
    /**
     * 驗證Token
     */
    public boolean validateToken(String key, String token) {
        String redisKey = TOKEN_PREFIX + key + ":" + token;
        Boolean result = redisTemplate.delete(redisKey);
        return Boolean.TRUE.equals(result);
    }
}

Controller層實現(xiàn)

@RestController
public class OrderController {
    
    @Autowired
    private TokenUtil tokenUtil;
    
    /**
     * 獲取提交Token
     */
    @GetMapping("/token")
    public Result<String> getToken() {
        String token = tokenUtil.generateToken("order");
        return Result.success(token);
    }
    
    /**
     * 提交訂單
     */
    @PostMapping("/order")
    public Result createOrder(@RequestBody OrderDTO orderDTO, 
                             @RequestHeader("X-Submit-Token") String token) {
        // 驗證Token
        if (!tokenUtil.validateToken("order", token)) {
            return Result.fail("請勿重復提交");
        }
        
        // 業(yè)務邏輯
        return orderService.createOrder(orderDTO);
    }
}

前端調(diào)用

// 獲取Token
async function getToken() {
    const response = await fetch('/token');
    const result = await response.json();
    return result.data;
}

// 提交訂單
async function submitOrder(orderData) {
    const token = await getToken();
    
    const response = await fetch('/order', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-Submit-Token': token
        },
        body: JSON.stringify(orderData)
    });
    
    return response.json();
}

5. 基于AOP的防重注解(優(yōu)雅方案)

5.1 自定義防重提交注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PreventDuplicateSubmit {
    /**
     * 防重key,支持SpEL表達式
     */
    String key() default "";
    
    /**
     * 過期時間(秒)
     */
    int expire() default 5;
    
    /**
     * 提示消息
     */
    String message() default "請勿重復提交";
}

5.2 AOP切面實現(xiàn)

@Aspect
@Component
public class PreventDuplicateSubmitAspect {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private HttpServletRequest request;
    
    private static final String LOCK_PREFIX = "submit_lock:";
    
    @Around("@annotation(preventDuplicateSubmit)")
    public Object around(ProceedingJoinPoint joinPoint, 
                        PreventDuplicateSubmit preventDuplicateSubmit) throws Throwable {
        
        String lockKey = generateLockKey(joinPoint, preventDuplicateSubmit);
        
        // 嘗試獲取鎖
        Boolean success = redisTemplate.opsForValue()
                .setIfAbsent(lockKey, "1", Duration.ofSeconds(preventDuplicateSubmit.expire()));
        
        if (Boolean.TRUE.equals(success)) {
            try {
                // 獲取鎖成功,執(zhí)行方法
                return joinPoint.proceed();
            } finally {
                // 刪除鎖(可選,等自動過期也行)
                // redisTemplate.delete(lockKey);
            }
        } else {
            // 獲取鎖失敗,重復提交
            throw new RuntimeException(preventDuplicateSubmit.message());
        }
    }
    
    /**
     * 生成鎖的Key
     */
    private String generateLockKey(ProceedingJoinPoint joinPoint, 
                                 PreventDuplicateSubmit preventDuplicateSubmit) {
        String key = preventDuplicateSubmit.key();
        
        if (StringUtils.hasText(key)) {
            // 解析SpEL表達式
            return LOCK_PREFIX + parseSpel(key, joinPoint);
        } else {
            // 默認生成方式:方法名 + 參數(shù)
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            String methodName = signature.getMethod().getName();
            String args = Arrays.toString(joinPoint.getArgs());
            String userAgent = request.getHeader("User-Agent");
            
            return LOCK_PREFIX + methodName + ":" + 
                   DigestUtils.md5DigestAsHex((args + userAgent).getBytes());
        }
    }
    
    /**
     * 解析SpEL表達式
     */
    private String parseSpel(String expression, ProceedingJoinPoint joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        EvaluationContext context = new StandardEvaluationContext();
        
        // 設置參數(shù)
        String[] parameterNames = signature.getParameterNames();
        Object[] args = joinPoint.getArgs();
        
        for (int i = 0; i < parameterNames.length; i++) {
            context.setVariable(parameterNames[i], args[i]);
        }
        
        ExpressionParser parser = new SpelExpressionParser();
        return parser.parseExpression(expression).getValue(context, String.class);
    }
}

5.3 使用示例

@RestController
public class OrderController {
    
    @PreventDuplicateSubmit(key = "#orderDTO.userId + ':' + #orderDTO.productId", 
                           expire = 10, 
                           message = "訂單正在處理中,請勿重復提交")
    @PostMapping("/order")
    public Result createOrder(@RequestBody OrderDTO orderDTO) {
        // 業(yè)務邏輯
        return orderService.createOrder(orderDTO);
    }
    
    @PreventDuplicateSubmit(expire = 30)
    @PostMapping("/payment")
    public Result payment(@RequestParam String orderNo) {
        // 支付邏輯
        return paymentService.processPayment(orderNo);
    }
}

6. 分布式鎖方案

6.1 基于Redis的分布式鎖

@Component
public class RedisDistributedLock {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    private static final String LOCK_PREFIX = "global_lock:";
    
    /**
     * 嘗試獲取鎖
     */
    public boolean tryLock(String key, long expireSeconds) {
        String lockKey = LOCK_PREFIX + key;
        String value = UUID.randomUUID().toString();
        
        Boolean result = redisTemplate.opsForValue()
                .setIfAbsent(lockKey, value, Duration.ofSeconds(expireSeconds));
        
        return Boolean.TRUE.equals(result);
    }
    
    /**
     * 釋放鎖
     */
    public void unlock(String key) {
        String lockKey = LOCK_PREFIX + key;
        redisTemplate.delete(lockKey);
    }
}

6.2 使用Redisson分布式鎖

@Component
public class RedissonLockService {
    
    @Autowired
    private RedissonClient redissonClient;
    
    public <T> T executeWithLock(String lockKey, long waitTime, long leaseTime, 
                                Supplier<T> supplier) {
        RLock lock = redissonClient.getLock(lockKey);
        
        try {
            // 嘗試獲取鎖
            boolean locked = lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS);
            if (locked) {
                return supplier.get();
            } else {
                throw new RuntimeException("系統(tǒng)繁忙,請稍后重試");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("獲取鎖失敗", e);
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

// 使用示例
@Service
public class OrderService {
    
    @Autowired
    private RedissonLockService lockService;
    
    public Result createOrder(OrderDTO orderDTO) {
        String lockKey = "order_submit:" + orderDTO.getUserId();
        
        return lockService.executeWithLock(lockKey, 3, 10, () -> {
            // 業(yè)務邏輯
            return processOrder(orderDTO);
        });
    }
}

7. 本地限流器

7.1 Guava RateLimiter

@Component
public class RateLimitService {
    
    private final Map<String, RateLimiter> limiterMap = new ConcurrentHashMap<>();
    
    /**
     * 嘗試獲取令牌
     */
    public boolean tryAcquire(String key, int permitsPerSecond) {
        RateLimiter limiter = limiterMap.computeIfAbsent(key, 
            k -> RateLimiter.create(permitsPerSecond));
        return limiter.tryAcquire();
    }
}

// 使用示例
@RestController
public class ApiController {
    
    @Autowired
    private RateLimitService rateLimitService;
    
    @PostMapping("/api/submit")
    public Result submitData(@RequestBody RequestData data) {
        String clientId = getClientId(); // 獲取客戶端標識
        
        if (!rateLimitService.tryAcquire(clientId, 5)) {
            return Result.fail("請求過于頻繁,請稍后重試");
        }
        
        // 處理業(yè)務
        return processData(data);
    }
}

8. 綜合方案:注解 + 分布式鎖 + 限流

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SubmitProtection {
    /** 防重提交key */
    String key() default "";
    /** 鎖過期時間 */
    int lockExpire() default 10;
    /** 限流配置:每秒允許的請求數(shù) */
    double rateLimit() default 1.0;
    /** 提示消息 */
    String message() default "請求過于頻繁,請稍后重試";
}

@Aspect
@Component
public class SubmitProtectionAspect {
    
    @Autowired
    private RedissonClient redissonClient;
    
    private final Map<String, RateLimiter> rateLimiterMap = new ConcurrentHashMap<>();
    
    @Around("@annotation(protection)")
    public Object around(ProceedingJoinPoint joinPoint, SubmitProtection protection) throws Throwable {
        String protectionKey = generateProtectionKey(joinPoint, protection);
        
        // 1. 限流檢查
        if (protection.rateLimit() > 0) {
            RateLimiter limiter = rateLimiterMap.computeIfAbsent(
                protectionKey, k -> RateLimiter.create(protection.rateLimit()));
            if (!limiter.tryAcquire()) {
                throw new RuntimeException(protection.message());
            }
        }
        
        // 2. 分布式鎖防重
        RLock lock = redissonClient.getLock("submit_protection:" + protectionKey);
        try {
            if (lock.tryLock(0, protection.lockExpire(), TimeUnit.SECONDS)) {
                return joinPoint.proceed();
            } else {
                throw new RuntimeException("請求正在處理中,請勿重復提交");
            }
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
    
    private String generateProtectionKey(ProceedingJoinPoint joinPoint, SubmitProtection protection) {
        // 生成key的邏輯,參考前面的AOP方案
        return "default_key";
    }
}

9. 方案對比總結

方案適用場景優(yōu)點缺點
前端控制普通表單提交實現(xiàn)簡單,用戶體驗好安全性低,可繞過
同步鎖單機簡單業(yè)務實現(xiàn)簡單集群無效,性能差
數(shù)據(jù)庫唯一索引數(shù)據(jù)強一致性要求可靠性最高數(shù)據(jù)庫壓力大
樂觀鎖并發(fā)更新場景性能較好實現(xiàn)復雜,需要版本字段
Token令牌Web表單提交安全性好,實現(xiàn)簡單需要前后端配合
AOP注解需要靈活控制的業(yè)務無侵入,使用方便學習成本稍高
分布式鎖分布式系統(tǒng)集群有效,可靠性高依賴Redis等中間件
限流器高頻請求場景防止惡意請求可能誤傷正常用戶

10. 最佳實踐建議

  1. 分層防護:前端 + 后端多重防護

  2. 合理超時:根據(jù)業(yè)務設置合理的鎖超時時間

  3. 友好提示:給用戶明確的重復提交提示

  4. 監(jiān)控告警:對頻繁的重復提交進行監(jiān)控

  5. 性能考慮:避免防重邏輯影響正常業(yè)務流程

結語

防止重復提交是保證系統(tǒng)數(shù)據(jù)一致性的重要手段。在實際項目中,建議根據(jù)具體業(yè)務場景選擇合適的方案,或者組合多種方案實現(xiàn)更完善的防護。AOP注解方案因其靈活性和無侵入性,是目前比較推薦的做法。

希望本文對您有所幫助!如有疑問,歡迎在評論區(qū)討論。

相關文章

最新評論

蓝田县| 北票市| 比如县| 天气| 仙居县| 武平县| 喜德县| 紫阳县| 通州市| 清水县| 五河县| 博罗县| 尼木县| 温宿县| 锡林浩特市| 时尚| 陆良县| 彰化市| 东光县| 扶绥县| 静宁县| 竹北市| 吐鲁番市| 无锡市| 湖州市| 利川市| 汝南县| 钟山县| 垫江县| 长沙县| 枣庄市| 龙井市| 屯留县| 石泉县| 梅河口市| 富川| 中江县| 大埔区| 乡宁县| 贡嘎县| 汝州市|