Backend Development 15 min read

Implementing Interface Debounce with Redis and Redisson in Java Backend

This article explains how to prevent duplicate submissions in Java backend APIs by applying debounce techniques using shared Redis caches or Redisson distributed locks, detailing the design of a @RequestLock annotation, key generation, aspect implementation, and practical testing results.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Implementing Interface Debounce with Redis and Redisson in Java Backend

As an experienced Java backend developer, the author shares practical experience designing single‑/multi‑tenant systems, integrating with open platforms, and building message centers, emphasizing that strict code standards and years of practice have prevented production crashes.

What is debounce? It protects against user mis‑clicks and network jitter. In web systems, uncontrolled form submissions can cause duplicate records, so both frontend (loading state) and backend (debounce logic) are needed.

An ideal debounce component should be logically correct, fast, easy to integrate, and provide clear user feedback such as "You clicked too fast".

Which APIs Need Debounce?

User input APIs (e.g., search boxes, form inputs) – throttle requests until the user stops typing.

Button click APIs (e.g., submit, save) – delay execution until clicks cease.

Scroll‑load APIs (e.g., pull‑to‑refresh, infinite scroll) – wait for scrolling to stop before sending requests.

How to Identify Duplicate Requests?

Determine duplication by:

Ensuring a time interval between two calls exceeds a configured threshold.

Comparing a subset of request parameters that uniquely identify the operation.

Optionally comparing request URLs.

Debounce in Distributed Deployment

Two solutions are presented:

Shared Cache (Redis)

The flow diagram (omitted) shows using a Redis key to lock a request. Common distributed components include Redis and Zookeeper, but Redis is usually preferred.

Distributed Lock (Redisson)

Redisson provides a higher‑level distributed lock API.

Concrete Implementation

Example controller method:

@PostMapping("/add")
@RequiresPermissions(value = "add")
@Log(methodDesc = "添加用户")
public ResponseEntity
add(@RequestBody AddReq addReq) {
    return userService.add(addReq);
}

Request DTO (AddReq):

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 @RequestLock annotation (properties: prefix, expire, timeUnit, delimiter) and @RequestKeyParam to mark parameters that compose the lock key.

Key generation utility:

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]);
        }
        // fallback to field annotations when method parameters are not annotated
        if (sb.length() == 0) {
            for (int i = 0; i < args.length; i++) {
                Object obj = args[i];
                for (Field field : obj.getClass().getDeclaredFields()) {
                    if (field.getAnnotation(RequestKeyParam.class) != null) {
                        field.setAccessible(true);
                        sb.append(requestLock.delimiter()).append(ReflectionUtils.getField(field, obj));
                    }
                }
            }
        }
        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);
        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, "系统异常");
        }
    }
}

Redisson Distributed Lock 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 joinPoint) throws Throwable {
        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();
            }
        }
    }
}

Redisson client configuration (RedissonConfig) is also provided to create a singleton client.

Testing Results

First submission succeeds ("Add user success"). Rapid repeated submissions within the lock period return "BIZ-0001: 您的操作太快了,请稍后重试". After the lock expires, submissions succeed again, confirming the debounce works.

However, once the cache expires or the lock is released, duplicate requests can still be processed; true idempotency also requires business‑level checks such as unique constraints on the database.

For production, it is recommended to include additional identifiers (user ID, IP, etc.) in the lock key to reduce false positives.

JavaRedisSpringDistributed LockRedissonDebounceRequest Lock
Code Ape Tech Column
Written by

Code Ape Tech Column

Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.