How to Implement Distributed API Debounce in Java with Redis and Redisson
This article explains why API debounce is needed in web back‑ends, identifies the types of endpoints that require it, outlines how to detect duplicate requests, and provides two concrete distributed solutions—shared Redis cache and Redisson lock—complete with annotation design, key generation logic, and full Java code examples.
What is debounce
Debounce prevents duplicate submissions caused by rapid user clicks or network retries. A backend debounce component must reject repeated requests that represent the same logical operation.
API categories that need debounce
User‑input APIs (e.g., search boxes, form fields) that fire frequently but do not require immediate processing.
Button‑click APIs (e.g., submit form, save settings) where a user may click repeatedly.
Scroll‑load APIs (infinite scroll, pull‑to‑refresh) that can be triggered many times during scrolling.
How to determine a duplicate request
Define a time window; requests spaced farther apart than the window are not duplicates.
Compare a subset of request parameters that uniquely identify the operation (e.g., userId, orderNo).
Optionally compare the request URL.
Distributed deployment options
1. Shared Redis cache
Store a lock key in Redis with an expiration. Use the SET_IF_ABSENT option so that only the first request creates the key; subsequent requests see the key and are rejected.
2. Redisson distributed lock
Acquire a Redisson RLock, set an expiration, and release it after processing. This provides a full distributed‑lock API.
Annotation definitions
public @interface RequestLock {
String prefix() default ""; // lock key prefix
long expire() default 5L; // lock expiration
TimeUnit timeUnit() default TimeUnit.SECONDS;
String delimiter() default "&"; // separator for composite keys
} @Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RequestKeyParam {}Lock key generation
The generator inspects method parameters and, if necessary, the fields of those parameters to build a composite key based on @RequestKeyParam annotations.
public static String getLockKey(ProceedingJoinPoint joinPoint) {
MethodSignature ms = (MethodSignature) joinPoint.getSignature();
Method method = ms.getMethod();
RequestLock lockAnno = 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 kp = params[i].getAnnotation(RequestKeyParam.class);
if (kp != null) {
sb.append(lockAnno.delimiter()).append(args[i]);
}
}
if (sb.length() == 0) {
for (int i = 0; i < args.length; i++) {
Object obj = args[i];
for (Field f : obj.getClass().getDeclaredFields()) {
if (f.getAnnotation(RequestKeyParam.class) != null) {
f.setAccessible(true);
sb.append(lockAnno.delimiter())
.append(ReflectionUtils.getField(f, obj));
}
}
}
}
return lockAnno.prefix() + sb;
}Redis‑based aspect
Uses StringRedisTemplate.execute with SET_IF_ABSENT to create the lock. If the lock cannot be set, a BizException with a “too fast” message is thrown.
@Around("execution(public * *(..)) && @annotation(com.summo.demo.config.requestlock.RequestLock)")
public Object interceptor(ProceedingJoinPoint jp) {
Method method = ((MethodSignature) jp.getSignature()).getMethod();
RequestLock rl = method.getAnnotation(RequestLock.class);
if (StringUtils.isEmpty(rl.prefix())) {
throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "重复提交前缀不能为空");
}
String lockKey = RequestKeyGenerator.getLockKey(jp);
Boolean success = stringRedisTemplate.execute(conn ->
conn.set(lockKey.getBytes(), new byte[0],
Expiration.from(rl.expire(), rl.timeUnit()),
RedisStringCommands.SetOption.SET_IF_ABSENT));
if (!success) {
throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "您的操作太快了,请稍后重试");
}
try {
return jp.proceed();
} catch (Throwable t) {
throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "系统异常");
}
}Redisson‑based aspect
Acquires a Redisson lock, tries to obtain it with tryLock(), sets the expiration, proceeds with the method, and finally releases the lock.
@Around("execution(public * *(..)) && @annotation(com.summo.demo.config.requestlock.RequestLock)")
public Object interceptor(ProceedingJoinPoint jp) {
Method method = ((MethodSignature) jp.getSignature()).getMethod();
RequestLock rl = method.getAnnotation(RequestLock.class);
String lockKey = RequestKeyGenerator.getLockKey(jp);
RLock lock = redissonClient.getLock(lockKey);
boolean acquired = lock.tryLock();
if (!acquired) {
throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "您的操作太快了,请稍后重试");
}
lock.lock(rl.expire(), rl.timeUnit());
try {
return jp.proceed();
} catch (Throwable t) {
throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "系统异常");
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}Testing results
First submission returns "添加用户成功" (user added).
Rapid repeat within the lock period returns error code BIZ-0001: "您的操作太快了,请稍后重试".
After the lock expires (a few seconds) the request succeeds again.
Observations and recommendations
The debounce works while the Redis entry or Redisson lock exists, but once the lock expires the same request can be processed again. True idempotency therefore requires additional safeguards such as database unique constraints (e.g., a unique index on userPhone) and inclusion of user‑specific fields (userId, IP) in the lock key to avoid false positives for multi‑user scenarios.
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.
Java Tech Enthusiast
Sharing computer programming language knowledge, focusing on Java fundamentals, data structures, related tools, Spring Cloud, IntelliJ IDEA... Book giveaways, red‑packet rewards and other perks await!
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.
