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

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

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

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

1. 什么是重復(fù)提交?為什么要防止?

1.1 重復(fù)提交的定義

重復(fù)提交是指用戶在短時(shí)間內(nèi)對(duì)同一業(yè)務(wù)請(qǐng)求進(jìn)行多次提交的行為。常見(jiàn)場(chǎng)景包括:

  • 網(wǎng)絡(luò)延遲:用戶點(diǎn)擊提交后頁(yè)面無(wú)響應(yīng),多次點(diǎn)擊

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

  • 惡意請(qǐng)求:攻擊者故意重復(fù)提交

1.2 重復(fù)提交的危害

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

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

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

2. 前端解決方案

2.1 按鈕禁用(最基礎(chǔ))

// 提交后禁用按鈕
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;
}

前端方案的局限性:無(wú)法防止惡意請(qǐng)求和瀏覽器刷新重復(fù)提交。

3. 后端解決方案

3.1 同步鎖(不推薦)

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

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

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

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

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

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

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

@Mapper
public interface OrderMapper {
    // 通過(guò)版本號(hào)控制
    @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. 查詢當(dāng)前版本號(hào)
        Order order = orderMapper.selectById(orderDTO.getId());
        
        // 2. 業(yè)務(wù)處理...
        
        // 3. 更新時(shí)校驗(yàn)版本號(hào)
        int count = orderMapper.updateWithVersion(order);
        if (count == 0) {
            throw new RuntimeException("訂單已處理,請(qǐng)勿重復(fù)提交");
        }
        return Result.success();
    }
}

4. Token令牌方案(推薦)

4.1 實(shí)現(xiàn)原理

  1. 頁(yè)面加載時(shí)向后端請(qǐng)求Token

  2. 提交時(shí)攜帶Token

  3. 后端校驗(yàn)Token并刪除

4.2 具體實(shí)現(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;
    }
    
    /**
     * 驗(yàn)證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層實(shí)現(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) {
        // 驗(yàn)證Token
        if (!tokenUtil.validateToken("order", token)) {
            return Result.fail("請(qǐng)勿重復(fù)提交");
        }
        
        // 業(yè)務(wù)邏輯
        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表達(dá)式
     */
    String key() default "";
    
    /**
     * 過(guò)期時(shí)間(秒)
     */
    int expire() default 5;
    
    /**
     * 提示消息
     */
    String message() default "請(qǐng)勿重復(fù)提交";
}

5.2 AOP切面實(shí)現(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 {
                // 刪除鎖(可選,等自動(dòng)過(guò)期也行)
                // redisTemplate.delete(lockKey);
            }
        } else {
            // 獲取鎖失敗,重復(fù)提交
            throw new RuntimeException(preventDuplicateSubmit.message());
        }
    }
    
    /**
     * 生成鎖的Key
     */
    private String generateLockKey(ProceedingJoinPoint joinPoint, 
                                 PreventDuplicateSubmit preventDuplicateSubmit) {
        String key = preventDuplicateSubmit.key();
        
        if (StringUtils.hasText(key)) {
            // 解析SpEL表達(dá)式
            return LOCK_PREFIX + parseSpel(key, joinPoint);
        } else {
            // 默認(rèn)生成方式:方法名 + 參數(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表達(dá)式
     */
    private String parseSpel(String expression, ProceedingJoinPoint joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        EvaluationContext context = new StandardEvaluationContext();
        
        // 設(shè)置參數(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 = "訂單正在處理中,請(qǐng)勿重復(fù)提交")
    @PostMapping("/order")
    public Result createOrder(@RequestBody OrderDTO orderDTO) {
        // 業(yè)務(wù)邏輯
        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)繁忙,請(qǐ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è)務(wù)邏輯
            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(); // 獲取客戶端標(biāo)識(shí)
        
        if (!rateLimitService.tryAcquire(clientId, 5)) {
            return Result.fail("請(qǐng)求過(guò)于頻繁,請(qǐng)稍后重試");
        }
        
        // 處理業(yè)務(wù)
        return processData(data);
    }
}

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

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

@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("請(qǐng)求正在處理中,請(qǐng)勿重復(fù)提交");
            }
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
    
    private String generateProtectionKey(ProceedingJoinPoint joinPoint, SubmitProtection protection) {
        // 生成key的邏輯,參考前面的AOP方案
        return "default_key";
    }
}

9. 方案對(duì)比總結(jié)

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

10. 最佳實(shí)踐建議

  1. 分層防護(hù):前端 + 后端多重防護(hù)

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

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

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

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

結(jié)語(yǔ)

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

希望本文對(duì)您有所幫助!如有疑問(wèn),歡迎在評(píng)論區(qū)討論。

相關(guān)文章

最新評(píng)論

神池县| 阿克| 邵阳市| 临颍县| 鹤岗市| 瑞安市| 开远市| 九龙县| 肃宁县| 博野县| 文山县| 武鸣县| 正定县| 高淳县| 仙游县| 徐州市| 辉南县| 湖南省| 桃源县| 江西省| 无为县| 石阡县| 亚东县| 宿州市| 六安市| 宾阳县| 井研县| 石泉县| 五家渠市| 贵溪市| 盈江县| 丹阳市| 东源县| 岳池县| 四子王旗| 宁津县| 康保县| 大港区| 逊克县| 托克托县| 九寨沟县|