Preventing Duplicate Form Submissions with a Redis‑Based Request Lock in Spring Boot
This article explains why duplicate form submissions occur, the risks they pose, and demonstrates a Spring Boot solution that uses a custom @RequestLock annotation together with Redis SETNX to generate a unique key and block rapid repeated requests, complete with project setup, code examples, and usage instructions.
When users click a submit button multiple times, refresh the page, or navigate back and resubmit, the same request can be processed repeatedly, leading to dirty data, potential denial‑of‑service attacks, and poor user experience.
The article first lists three typical scenarios that cause duplicate submissions and then describes the problems caused by unchecked repeats, such as database contamination, increased load, and security risks.
To solve the issue, a Redis‑based lock is introduced. By annotating a controller method with @RequestLock(prefix = "addUser") and marking the unique request fields (e.g., phone number) with @RequestKeyParam(name = "phone") , a composite key is generated and stored in Redis with a short expiration (default 2 seconds). If the key already exists, the second request is rejected.
The sample project structure includes a Maven pom.xml with Spring Boot, Redis, Web, and AspectJ dependencies, and an application.properties file configuring the Redis connection.
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" ...> ... <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.5</version> </dependency> </project>
The core annotations are defined as follows:
package com.example.requestlock.lock.annotation; import java.lang.annotation.*; @Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface RequestKeyParam { String name() default ""; }
package com.example.requestlock.lock.annotation; import java.lang.annotation.*; import java.util.concurrent.TimeUnit; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface RequestLock { String prefix() default ""; int expire() default 2; TimeUnit timeUnit() default TimeUnit.SECONDS; String delimiter() default ":"; }
The aspect RequestLockMethodAspect intercepts methods annotated with @RequestLock , builds the lock key via RequestKeyGenerator , attempts to set it in Redis with SET_IF_ABSENT , and either proceeds with the method or returns a rate‑limit message.
The key generator scans method parameters and fields for @RequestKeyParam to compose the final key:
public String getLockKey(ProceedingJoinPoint joinPoint) { MethodSignature ms = (MethodSignature) joinPoint.getSignature(); Method method = ms.getMethod(); RequestLock lock = method.getAnnotation(RequestLock.class); Object[] args = joinPoint.getArgs(); Parameter[] params = method.getParameters(); StringBuilder sb = new StringBuilder(); for (int i = 0; i < params.length; i++) { RequestKeyParam p = params[i].getAnnotation(RequestKeyParam.class); if (p != null) { sb.append(lock.delimiter()).append(args[i]); } } // fallback to field inspection omitted for brevity return lock.prefix() + sb.toString(); }
The controller demonstrates two endpoints: /addUser1 without lock (always succeeds) and /addUser2 with @RequestLock (rejects rapid duplicate submissions).
Principle: Redis’s single‑threaded command execution and key expiration ensure that once a lock key is set, any subsequent request with the same key within the expiration window is considered a duplicate and blocked.
Usage steps: add @RequestLock(prefix = "addUser") to a controller method, annotate the unique field in the request object with @RequestKeyParam(name = "phone") , and the framework handles duplicate detection automatically.
Demo screenshots show that addUser1 always returns "添加成功" regardless of click frequency, while addUser2 returns "添加成功" on the first click and "您的操作太快了,请稍后重试" on rapid subsequent clicks.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
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.