How to Implement Distributed API Debounce in Java with Redis and Redisson

This article explains why API debounce is needed in web back‑ends, identifies the types of endpoints that require it, outlines how to detect duplicate requests, and provides two concrete distributed solutions—shared Redis cache and Redisson lock—complete with annotation design, key generation logic, and full Java code examples.

Java Tech Enthusiast
Java Tech Enthusiast
Java Tech Enthusiast
How to Implement Distributed API Debounce in Java with Redis and Redisson

What is debounce

Debounce prevents duplicate submissions caused by rapid user clicks or network retries. A backend debounce component must reject repeated requests that represent the same logical operation.

API categories that need debounce

User‑input APIs (e.g., search boxes, form fields) that fire frequently but do not require immediate processing.

Button‑click APIs (e.g., submit form, save settings) where a user may click repeatedly.

Scroll‑load APIs (infinite scroll, pull‑to‑refresh) that can be triggered many times during scrolling.

How to determine a duplicate request

Define a time window; requests spaced farther apart than the window are not duplicates.

Compare a subset of request parameters that uniquely identify the operation (e.g., userId, orderNo).

Optionally compare the request URL.

Distributed deployment options

1. Shared Redis cache

Store a lock key in Redis with an expiration. Use the SET_IF_ABSENT option so that only the first request creates the key; subsequent requests see the key and are rejected.

Redis shared‑cache flowchart
Redis shared‑cache flowchart

2. Redisson distributed lock

Acquire a Redisson RLock, set an expiration, and release it after processing. This provides a full distributed‑lock API.

Redisson lock flowchart
Redisson lock flowchart

Annotation definitions

public @interface RequestLock {
    String prefix() default "";          // lock key prefix
    long expire() default 5L;              // lock expiration
    TimeUnit timeUnit() default TimeUnit.SECONDS;
    String delimiter() default "&";       // separator for composite keys
}
@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RequestKeyParam {}

Lock key generation

The generator inspects method parameters and, if necessary, the fields of those parameters to build a composite key based on @RequestKeyParam annotations.

public static String getLockKey(ProceedingJoinPoint joinPoint) {
    MethodSignature ms = (MethodSignature) joinPoint.getSignature();
    Method method = ms.getMethod();
    RequestLock lockAnno = method.getAnnotation(RequestLock.class);
    Object[] args = joinPoint.getArgs();
    Parameter[] params = method.getParameters();
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < params.length; i++) {
        RequestKeyParam kp = params[i].getAnnotation(RequestKeyParam.class);
        if (kp != null) {
            sb.append(lockAnno.delimiter()).append(args[i]);
        }
    }
    if (sb.length() == 0) {
        for (int i = 0; i < args.length; i++) {
            Object obj = args[i];
            for (Field f : obj.getClass().getDeclaredFields()) {
                if (f.getAnnotation(RequestKeyParam.class) != null) {
                    f.setAccessible(true);
                    sb.append(lockAnno.delimiter())
                      .append(ReflectionUtils.getField(f, obj));
                }
            }
        }
    }
    return lockAnno.prefix() + sb;
}

Redis‑based aspect

Uses StringRedisTemplate.execute with SET_IF_ABSENT to create the lock. If the lock cannot be set, a BizException with a “too fast” message is thrown.

@Around("execution(public * *(..)) && @annotation(com.summo.demo.config.requestlock.RequestLock)")
public Object interceptor(ProceedingJoinPoint jp) {
    Method method = ((MethodSignature) jp.getSignature()).getMethod();
    RequestLock rl = method.getAnnotation(RequestLock.class);
    if (StringUtils.isEmpty(rl.prefix())) {
        throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "重复提交前缀不能为空");
    }
    String lockKey = RequestKeyGenerator.getLockKey(jp);
    Boolean success = stringRedisTemplate.execute(conn ->
        conn.set(lockKey.getBytes(), new byte[0],
            Expiration.from(rl.expire(), rl.timeUnit()),
            RedisStringCommands.SetOption.SET_IF_ABSENT));
    if (!success) {
        throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "您的操作太快了,请稍后重试");
    }
    try {
        return jp.proceed();
    } catch (Throwable t) {
        throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "系统异常");
    }
}

Redisson‑based aspect

Acquires a Redisson lock, tries to obtain it with tryLock(), sets the expiration, proceeds with the method, and finally releases the lock.

@Around("execution(public * *(..)) && @annotation(com.summo.demo.config.requestlock.RequestLock)")
public Object interceptor(ProceedingJoinPoint jp) {
    Method method = ((MethodSignature) jp.getSignature()).getMethod();
    RequestLock rl = method.getAnnotation(RequestLock.class);
    String lockKey = RequestKeyGenerator.getLockKey(jp);
    RLock lock = redissonClient.getLock(lockKey);
    boolean acquired = lock.tryLock();
    if (!acquired) {
        throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "您的操作太快了,请稍后重试");
    }
    lock.lock(rl.expire(), rl.timeUnit());
    try {
        return jp.proceed();
    } catch (Throwable t) {
        throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "系统异常");
    } finally {
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

Testing results

First submission returns "添加用户成功" (user added).

first submission
first submission

Rapid repeat within the lock period returns error code BIZ-0001: "您的操作太快了,请稍后重试".

duplicate attempt
duplicate attempt

After the lock expires (a few seconds) the request succeeds again.

post‑expiry submission
post‑expiry submission

Observations and recommendations

The debounce works while the Redis entry or Redisson lock exists, but once the lock expires the same request can be processed again. True idempotency therefore requires additional safeguards such as database unique constraints (e.g., a unique index on userPhone) and inclusion of user‑specific fields (userId, IP) in the lock key to avoid false positives for multi‑user scenarios.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Javaredisdistributed-lockIdempotencyspring-aopredissonAPI Debounce
Java Tech Enthusiast
Written by

Java Tech Enthusiast

Sharing computer programming language knowledge, focusing on Java fundamentals, data structures, related tools, Spring Cloud, IntelliJ IDEA... Book giveaways, red‑packet rewards and other perks await!

0 followers
Reader feedback

How this landed with the community

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.