SpringBoot Techniques for API Debounce to Eliminate Duplicate Submissions
The article explains why API debounce is essential, identifies the types of endpoints that need it, and walks through two SpringBoot solutions—Redis‑based shared cache and Redisson distributed lock—complete with custom annotations, key generation, code samples, test results, and practical limitations.
Debounce (防抖) prevents duplicate requests caused by user mis‑clicks or network jitter. In web systems, form submissions can be sent multiple times, creating duplicate records; front‑end loading states handle user clicks, but back‑end logic is required for network‑induced repeats.
Ideal Debounce Component
Correct logic without false positives.
Fast response.
Easy integration and business‑logic decoupling.
Clear user feedback, e.g., "You clicked too fast".
Which APIs Need Debounce?
User‑input APIs such as search or form fields where rapid input does not require immediate requests.
Button‑click APIs like submit or save where users may click repeatedly.
Scroll‑load APIs (pull‑to‑refresh, infinite scroll) that can fire many requests during scrolling.
How to Identify Duplicate Requests
Determine duplication by:
Setting a time interval; requests spaced beyond it are not duplicates.
Comparing a subset of parameters that uniquely identify the request.
Optionally comparing the request URL.
Distributed Deployment Options
Shared Cache (Redis)
A flowchart (image) shows using a shared Redis cache. The core code executes stringRedisTemplate.execute with SET_IF_ABSENT and an expiration time to create a lock key.
Distributed Lock (Redisson)
Another flowchart illustrates using Redisson's distributed lock. Redisson is preferred because it is commonly part of a SpringBoot stack.
Custom Annotations
@RequestLockis placed on the controller method to configure lock prefix, expiration, time unit, and delimiter. @RequestKeyParam marks method parameters or object fields that should be concatenated to form the lock key.
Lock Key Generation
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 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) {
Method method = ((MethodSignature) joinPoint.getSignature()).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<Boolean>) 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 Implementation
@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 joinPoint) {
Method method = ((MethodSignature) joinPoint.getSignature()).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 (Throwable t) {
throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "系统异常");
} finally {
if (isLocked && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}Testing Results
First submission: "Add user success".
Rapid repeat within lock period: "BIZ-0001: 您的操作太快了,请稍后重试".
After a few seconds (lock expired): "Add user success" again.
The tests confirm that debounce works, but once the cache expires or the lock is released, duplicate requests can still be processed. True idempotency also requires business‑level checks such as a unique index on userPhone and possibly including user‑ID or IP in the lock key.
Practical Recommendations
When generating the lock key, select fields that uniquely identify the request but keep the key size small for Redis efficiency. In production, add user‑related information (e.g., user ID, IP region) to reduce false positives. Combine debounce with database unique constraints for complete protection against duplicate data.
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.
Programmer XiaoFu
xiaofucode.com – a programmer learning guide driven by the pursuit of profit
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.
