Java中防止重复提交的八种解决方案(最后一种很优雅)

 更新时间:2026年03月24日 10:48:27   作者:小卜NPE  
重复提交是指用户在短时间内多次发送相同请求到服务端,导致数据被多次处理的现象,下面这篇文章主要介绍了Java中防止重复提交的八种解决方案的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下

在Web开发中,防止重复提交是一个常见且重要的需求。本文将详细介绍Java中防止重复提交的8种解决方案,并分析各自的优缺点。

1. 什么是重复提交?为什么要防止?

1.1 重复提交的定义

重复提交是指用户在短时间内对同一业务请求进行多次提交的行为。常见场景包括:

  • 网络延迟:用户点击提交后页面无响应,多次点击

  • 误操作:用户双击提交按钮

  • 恶意请求:攻击者故意重复提交

1.2 重复提交的危害

  • 数据不一致:创建重复订单、重复扣款等

  • 系统资源浪费:增加数据库和服务器压力

  • 业务逻辑错误:影响统计数据和业务流程

2. 前端解决方案

2.1 按钮禁用(最基础)

// 提交后禁用按钮
function submitForm() {
    const submitBtn = document.getElementById('submitBtn');
    submitBtn.disabled = true;
    // 执行提交逻辑
    document.forms[0].submit();
}

2.2 加载状态提示

// 显示加载状态
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) {
            // 业务逻辑
            return processOrder(orderDTO);
        }
    }
}

缺点:集群环境下无效,性能差。

3.2 数据库唯一索引

-- 为订单号添加唯一索引
ALTER TABLE orders ADD UNIQUE INDEX uk_order_no (order_no);

-- 或者为业务关键字段添加联合唯一索引
ALTER TABLE orders ADD UNIQUE INDEX uk_business_key (user_id, product_id, create_date);

优点:最可靠的防重方案
缺点:数据库压力大,不友好的错误提示

3.3 数据库乐观锁

@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. 业务处理...
        
        // 3. 更新时校验版本号
        int count = orderMapper.updateWithVersion(order);
        if (count == 0) {
            throw new RuntimeException("订单已处理,请勿重复提交");
        }
        return Result.success();
    }
}

4. Token令牌方案(推荐)

4.1 实现原理

  1. 页面加载时向后端请求Token

  2. 提交时携带Token

  3. 后端校验Token并删除

4.2 具体实现

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层实现

@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("请勿重复提交");
        }
        
        // 业务逻辑
        return orderService.createOrder(orderDTO);
    }
}

前端调用

// 获取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的防重注解(优雅方案)

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切面实现

@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 {
                // 获取锁成功,执行方法
                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 {
            // 默认生成方式:方法名 + 参数
            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();
        
        // 设置参数
        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) {
        // 业务逻辑
        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("系统繁忙,请稍后重试");
            }
        } 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, () -> {
            // 业务逻辑
            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("请求过于频繁,请稍后重试");
        }
        
        // 处理业务
        return processData(data);
    }
}

8. 综合方案:注解 + 分布式锁 + 限流

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SubmitProtection {
    /** 防重提交key */
    String key() default "";
    /** 锁过期时间 */
    int lockExpire() default 10;
    /** 限流配置:每秒允许的请求数 */
    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. 方案对比总结

方案适用场景优点缺点
前端控制普通表单提交实现简单,用户体验好安全性低,可绕过
同步锁单机简单业务实现简单集群无效,性能差
数据库唯一索引数据强一致性要求可靠性最高数据库压力大
乐观锁并发更新场景性能较好实现复杂,需要版本字段
Token令牌Web表单提交安全性好,实现简单需要前后端配合
AOP注解需要灵活控制的业务无侵入,使用方便学习成本稍高
分布式锁分布式系统集群有效,可靠性高依赖Redis等中间件
限流器高频请求场景防止恶意请求可能误伤正常用户

10. 最佳实践建议

  1. 分层防护:前端 + 后端多重防护

  2. 合理超时:根据业务设置合理的锁超时时间

  3. 友好提示:给用户明确的重复提交提示

  4. 监控告警:对频繁的重复提交进行监控

  5. 性能考虑:避免防重逻辑影响正常业务流程

结语

防止重复提交是保证系统数据一致性的重要手段。在实际项目中,建议根据具体业务场景选择合适的方案,或者组合多种方案实现更完善的防护。AOP注解方案因其灵活性和无侵入性,是目前比较推荐的做法。

希望本文对您有所帮助!如有疑问,欢迎在评论区讨论。

相关文章

  • mybatis plus 自动转驼峰配置小结

    mybatis plus 自动转驼峰配置小结

    SpringBoot提供两种配置Mybatis的方式,第一种是通过yml或application.properties文件开启配置,第二种是使用自定义配置类,通过给容器添加一个ConfigurationCustomizer来实现更灵活的配置,这两种方法可以根据项目需求和个人喜好选择使用
    2024-10-10
  • MyBatis增删改查快速上手

    MyBatis增删改查快速上手

    这篇文章给大家讲解的是MyBatis 这门技术的 CURD (增删改查) ,非常的详细与实用,有需要的小伙伴可以参考下
    2020-02-02
  • SpringBoot整合FreeMarker的过程详解

    SpringBoot整合FreeMarker的过程详解

    FreeMarker 是一个模板引擎,可以将模板与数据结合生成文本输出,本文给大家介绍SpringBoot整合FreeMarker的过程,感兴趣的朋友一起看看吧
    2024-01-01
  • java JVM原理与常识知识点

    java JVM原理与常识知识点

    在本文中小编给大家分享的是关于java的JVM原理和java常识,有兴趣的朋友们可以学习下
    2018-12-12
  • Spring事务annotation原理详解

    Spring事务annotation原理详解

    这篇文章主要介绍了Spring事务annotation原理详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-02-02
  • Java异或技操作给任意的文件加密原理及使用详解

    Java异或技操作给任意的文件加密原理及使用详解

    这篇文章主要介绍了Java异或技操作给任意的文件加密原理及使用详解,具有一定借鉴价值,需要的朋友可以参考下。
    2017-12-12
  • SpringBoot整合Logback日志框架及高并发下的性能优化

    SpringBoot整合Logback日志框架及高并发下的性能优化

    在现代的Java应用开发中,日志记录是不可或缺的一部分,Spring Boot作为目前最流行的Java开发框架之一,默认集成了Logback作为日志框架,Logback是Log4j的继任者,具有更高的性能和更丰富的功能,本文将详细介绍如何在Spring Boot中整合Logback日志框架
    2025-03-03
  • Java字符处理之char、String、StringBuilder和StringBuffer详解

    Java字符处理之char、String、StringBuilder和StringBuffer详解

    这篇文章主要介绍了Java字符处理之char、String、StringBuilder和StringBuffer的相关资料,它们各自有着独特的特性和适用场景,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2026-03-03
  • Java中List的使用方法简单介绍

    Java中List的使用方法简单介绍

    这篇文章主要针对Java中List的使用方法为大家介绍了进行简单介绍,List是个集合接口,只要是集合类接口都会有个“迭代子”( Iterator ),利用这个迭代子,就可以对list内存的一组对象进行操作,感兴趣的小伙伴们可以参考一下
    2016-07-07
  • 解释:int型默认值为0的问题

    解释:int型默认值为0的问题

    这篇文章主要介绍了解释:int型默认值为0的问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-08-08

最新评论