Backend Request Debounce and Distributed Lock Implementation in Java
This article explains the concept of request debouncing, outlines ideal debounce component characteristics, identifies which API endpoints need debouncing, and provides detailed Java implementations using shared Redis cache and Redisson distributed locks, including custom annotations, key generation, and practical testing results.
As an experienced Java backend developer, the author shares practical techniques for preventing duplicate submissions (debounce) in web APIs. The article first defines debounce, distinguishes user‑side and network‑side debounce, and lists four desirable properties of a debounce component: logical correctness, fast response, easy integration, and clear user feedback.
Which Interfaces Need Debounce?
Not every API requires debounce. Typical candidates are:
User input interfaces such as search boxes or form fields, where rapid input should be coalesced.
Button‑click interfaces like form submission or settings save, where multiple clicks may occur.
Scroll‑load interfaces such as infinite scrolling or pull‑to‑refresh, which can trigger many requests.
How to Determine Duplicate Requests?
Duplicate detection relies on three checks: a time interval threshold, comparison of key parameters (not necessarily all parameters), and optionally the request URL.
Distributed Deployment Solutions
Two common approaches are presented.
Shared Cache (Redis)
The following flowchart illustrates the Redis‑based solution (image omitted for brevity).
Distributed Lock (Redisson)
The Redisson flowchart is similar, using a distributed lock instead of a simple cache entry.
Both solutions typically use Redis because it is a standard component in most web systems.
Concrete Implementation
Below is a sample controller method for adding a user:
@PostMapping("/add")
@RequiresPermissions(value = "add")
@Log(methodDesc = "添加用户")
public ResponseEntity
add(@RequestBody AddReq addReq) {
return userService.add(addReq);
}The request DTO:
package com.summo.demo.model.request;
import java.util.List;
import lombok.Data;
@Data
public class AddReq {
/** User name */
private String userName;
/** User phone */
private String userPhone;
/** Role ID list */
private List
roleIdList;
}Because the userPhone column lacks a unique index, duplicate submissions would create multiple identical users.
Request Lock Annotation
A custom @RequestLock annotation is introduced. Applying it to a controller method activates the debounce logic.
package com.example.requestlock.lock.annotation;
import java.lang.annotation.*;
/**
* Marks a parameter as part of the lock key.
*/
@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RequestKeyParam {}The key generator extracts values from parameters annotated with @RequestKeyParam (or from fields of those parameters) and builds a unique Redis key:
public class RequestKeyGenerator {
public static String getLockKey(ProceedingJoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
RequestLock requestLock = method.getAnnotation(RequestLock.class);
Object[] args = joinPoint.getArgs();
Parameter[] parameters = method.getParameters();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < parameters.length; i++) {
RequestKeyParam keyParam = parameters[i].getAnnotation(RequestKeyParam.class);
if (keyParam == null) continue;
sb.append(requestLock.delimiter()).append(args[i]);
}
if (StringUtils.isEmpty(sb.toString())) {
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
for (int i = 0; i < parameterAnnotations.length; i++) {
Object object = args[i];
Field[] fields = object.getClass().getDeclaredFields();
for (Field field : fields) {
RequestKeyParam annotation = field.getAnnotation(RequestKeyParam.class);
if (annotation == null) continue;
field.setAccessible(true);
sb.append(requestLock.delimiter()).append(ReflectionUtils.getField(field, object));
}
}
}
return requestLock.prefix() + sb;
}
}Redis Cache Implementation
@Aspect
@Configuration
@Order(2)
public class RedisRequestLockAspect {
private final StringRedisTemplate stringRedisTemplate;
@Autowired
public RedisRequestLockAspect(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Around("execution(public * * (..)) && @annotation(com.summo.demo.config.requestlock.RequestLock)")
public Object interceptor(ProceedingJoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
RequestLock requestLock = method.getAnnotation(RequestLock.class);
if (StringUtils.isEmpty(requestLock.prefix())) {
throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "重复提交前缀不能为空");
}
String lockKey = RequestKeyGenerator.getLockKey(joinPoint);
Boolean success = stringRedisTemplate.execute((RedisCallback
) connection ->
connection.set(lockKey.getBytes(), new byte[0],
Expiration.from(requestLock.expire(), requestLock.timeUnit()),
RedisStringCommands.SetOption.SET_IF_ABSENT));
if (!success) {
throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "您的操作太快了,请稍后重试");
}
try {
return joinPoint.proceed();
} catch (Throwable t) {
throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "系统异常");
}
}
}Redisson Distributed Lock Implementation
@Aspect
@Configuration
@Order(2)
public class RedissonRequestLockAspect {
private RedissonClient redissonClient;
@Autowired
public RedissonRequestLockAspect(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
}
@Around("execution(public * * (..)) && @annotation(com.summo.demo.config.requestlock.RequestLock)")
public Object interceptor(ProceedingJoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
RequestLock requestLock = method.getAnnotation(RequestLock.class);
if (StringUtils.isEmpty(requestLock.prefix())) {
throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "重复提交前缀不能为空");
}
String lockKey = RequestKeyGenerator.getLockKey(joinPoint);
RLock lock = redissonClient.getLock(lockKey);
boolean isLocked = false;
try {
isLocked = lock.tryLock();
if (!isLocked) {
throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "您的操作太快了,请稍后重试");
}
lock.lock(requestLock.expire(), requestLock.timeUnit());
return joinPoint.proceed();
} catch (Exception e) {
throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "您的操作太快了,请稍后重试");
} finally {
if (isLocked && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}Testing shows that the first submission succeeds, rapid duplicate submissions are blocked with an error message, and after the lock expires the request succeeds again. The author notes that true idempotency also requires database constraints (e.g., unique indexes) and possibly additional business‑level checks.
Overall, the article provides a complete, production‑ready guide for implementing request debounce and preventing duplicate submissions in Java backend services using Redis or Redisson.
Top Architect
Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.
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.