Java中防止重复提交的八种解决方案(最后一种很优雅)
作者:小卜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 实现原理
页面加载时向后端请求Token
提交时携带Token
后端校验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. 最佳实践建议
分层防护:前端 + 后端多重防护
合理超时:根据业务设置合理的锁超时时间
友好提示:给用户明确的重复提交提示
监控告警:对频繁的重复提交进行监控
性能考虑:避免防重逻辑影响正常业务流程
结语
防止重复提交是保证系统数据一致性的重要手段。在实际项目中,建议根据具体业务场景选择合适的方案,或者组合多种方案实现更完善的防护。AOP注解方案因其灵活性和无侵入性,是目前比较推荐的做法。
希望本文对您有所帮助!如有疑问,欢迎在评论区讨论。
