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.
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
end5. 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=63794. 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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
