Backend Development 15 min read

Implementing Interface Debounce with Redis and Redisson Distributed Locks in Java

This article explains how to prevent duplicate submissions in Java backend services by implementing request debouncing using Redis caching and Redisson distributed locks, detailing the design of request lock annotations, unique key generation, and practical code examples for robust idempotent APIs.

Java Architect Essentials
Java Architect Essentials
Java Architect Essentials
Implementing Interface Debounce with Redis and Redisson Distributed Locks in Java

Preface

As an experienced backend Java developer, the author shares practical techniques for preventing duplicate submissions (debounce) in web APIs, emphasizing the need for both frontend safeguards and backend logic.

What Is Debounce?

Debounce refers to preventing user‑hand shaking and network jitter. Frontend can show loading states, but backend must also enforce debounce to avoid processing the same request multiple times.

Logic must be correct, without false positives.

Response should be fast.

Implementation should be easy to integrate and decoupled from business logic.

Provide clear user feedback, e.g., "You clicked too fast".

Idea Analysis

Not all interfaces need debounce. Typical candidates are:

User input interfaces (search, form fields) where requests can be delayed until input stabilizes.

Button click interfaces (submit, save) where rapid repeated clicks should be throttled.

Scroll‑load interfaces (infinite scroll, pull‑to‑refresh) where frequent triggers need consolidation.

To determine duplication, compare timestamps, key parameters, and optionally request URLs.

Debounce in Distributed Deployment

Two common approaches:

Shared Cache (Redis)

Use a Redis key with an expiration time to represent a lock. If the key already exists, reject the request.

Distributed Lock (Redisson)

Leverage Redisson's RLock to acquire a lock with a timeout; if acquisition fails, the request is considered duplicate.

Concrete Implementation

Example controller method:

@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;
}

Request Lock Annotation

Define a simple annotation to mark methods that require debounce:

import java.lang.annotation.*;
@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RequestLock {
    String prefix() default "reqLock"; // Redis key prefix
    long expire() default 5; // lock expiration
    TimeUnit timeUnit() default TimeUnit.SECONDS;
    String delimiter() default "&"; // key separator
}

Unique Key Generation

Mark parameters that should compose the lock key:

import java.lang.annotation.*;
@Target({ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestKeyParam {}

Utility to build the key from annotated parameters or fields:

public class RequestKeyGenerator {
    public static String getLockKey(ProceedingJoinPoint joinPoint) {
        MethodSignature ms = (MethodSignature) joinPoint.getSignature();
        Method method = ms.getMethod();
        RequestLock lock = 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(lock.delimiter()).append(args[i]);
            }
        }
        if (sb.length() == 0) {
            Annotation[][] paramAnnos = method.getParameterAnnotations();
            for (int i = 0; i < paramAnnos.length; i++) {
                for (Field f : args[i].getClass().getDeclaredFields()) {
                    if (f.getAnnotation(RequestKeyParam.class) != null) {
                        f.setAccessible(true);
                        sb.append(lock.delimiter()).append(ReflectionUtils.getField(f, args[i]));
                    }
                }
            }
        }
        return lock.prefix() + sb.toString();
    }
}

Duplicate Submission Detection

Redis Cache Method

Aspect that uses StringRedisTemplate to set a key with SET_IF_ABSENT :

@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 lock = method.getAnnotation(RequestLock.class);
        String lockKey = RequestKeyGenerator.getLockKey(joinPoint);
        Boolean success = stringRedisTemplate.execute((RedisCallback
) conn ->
            conn.set(lockKey.getBytes(), new byte[0],
                Expiration.from(lock.expire(), lock.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 Method

Aspect that acquires a Redisson lock:

@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 lock = method.getAnnotation(RequestLock.class);
        String lockKey = RequestKeyGenerator.getLockKey(joinPoint);
        RLock rLock = redissonClient.getLock(lockKey);
        boolean acquired = false;
        try {
            acquired = rLock.tryLock();
            if (!acquired) {
                throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "您的操作太快了,请稍后重试");
            }
            rLock.lock(lock.expire(), lock.timeUnit());
            return joinPoint.proceed();
        } catch (Throwable t) {
            throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "系统异常");
        } finally {
            if (acquired && rLock.isHeldByCurrentThread()) {
                rLock.unlock();
            }
        }
    }
}

Testing Results

First submission succeeds, rapid repeated submissions are rejected with an error message, and after the lock expires the request succeeds again, demonstrating effective debounce.

For true idempotency, combine this lock mechanism with database unique constraints and additional business checks.

backendJavaRedisSpring BootDistributed LockDebounce
Java Architect Essentials
Written by

Java Architect Essentials

Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow together.

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.