Mastering Rate Limiting in Spring Boot: Redis + Lua AOP Solution

This article explains what rate limiting is, why it’s essential for high‑availability systems, and walks through multiple limiting strategies—including counters, leaky‑bucket, token‑bucket, and a Redis‑Lua approach—culminating in a complete Spring Boot implementation with custom annotations, AOP interceptors, and gateway integration.

macrozheng
macrozheng
macrozheng
Mastering Rate Limiting in Spring Boot: Redis + Lua AOP Solution

What is Rate Limiting? Why Rate Limit?

Just like a subway line has limited capacity, a program can only handle a finite number of requests per second; exceeding this capacity leads to overload or crashes, so we deliberately delay some requests to protect system stability.

In high‑traffic scenarios such as flash‑sale events, systems evaluate peak traffic and reject a portion of requests once a threshold is reached, ensuring the service remains available.

Rate limiting is typically measured by QPS (queries per second) or TPS (transactions per second). For example, if the threshold is 1000 QPS, the 1001st request in that second will be throttled.

Rate Limiting Solutions

1. Counter

Java can use atomic counters such as AtomicInteger or semaphores to implement simple rate limiting.

// limit count
private int maxCount = 10;
private long interval = 60;
private AtomicInteger atomicInteger = new AtomicInteger(0);
private long startTime = System.currentTimeMillis();

public boolean limit(int maxCount, int interval) {
    atomicInteger.addAndGet(1);
    if (atomicInteger.get() == 1) {
        startTime = System.currentTimeMillis();
        atomicInteger.addAndGet(1);
        return true;
    }
    // reset after interval
    if (System.currentTimeMillis() - startTime > interval * 1000) {
        startTime = System.currentTimeMillis();
        atomicInteger.set(1);
        return true;
    }
    if (atomicInteger.get() > maxCount) {
        return false;
    }
    return true;
}

2. Leaky Bucket Algorithm

The leaky bucket treats incoming requests as water flowing into a bucket with limited capacity; water leaks out at a constant rate. When the inflow exceeds the outflow, excess requests are dropped.

3. Token Bucket Algorithm

Similar to a hospital registration system, a token bucket generates tokens at a fixed rate. A request can proceed only if it successfully acquires a token; otherwise it is rejected.

4. Redis + Lua

Lua scripts in Redis provide atomic execution of complex rate‑limiting logic. Compared with Redis transactions, Lua scripts reduce network overhead, guarantee atomicity, and can be reused across clients.

Reduce network overhead: a single script execution replaces multiple Redis commands.

Atomic operation: the whole script runs as one atomic command.

Reuse: once stored, the script can be invoked by any client.

Typical Lua script logic:

-- get key
local key = KEYS[1]
-- limit size
local limit = tonumber(ARGV[1])
-- current count
local current = tonumber(redis.call('get', key) or "0")
if current + 1 > limit then
    return 0
else
    redis.call('INCRBY', key, 1)
    redis.call('EXPIRE', key, 2)
    return 1
end

5. Gateway Layer Rate Limiting

Rate limiting is often applied at the gateway level using tools such as Nginx, OpenResty, Kong, Zuul, or Spring Cloud Gateway. Spring Cloud Gateway implements rate limiting via Redis + Lua scripts.

Redis + Lua Implementation in Spring Boot

Below is a step‑by‑step guide to building a rate‑limiting solution using Spring Boot, AOP, and Redis‑Lua.

1. Environment Preparation

Create a Spring Boot project via https://start.spring.io.

2. Add Dependencies

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    ...
</dependencies>

3. application.properties

spring.redis.host=127.0.0.1
spring.redis.port=6379

4. RedisTemplate Configuration

@Configuration
public class RedisLimiterHelper {
    @Bean
    public RedisTemplate<String, Serializable> limitRedisTemplate(LettuceConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Serializable> template = new RedisTemplate<>();
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }
}

5. Limit Type Enum

public enum LimitType {
    CUSTOMER, // custom key
    IP        // request IP
}

