How to Implement Distributed API Debounce with Redis and Redisson in Java
This article explains why API debounce is essential, identifies the types of endpoints that need it, and provides two distributed solutions—shared Redis cache and Redisson lock—along with complete Java code examples and deployment tips for preventing duplicate submissions in a Spring Boot backend.
Preface
As an experienced backend Java developer who has built management back‑ends and mini‑programs, I have never encountered a production outage caused by code crashes. This reliability stems from simple business logic, strict adherence to a large‑company coding standard, and years of practical experience.
What Is Debounce?
Debounce refers to two problems: user hand‑shaking (multiple clicks) and network jitter (re‑sending the same request). In web systems, uncontrolled form submissions can create duplicate records. Front‑end solutions (e.g., button loading states) mitigate user errors, but network‑induced repeats require back‑end protection.
Debounce is about preventing both user mistakes and network‑induced duplicate requests; the back‑end must enforce idempotency.
Logic must be correct—no false positives.
Response should be fast.
Easy to integrate and decoupled from business logic.
Provide clear user feedback, such as “You clicked too fast.”
Thought Process
Which APIs Need Debounce?
User‑input APIs (search boxes, form fields) that fire frequently.
Button‑click APIs (submit, save settings) that may be clicked repeatedly.
Scroll‑loading APIs (infinite scroll, pull‑to‑refresh) that trigger on rapid scrolling.
How to Identify Duplicate Requests?
First, define a time window—requests spaced farther apart than the window are not duplicates. Second, compare a subset of request parameters that uniquely identify the operation (e.g., user ID, article ID). Third, optionally compare the request URL.
Distributed Deployment: How to Implement Debounce?
Two common approaches are presented.
Shared Cache (Redis)
Redis, Zookeeper, etc., are typical distributed components, but Redis is usually preferred because it is already part of most web stacks.
Distributed Lock (Redisson)
Redisson implements a lock‑acquire‑then‑expire pattern, ensuring that only the first request proceeds within the lock’s TTL.
Specific Implementation
Below is a simplified controller method for adding a user.
@PostMapping("/add")
@RequiresPermissions(value = "add")
@Log(methodDesc = "Add user")
public ResponseEntity<String> 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<Long> roleIdList;
}A custom annotation @RequestLock marks the method for debounce handling.
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 {}The lock key is generated by scanning parameters annotated with @RequestKeyParam or their fields.
public class RequestKeyGenerator {
public static 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 kp = params[i].getAnnotation(RequestKeyParam.class);
if (kp != null) {
sb.append(lock.delimiter()).append(args[i]);
}
}
if (sb.length() == 0) {
Annotation[][] paramAnnos = method.getParameterAnnotations();
for (int i = 0; i < paramAnnos.length; i++) {
for (Field f : args[i].getClass().getDeclaredFields()) {
if (f.isAnnotationPresent(RequestKeyParam.class)) {
f.setAccessible(true);
sb.append(lock.delimiter()).append(ReflectionUtils.getField(f, args[i]));
}
}
}
}
return lock.prefix() + sb;
}
}Redis‑based aspect:
@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 jp) throws Throwable {
Method method = ((MethodSignature) jp.getSignature()).getMethod();
RequestLock lock = method.getAnnotation(RequestLock.class);
String lockKey = RequestKeyGenerator.getLockKey(jp);
Boolean success = stringRedisTemplate.execute((RedisCallback<Boolean>) conn ->
conn.set(lockKey.getBytes(), new byte[0],
Expiration.from(lock.expire(), lock.timeUnit()),
RedisStringCommands.SetOption.SET_IF_ABSENT));
if (!success) {
throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "Your operation is too fast, please try later");
}
try {
return jp.proceed();
} catch (Throwable t) {
throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "System error");
}
}
}Redisson‑based aspect:
@Aspect
@Configuration
@Order(2)
public class RedissonRequestLockAspect {
private final RedissonClient redissonClient;
@Autowired
public RedissonRequestLockAspect(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
}
@Around("execution(public * * (..)) && @annotation(com.summo.demo.config.requestlock.RequestLock)")
public Object interceptor(ProceedingJoinPoint jp) throws Throwable {
Method method = ((MethodSignature) jp.getSignature()).getMethod();
RequestLock lock = method.getAnnotation(RequestLock.class);
String lockKey = RequestKeyGenerator.getLockKey(jp);
RLock rLock = redissonClient.getLock(lockKey);
boolean acquired = rLock.tryLock();
if (!acquired) {
throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "Your operation is too fast, please try later");
}
rLock.lock(lock.expire(), lock.timeUnit());
try {
return jp.proceed();
} finally {
if (rLock.isHeldByCurrentThread()) {
rLock.unlock();
}
}
}
}Testing shows the first request succeeds, rapid duplicate requests are blocked with an error message, and after the lock expires the request succeeds again.
Debounce works, but true idempotency also requires business‑level checks and database unique constraints (e.g., a unique index on userPhone).
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 Backend Technology
Focus on Java-related technologies: SSM, Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading. Occasionally cover DevOps tools like Jenkins, Nexus, Docker, and ELK. Also share technical insights from time to time, committed to Java full-stack development!
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.
