Backend Development 17 min read

Implementing Request Debounce in Java Backend Using Redis and Redisson

This article explains how to implement request debouncing in Java backend services using Redis and Redisson, covering the concept, interface types needing debouncing, duplicate request detection, shared cache and distributed lock solutions, with complete code examples and testing results.

Top Architect
Top Architect
Top Architect
Implementing Request Debounce in Java Backend Using Redis and Redisson

Introduction

The author, a senior Java backend developer, shares experiences designing multi‑tenant systems and handling complex message centers, emphasizing that careful coding standards and years of practice have prevented production crashes.

What Is Debounce

Debounce prevents both user‑induced rapid clicks and network‑induced duplicate requests. In web systems, uncontrolled form submissions can create duplicate records, so front‑end loading states and back‑end debounce logic are both required.

Interface Types Requiring Debounce

User input interfaces such as search boxes and form fields.

Button click interfaces like form submission or settings save.

Scroll‑load interfaces such as pull‑to‑refresh or infinite scrolling.

Determining Duplicate Requests

Duplicate detection relies on a time window, comparison of key parameters, and optionally the request URL.

Distributed Deployment Solutions

Shared Cache

A Redis key is generated and stored with an expiration; the SET_IF_ABSENT option ensures only the first request succeeds.

Distributed Lock

Redisson provides a distributed lock that, when acquired, holds the request for a configured period, preventing subsequent identical requests.

Implementation Details

RequestKeyGenerator

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

// AddReq.java
package com.summo.demo.model.request;
import java.util.List;
import lombok.Data;
@Data
public class AddReq {
    private String userName;
    private String userPhone;
    private List&lt;Long&gt; roleIdList;
}
</code>

RedisRequestLockAspect

<code>import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.types.Expiration;

@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<Boolean>) 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, "系统异常");
        }
    }
}
</code>

Redisson Configuration

<code>@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);
    }
}
</code>

RedissonRequestLockAspect

<code>@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 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();
            }
        }
    }
}
</code>

Testing Results

First submission: "Add user success".

Rapid repeat: "BIZ-0001: 您的操作太快了,请稍后重试".

After a few seconds: "Add user success" again.

Conclusion

Debounce effectively prevents rapid duplicate submissions, but true idempotency also requires business‑level checks and database unique constraints. Selecting appropriate fields for the lock key (e.g., user ID, IP) further reduces false positives in production environments.

backendJavaRedisSpringDistributed LockDebounce
Top Architect
Written by

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.

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.