Implementing Idempotent Requests with Local Locks, AOP, and Redis in Java
This article explains the concept of idempotency in web services, analyzes common causes of duplicate submissions, and presents multiple backend solutions—including frontend button disabling, PRG pattern, session flags, local locks using ConcurrentHashMap, AOP interceptors, and distributed Redis locks—accompanied by complete Java code examples.
Idempotency means that multiple identical requests have the same effect as a single request. In typical programming, SELECT queries are naturally idempotent, DELETE is idempotent, simple UPDATE is idempotent, while INSERT and incremental UPDATE are not.
Causes of duplicate submissions
Repeated clicks, page refreshes, browser back/forward actions, HTTP request retries, Nginx retransmission, and distributed RPC retries can all trigger duplicate submissions.
Backend solutions
1) Front‑end button disabling – use JavaScript components to prevent multiple clicks.
2) Post/Redirect/Get (PRG) pattern – after a form POST, redirect to a success page to avoid resubmission on F5.
3) Session flag – store a unique token in the session and compare it with a hidden form field; process only the first submission.
4) Cache‑control headers – set appropriate HTTP caching headers (not suitable for mobile apps).
5) Database constraints – use unique indexes for INSERT and optimistic locking (version field) for UPDATE.
6) Pessimistic lock – use SELECT … FOR UPDATE (avoid deadlocks, less efficient).
7) Local lock (focus of this article) – implement a lock with ConcurrentHashMap<String, Object> and a scheduled task to release it after a delay. The lock key is generated by MD5 of request parameters (Content‑MD5).
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Resubmit {
/** Delay time in seconds before the request can be submitted again */
int delaySeconds() default 20;
}Lock implementation:
public final class ResubmitLock {
private static final ConcurrentHashMap<String, Object> LOCK_CACHE = new ConcurrentHashMap<>(200);
private static final ScheduledThreadPoolExecutor EXECUTOR = new ScheduledThreadPoolExecutor(5, new ThreadPoolExecutor.DiscardPolicy());
private ResubmitLock() {}
private static class SingletonInstance { private static final ResubmitLock INSTANCE = new ResubmitLock(); }
public static ResubmitLock getInstance() { return SingletonInstance.INSTANCE; }
public static String handleKey(String param) { return DigestUtils.md5Hex(param == null ? "" : param); }
public boolean lock(final String key, Object value) { return Objects.isNull(LOCK_CACHE.putIfAbsent(key, value)); }
public void unLock(final boolean lock, final String key, final int delaySeconds) {
if (lock) {
EXECUTOR.schedule(() -> LOCK_CACHE.remove(key), delaySeconds, TimeUnit.SECONDS);
}
}
}AOP interceptor that applies the lock based on the @Resubmit annotation:
@Aspect
@Component
public class ResubmitDataAspect {
@Around("@annotation(com.cn.xxx.common.annotation.Resubmit)")
public Object handleResubmit(ProceedingJoinPoint joinPoint) throws Throwable {
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
Resubmit annotation = method.getAnnotation(Resubmit.class);
int delaySeconds = annotation.delaySeconds();
Object[] args = joinPoint.getArgs();
String key = "";
Object firstParam = args[0];
if (firstParam instanceof RequestDTO) {
JSONObject requestDTO = JSONObject.parseObject(firstParam.toString());
JSONObject data = JSONObject.parseObject(requestDTO.getString("data"));
if (data != null) {
StringBuilder sb = new StringBuilder();
data.forEach((k, v) -> sb.append(v));
key = ResubmitLock.handleKey(sb.toString());
}
}
boolean lock = false;
try {
lock = ResubmitLock.getInstance().lock(key, new Object());
if (lock) {
return joinPoint.proceed();
} else {
return new ResponseDTO<>(ResponseCode.REPEAT_SUBMIT_OPERATION_EXCEPTION);
}
} finally {
ResubmitLock.getInstance().unLock(lock, key, delaySeconds);
}
}
}Usage example on a controller method:
@ApiOperation(value = "Save my post", notes = "Save my post")
@PostMapping("/posts/save")
@Resubmit(delaySeconds = 10)
public ResponseDTO<BaseResponseDataDTO> saveBbsPosts(@RequestBody @Validated RequestDTO<BbsPostsRequestDTO> requestDto) {
return bbsPostsBizService.saveBbsPosts(requestDto);
}Distributed Redis lock (alternative)
By adding spring-boot-starter-web, spring-boot-starter-aop, and spring-boot-starter-data-redis dependencies, a Redis‑based lock can be implemented using setIfAbsent with an expiration time, providing a safe lock for clustered environments.
Configuration example (pom.xml) and Redis properties are shown, followed by a utility class RedisLockHelper that offers tryLock, lock, and unlock methods with optional delayed release.
Overall, the article demonstrates how to achieve idempotent request handling in Java backend services through various techniques ranging from simple front‑end controls to sophisticated distributed locks.
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.
