Prevent Duplicate API Submissions in Spring Boot with Redis, AOP & Custom Annotations
This article explains how to implement duplicate submission prevention in Spring Boot using a token mechanism with Redis, AOP, and custom annotations, covering idempotency concepts, solution comparisons, code implementation with Lua scripts for atomic operations, and common pitfalls.
What is Interface Idempotency?
Idempotency means multiple executions produce the same effect as a single execution. In web development, non-idempotent scenarios include users double-clicking payment buttons, frontend network retries causing duplicate inserts, and message queue consumers retrying the same message. To ensure data consistency, key interfaces require duplicate submission prevention.
Common Solutions Comparison
Database Unique Index : Uses unique constraints to block duplicate inserts. Pros: bottom-layer guarantee. Cons: only works for insert scenarios. Applicable: all unique data scenarios.
Token Mechanism : Request token before submit, validate and delete on submit. Pros: universal, clear logic. Cons: requires frontend cooperation, extra interaction. Applicable: form submissions, critical operations.
Distributed Lock : Lock based on request parameters or user ID. Pros: simple, no frontend changes. Cons: lock granularity control complex, may block legitimate requests. Applicable: simple deduplication, non-strong consistency.
State Machine Control : Record state transitions (e.g., order status). Pros: business-level deduplication. Cons: intrusive to business code. Applicable: orders, approval flows.
The article focuses on Token Mechanism + AOP + Redis , the most common enterprise solution with the best user experience.
Core Idea: Token Mechanism
Frontend calls /api/token to obtain a unique token before submission.
Backend stores token in Redis with expiration (e.g., 5 minutes).
Frontend includes token in request header when submitting business data.
Backend AOP intercepts request, checks Redis for token:
Exists : delete token, allow request.
Not exists : duplicate or expired token, reject request.
Why store-then-delete? Redis del is atomic; combined with setnx (Set if Not Exists) ensures only one request succeeds in acquiring the token.
Implementation
1. Custom Annotation
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NoRepeatSubmit {
long expireTime() default 5;
String message() default "请勿重复提交";
}2. Redis Utility Wrapper
@Component
public class RedisUtil {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String PREFIX = "no_repeat_submit:";
public boolean tryLock(String key, long expireTime) {
Boolean success = redisTemplate.opsForValue()
.setIfAbsent(PREFIX + key, "1", expireTime, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
public boolean releaseLock(String key) {
return Boolean.TRUE.equals(redisTemplate.delete(PREFIX + key));
}
}3. AOP Aspect
@Slf4j
@Aspect
@Component
public class NoRepeatSubmitAspect {
@Autowired
private RedisUtil redisUtil;
@Pointcut("@annotation(com.example.annotation.NoRepeatSubmit)")
public void pointcut() {}
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
NoRepeatSubmit annotation = method.getAnnotation(NoRepeatSubmit.class);
HttpServletRequest request = getRequest();
String userId = getCurrentUserId();
String uri = request.getRequestURI();
String paramsHash = getParamsHash(joinPoint.getArgs());
String lockKey = userId + ":" + uri + ":" + paramsHash;
if (redisUtil.releaseLock(lockKey)) {
log.info("重复提交校验通过: key={}", lockKey);
return joinPoint.proceed();
} else {
log.warn("重复提交拦截: key={}", lockKey);
throw new BizException(400, annotation.message());
}
}
// helper methods: getRequest(), getCurrentUserId(), getParamsHash()
}Token Mode vs Lock Mode Token Mode : Frontend requests /token , backend redis.set(token, 1) . On submit, backend redis.del(token) . If delete succeeds (returns 1), first submission; if fails (returns 0), duplicate. More precise, avoids killing concurrent requests. AOP Lock Mode : Direct interception as above. Simultaneous requests may race on del . For rigor, use Redis Lua script for atomic "Get and Delete" or adopt Token Mode.
4. Advanced: Atomic Check-and-Delete with Lua Script
-- Lua script: if key exists, delete it and return 1, else return 0
if redis.call("exists", KEYS[1]) == 1 then
return redis.call("del", KEYS[1])
else
return 0
end public boolean checkAndDelete(String key) {
String script = "if redis.call('exists', KEYS[1]) == 1 then return redis.call('del', KEYS[1]) else return 0 end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
Long result = redisTemplate.execute(redisScript, Collections.singletonList(PREFIX + key));
return result != null && result > 0;
}Replace AOP judgment with if (redisUtil.checkAndDelete(lockKey)).
Deployment Summary
Frontend Cooperation : For Token Mode, frontend must request token on page entry, cache it, and include it on submit.
Key Generation : Must include user identifier to prevent User A's token being reused by User B.
Expiration Time : Always set TTL to prevent Redis memory leaks.
Granularity Control : Include parameter hash in key to allow concurrent requests with different parameters; omit for per-user per-interface locking.
Common Pitfalls
Accidental Blocking of Legitimate Requests : Simple setnx + expire locks auto-release after expiry; if processing exceeds TTL, second request enters. Suitable for short debounce, not Token Mode which must be "one-time".
Transaction Ordering : If method has @Transactional, ensure deduplication runs before transaction starts (default AOP order satisfies this).
Distributed Environment : Must use shared storage (Redis, ZooKeeper), not local ConcurrentHashMap.
Conclusion
Using Custom Annotation + AOP + Redis , duplicate submission logic is decoupled from business code, achieving "out-of-the-box" usability. Developers only add @NoRepeatSubmit on Controller methods to easily solve data corruption from concurrent submissions, greatly improving system robustness.
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.
Xiaolin Talks Programming
Focuses on sharing original technical insights. Senior architect at a top tech company with years of experience in technical architecture and management, and extensive interview experience. Offers one-on-one technical coaching, guiding you from beginner to architecture design to technical management. Follow for free learning resources. Free one-on-one interview coaching to help you land offers quickly.
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.
