Implementing Interface Debounce and Distributed Locks in Java Backend Applications
This article explains how to prevent duplicate API submissions by applying debounce principles and implementing distributed locks using Redis and Redisson in a Java Spring backend, providing detailed code examples, key generation strategies, and practical testing results.
The author, a senior Java backend architect, shares practical techniques for preventing duplicate submissions (debounce) of API requests.
What is debounce? It protects against user accidental multiple clicks and network retries; while the frontend can show loading states, the backend must also enforce debounce logic to avoid processing the same request repeatedly.
An ideal debounce component should be logically correct, fast, easy to integrate, and provide clear user feedback.
Which interfaces need debounce? User input interfaces (e.g., search boxes), button click interfaces (e.g., form submissions), and scroll‑loading interfaces (e.g., infinite scroll) are typical candidates.
To determine if two requests are duplicates, compare a time interval, key parameters with strong identifiers, and optionally the request URL.
Distributed deployment solutions: Two common approaches are using a shared cache (Redis) or a distributed lock (Redisson), each illustrated with flow diagrams.
Implementation example – user‑add API:
@PostMapping("/add")
@RequiresPermissions(value = "add")
@Log(methodDesc = "添加用户")
public ResponseEntity
add(@RequestBody AddReq addReq) {
return userService.add(addReq);
}Request DTO:
package com.summo.demo.model.request;
import java.util.List;
import lombok.Data;
@Data
public class AddReq {
/** 用户名称 */ private String userName;
/** 用户手机号 */ private String userPhone;
/** 角色ID列表 */ private List
roleIdList;
}Define a custom annotation to mark methods that require request locking:
@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RequestLock {
String prefix() default "reqLock:";
long expire() default 5;
TimeUnit timeUnit() default TimeUnit.SECONDS;
String delimiter() default "&";
}Generate a unique lock key based on annotated parameters:
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‑based lock 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) throws Throwable {
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
RequestLock requestLock = method.getAnnotation(RequestLock.class);
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();
} finally {
stringRedisTemplate.delete(lockKey);
}
}
}Redisson‑based lock aspect and configuration:
@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);
}
} @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) throws Throwable {
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
RequestLock requestLock = method.getAnnotation(RequestLock.class);
String lockKey = RequestKeyGenerator.getLockKey(joinPoint);
RLock lock = redissonClient.getLock(lockKey);
boolean locked = lock.tryLock();
if (!locked) {
throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "您的操作太快了,请稍后重试");
}
lock.lock(requestLock.expire(), requestLock.timeUnit());
try {
return joinPoint.proceed();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}Testing shows the first submission succeeds, rapid duplicate submissions are blocked with an error, and after the lock expires the request succeeds again, confirming the debounce works. However, true idempotency also requires business‑level checks such as unique constraints in the database.
In summary, combining request‑level debouncing with Redis or Redisson distributed locks provides an effective safeguard against duplicate API calls in distributed Java backend systems.
Top Architect
Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.
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.