Prevent Duplicate Submissions in SpringBoot: 4 Proven Solutions
This article explains why front‑end debouncing is insufficient for preventing duplicate orders, then walks through four backend strategies—local cache with AOP, Redis atomic operations, database unique indexes, and token verification—providing core principles, code examples, and pros/cons for each.
Why Backend Protection Is Essential
When a user clicks the submit button twice, duplicate orders are created, inventory is deducted twice, and developers are woken up at 3 am to delete rows manually; front‑end debouncing only handles accidental clicks, while malicious or network‑retry requests require server‑side safeguards.
1. Single‑Instance Scenario: Local Cache + AOP
Suitable for non‑distributed, single‑service deployments. It uses a custom @NoRepeatSubmit annotation, an AOP interceptor, and a thread‑safe ConcurrentHashMap to store a unique key ( userId + requestPath) with an expiration time.
Define @NoRepeatSubmit with timeout and message.
Intercept annotated methods, generate the unique key, and store it in the map.
If the key already exists and is not expired, reject the request.
After processing, optionally remove the key.
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NoRepeatSubmit {
long timeout() default 5;
TimeUnit unit() default TimeUnit.SECONDS;
String message() default "请勿重复提交,请稍后再试!";
} import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
@Aspect
@Component
public class NoRepeatSubmitAspect {
private final Map<String, Long> localCache = new ConcurrentHashMap<>();
@Pointcut("@annotation(com.yourpackage.NoRepeatSubmit)")
public void noRepeatSubmitPointcut() {}
@Around("noRepeatSubmitPointcut() && @annotation(noRepeatSubmit)")
public Object around(ProceedingJoinPoint joinPoint, NoRepeatSubmit noRepeatSubmit) throws Throwable {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String userId = request.getHeader("X-User-ID");
String requestPath = request.getRequestURI();
String cacheKey = userId + ":" + requestPath;
long currentTime = System.currentTimeMillis();
long expireTime = currentTime + noRepeatSubmit.unit().toMillis(noRepeatSubmit.timeout());
if (localCache.containsKey(cacheKey)) {
long lastRequestTime = localCache.get(cacheKey);
if (lastRequestTime > currentTime) {
throw new RuntimeException(noRepeatSubmit.message());
}
}
localCache.put(cacheKey, expireTime);
try {
return joinPoint.proceed();
} finally {
// optional: localCache.remove(cacheKey);
}
}
}Pros & Cons & Suitable Scenarios
Pros: Lightweight, no external dependencies, fast development.
Cons: Fails in distributed deployments because caches are not shared.
Suitable for: Single‑service, low‑concurrency internal tools.
2. Distributed Scenario: Redis + Atomic Operations
When the application is split into microservices or multiple instances, a shared cache like Redis is required. The solution uses SETNX (or setIfAbsent) to ensure atomic insertion of a unique key.
Replace the local map with Redis.
Use setIfAbsent(key, value, timeout, unit) to atomically acquire the lock.
If the operation fails, reject the duplicate request.
After business logic, delete the key to allow quick retries.
import org.springframework.data.redis.core.RedisTemplate;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.annotation.Resource;
@Aspect
@Component
public class RedisNoRepeatSubmitAspect {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Pointcut("@annotation(com.yourpackage.NoRepeatSubmit)")
public void noRepeatSubmitPointcut() {}
@Around("noRepeatSubmitPointcut() && @annotation(noRepeatSubmit)")
public Object around(ProceedingJoinPoint joinPoint, NoRepeatSubmit noRepeatSubmit) throws Throwable {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String userId = request.getHeader("X-User-ID");
String requestPath = request.getRequestURI();
String cacheKey = "anti_duplicate:" + userId + ":" + requestPath;
Boolean isSuccess = redisTemplate.opsForValue().setIfAbsent(
cacheKey,
"1",
noRepeatSubmit.timeout(),
noRepeatSubmit.unit()
);
if (isSuccess == null || !isSuccess) {
throw new RuntimeException(noRepeatSubmit.message());
}
try {
return joinPoint.proceed();
} finally {
redisTemplate.delete(cacheKey);
}
}
}Pros & Cons & Suitable Scenarios
Pros: Distributed shared cache, high reliability under concurrency.
Cons: Requires a reliable Redis service.
Suitable for: Microservice architectures, high‑traffic e‑commerce order systems.
3. Core Scenario: Database Unique Index
For critical business such as orders or payments, a database‑level unique constraint provides the ultimate safeguard. Even if previous layers miss a duplicate, the insert will fail with a DuplicateKeyException, which can be caught and transformed into a user‑friendly message.
Create a composite unique index on columns that identify a transaction (e.g., user_id + order_no).
When a duplicate request reaches the DB, the unique index triggers an exception.
Catch the exception and return a “duplicate submission” response.
-- Example table with composite unique index
CREATE TABLE `t_order` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`user_id` VARCHAR(64) NOT NULL COMMENT '用户ID',
`order_no` VARCHAR(64) NOT NULL COMMENT '订单号(唯一)',
`amount` DECIMAL(10,2) NOT NULL COMMENT '订单金额',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_order` (`user_id`,`order_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表'; import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.UUID;
@Service
public class OrderService {
@Resource
private OrderMapper orderMapper;
@Transactional(rollbackFor = Exception.class)
public String createOrder(String userId, BigDecimal amount) {
String orderNo = UUID.randomUUID().toString().replace("-", "");
OrderDO orderDO = new OrderDO();
orderDO.setUserId(userId);
orderDO.setOrderNo(orderNo);
orderDO.setAmount(amount);
try {
orderMapper.insert(orderDO);
return "订单创建成功,订单号:" + orderNo;
} catch (DuplicateKeyException e) {
throw new RuntimeException("订单已存在,请勿重复提交!");
}
}
}Pros & Cons & Suitable Scenarios
Pros: Ultimate protection; data cannot be duplicated.
Cons: Requires schema changes; insert failures raise exceptions (minimal performance impact).
Suitable for: Core business processes such as order creation, payment, fund transfer.
4. Front‑Back Separation: Token Verification (CSRF Protection)
In SPA architectures, a token generated by the backend and stored in Redis can prevent both duplicate submissions and CSRF attacks.
Frontend requests a token from /token/get.
Backend creates a UUID token, stores it in Redis with a short TTL, and returns it.
Frontend includes the token in request headers when submitting.
Backend validates the token: if present, delete it and proceed; otherwise reject.
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@RestController
@RequestMapping("/token")
public class TokenController {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@GetMapping("/get")
public String getToken(HttpServletRequest request) {
String userId = request.getHeader("X-User-ID");
String token = "submit_token:" + UUID.randomUUID().toString().replace("-", "");
redisTemplate.opsForValue().set(token, userId, 30, TimeUnit.MINUTES);
return token;
}
} import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
@Aspect
@Component
public class TokenNoRepeatSubmitAspect {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Pointcut("@annotation(com.yourpackage.NoRepeatSubmit)")
public void noRepeatSubmitPointcut() {}
@Around("noRepeatSubmitPointcut() && @annotation(noRepeatSubmit)")
public Object around(ProceedingJoinPoint joinPoint, NoRepeatSubmit noRepeatSubmit) throws Throwable {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String token = request.getHeader("X-Submit-Token");
if (token == null || token.isEmpty()) {
throw new RuntimeException("请先获取提交Token!");
}
Boolean isDeleted = redisTemplate.delete(token);
if (isDeleted == null || !isDeleted) {
throw new RuntimeException(noRepeatSubmit.message());
}
return joinPoint.proceed();
}
}Pros & Cons & Suitable Scenarios
Pros: Simultaneously prevents duplicate submissions and CSRF attacks; elegant for SPA front‑ends.
Cons: Requires an extra token request (negligible performance impact).
Suitable for: Front‑back separated projects (Vue, React, etc.) using SpringBoot.
Solution Selection Summary
Single service, low concurrency: Local cache + AOP – lightweight, fast, but not distributed.
Microservice, high concurrency: Redis + atomic ops – shared cache, high reliability, needs Redis HA.
Core order/payment business: Database unique index – ultimate data safety, requires schema design.
Front‑back separation: Token verification – prevents duplicate and CSRF, minimal overhead.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Architecture Digest
Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