6. Custom Annotation

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Limit {
    String name() default "";
    String key() default "";
    String prefix() default "";
    int period(); // seconds
    int count();   // max requests in period
    LimitType limitType() default LimitType.CUSTOMER;
}

7. AOP Interceptor

@Aspect
@Configuration
public class LimitInterceptor {
    private static final Logger logger = LoggerFactory.getLogger(LimitInterceptor.class);
    private static final String UNKNOWN = "unknown";
    private final RedisTemplate<String, Serializable> limitRedisTemplate;

    @Autowired
    public LimitInterceptor(RedisTemplate<String, Serializable> limitRedisTemplate) {
        this.limitRedisTemplate = limitRedisTemplate;
    }

    @Around("execution(public * *(..)) && @annotation(com.xiaofu.limit.api.Limit)")
    public Object interceptor(ProceedingJoinPoint pjp) throws Throwable {
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();
        Limit limitAnnotation = method.getAnnotation(Limit.class);
        LimitType limitType = limitAnnotation.limitType();
        String name = limitAnnotation.name();
        String key;
        int limitPeriod = limitAnnotation.period();
        int limitCount = limitAnnotation.count();
        switch (limitType) {
            case IP:
                key = getIpAddress();
                break;
            case CUSTOMER:
                key = limitAnnotation.key();
                break;
            default:
                key = StringUtils.upperCase(method.getName());
        }
        ImmutableList<String> keys = ImmutableList.of(StringUtils.join(limitAnnotation.prefix(), key));
        try {
            String luaScript = buildLuaScript();
            RedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);
            Number count = limitRedisTemplate.execute(redisScript, keys, limitCount, limitPeriod);
            logger.info("Access try count is {} for name={} and key = {}", count, name, key);
            if (count != null && count.intValue() <= limitCount) {
                return pjp.proceed();
            } else {
                throw new RuntimeException("You have been dragged into the blacklist");
            }
        } catch (Throwable e) {
            if (e instanceof RuntimeException) {
                throw new RuntimeException(e.getLocalizedMessage());
            }
            throw new RuntimeException("server exception");
        }
    }

    public String buildLuaScript() {
        StringBuilder lua = new StringBuilder();
        lua.append("local c");
        lua.append("
c = redis.call('get',KEYS[1])");
        lua.append("
if c and tonumber(c) > tonumber(ARGV[1]) then
return c;
end");
        lua.append("
c = redis.call('incr',KEYS[1])");
        lua.append("
if tonumber(c) == 1 then
redis.call('expire',KEYS[1],ARGV[2])
end");
        lua.append("
return c;");
        return lua.toString();
    }

    public String getIpAddress() {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}

8. Controller with @Limit

@RestController
public class LimiterController {
    private static final AtomicInteger ATOMIC_INTEGER_1 = new AtomicInteger();
    private static final AtomicInteger ATOMIC_INTEGER_2 = new AtomicInteger();
    private static final AtomicInteger ATOMIC_INTEGER_3 = new AtomicInteger();

    @Limit(key = "limitTest", period = 10, count = 3)
    @GetMapping("/limitTest1")
    public int testLimiter1() {
        return ATOMIC_INTEGER_1.incrementAndGet();
    }

    @Limit(key = "customer_limit_test", period = 10, count = 3, limitType = LimitType.CUSTOMER)
    @GetMapping("/limitTest2")
    public int testLimiter2() {
        return ATOMIC_INTEGER_2.incrementAndGet();
    }

    @Limit(key = "ip_limit_test", period = 10, count = 3, limitType = LimitType.IP)
    @GetMapping("/limitTest3")
    public int testLimiter3() {
        return ATOMIC_INTEGER_3.incrementAndGet();
    }
}

9. Testing

Send four consecutive requests to http://127.0.0.1:8080/limitTest1. The first three succeed; the fourth is rejected, confirming the Spring Boot + AOP + Lua rate‑limiting works as expected.

Overall, this Spring Boot + AOP + Lua solution provides a simple yet powerful way to implement rate limiting, useful for interviews and real‑world scenarios. Choose the appropriate algorithm based on business requirements rather than using a technique just for its novelty.

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.

Lua
macrozheng
Written by

macrozheng

Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.

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.