Implementing Request Debounce in Backend Systems Using Redis and Redisson
This article explains why request debouncing is essential for backend APIs, identifies the types of endpoints that need it, and provides two distributed solutions—shared Redis cache and Redisson locks—along with complete Java annotation and aspect implementations, testing results, and best‑practice recommendations.
As a seasoned backend Java developer, the author shares experiences with multi‑tenant systems, message centers, and open‑platform integrations, emphasizing that proper coding standards and practical techniques can prevent serious production issues.
What is debounce? It prevents duplicate submissions caused by user errors or network jitter. Front‑end solutions (e.g., button loading states) handle user‑side issues, while back‑end logic must also guard against repeated processing.
An ideal debounce component should be logically correct, fast, easy to integrate, and provide clear user feedback.
Which APIs need debounce?
User input APIs (search, form fields) – throttle requests until input stabilizes.
Button click APIs (submit, save) – wait for a pause before processing.
Scroll‑load APIs (infinite scroll, pull‑to‑refresh) – delay until scrolling stops.
How to detect duplicate requests?
Define a time window, compare key parameters (not necessarily all fields), and optionally compare request URLs.
Distributed deployment solutions
Two approaches are presented:
1. Shared cache (Redis)
Store a temporary key for each request; if the key already exists, reject the request.
2. Distributed lock (Redisson)
Acquire a lock per request; if the lock cannot be obtained, the request is considered a duplicate.
Implementation details
Define a custom annotation to mark methods that require debouncing:
@RequestLockExample controller method:
@PostMapping("/add")
@RequiresPermissions(value = "add")
@Log(methodDesc = "添加用户")
public ResponseEntity<String> add(@RequestBody AddReq addReq) {
return userService.add(addReq);
}Data transfer object:
@Data
public class AddReq {
private String userName;
private String userPhone;
private List<Long> roleIdList;
}Generate a unique lock key using another annotation @RequestKeyParam on method parameters or object fields, and a utility class RequestKeyGenerator that builds the key from annotated values.
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 joinPoint) {
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
RequestLock requestLock = method.getAnnotation(RequestLock.class);
String lockKey = RequestKeyGenerator.getLockKey(joinPoint);
Boolean success = stringRedisTemplate.execute((RedisCallback
) conn ->
conn.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‑based aspect (requires the Redisson client dependency):
@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);
String lockKey = RequestKeyGenerator.getLockKey(joinPoint);
RLock lock = redissonClient.getLock(lockKey);
boolean acquired = false;
try {
acquired = lock.tryLock();
if (!acquired) {
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 (acquired && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}Testing shows the first request succeeds, rapid duplicate submissions are blocked with error code BIZ‑0001, and after the lock expires the request succeeds again. The author notes that true idempotency also requires database unique constraints and additional business checks.
In summary, implementing request debounce with Redis or Redisson provides an effective safeguard against duplicate submissions, but it should be complemented by proper database design and comprehensive business logic.
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.