Implementing Interface Debounce with Distributed Locks in Java (Redis & Redisson)
This article explains the concept of request debouncing, identifies which API endpoints need it, and provides a detailed tutorial on implementing distributed debounce using shared Redis cache or Redisson locks in a Spring Boot backend, complete with annotation design, unique key generation, and code examples.
In the preface, the author shares personal experience developing backend Java services and emphasizes the importance of preventing duplicate submissions caused by user errors or network instability.
The article defines "debounce" as both preventing user hand‑shake and network jitter, explaining why frontend loading states are insufficient and why backend logic is required.
Key characteristics of an ideal debounce component are listed: logical correctness, fast response, easy integration, and clear user feedback.
It then analyzes which types of interfaces typically need debounce, such as user input, button click, and scroll‑load APIs, and describes how to determine duplicate requests by time interval, parameter comparison, and optional URL comparison.
Two distributed deployment solutions are presented:
Using a shared cache (Redis) – a flowchart is shown.
Using a distributed lock (Redisson) – another flowchart is shown.
For the concrete implementation, the article provides several code snippets.
@PostMapping("/add")
@RequiresPermissions(value = "add")
@Log(methodDesc = "添加用户")
public ResponseEntity
add(@RequestBody AddReq addReq) {
return userService.add(addReq);
} package com.summo.demo.model.request;
import java.util.List;
import lombok.Data;
@Data
class AddReq {
/** 用户名称 */
private String userName;
/** 用户手机号 */
private String userPhone;
/** 角色ID列表 */
private List
roleIdList;
}A custom annotation @RequestLock is introduced to mark methods that require debounce. The annotation includes properties for Redis lock prefix, expiration time, time unit, and a delimiter for building the lock key.
package com.example.requestlock.lock.annotation;
import java.lang.annotation.*;
/**
* @description 加上这个注解可以将参数设置为key
*/
@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RequestKeyParam {
}The RequestKeyGenerator class extracts the annotated parameters or fields at runtime to build a unique lock key.
public class RequestKeyGenerator {
/**
* 获取LockKey
*/
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;
}
}Two aspect implementations enforce the debounce logic:
1. RedisRequestLockAspect uses StringRedisTemplate to execute a SET command with SET_IF_ABSENT and an expiration time.
@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
) 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, "系统异常");
}
}
}2. RedissonRequestLockAspect obtains a distributed lock from Redisson, tries to acquire it, sets an expiration, proceeds with the method, and finally releases the lock.
@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) {
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();
}
}
}
}The article also shows how to add the Redisson dependency and configure a RedissonClient bean.
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.10.6</version>
</dependency> @Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
return Redisson.create(config);
}
}Testing results demonstrate that the first submission succeeds, rapid repeated submissions are blocked with a "operation too fast" error, and after the lock expires the request succeeds again. The author notes that true idempotency also requires database unique constraints and business‑level checks.
Finally, the article includes promotional messages and related reading links, which are not part of the technical content.
Architect
Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.
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.