Implementing Rate Limiting in Java with Guava, Custom Annotations, and Redis Lua Scripts
This article explains how to protect high‑concurrency Java applications using rate‑limiting techniques, covering basic algorithms such as counter, leaky‑bucket and token‑bucket, demonstrating a single‑node implementation with Guava’s RateLimiter and custom annotations, and showing a distributed solution based on Redis and Lua scripts.
In high‑concurrency systems, three tools—cache, degradation, and rate limiting—are used to protect stability; rate limiting restricts request flow to avoid overload.
Common Rate‑Limiting Algorithms
Counter Limiting
The simplest method limits total concurrent accesses, such as database connection pools or thread pools, often implemented with an AtomicInteger to count active requests.
Leaky Bucket
Requests are treated as water entering a bucket that drains at a fixed rate; excess water overflows, representing rejected requests.
Token Bucket
A token bucket supplies tokens at a constant rate; a request must acquire a token before processing, otherwise it is rejected. Controlling bucket capacity and token rate enforces the limit.
Single‑Machine Mode
Google Guava provides RateLimiter , a token‑bucket implementation that is easy and efficient.
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.1-jre</version>
</dependency> package com.example.demo.common.annotation;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface Limit {
String key() default "";
double permitsPerSecond();
long timeout();
TimeUnit timeunit() default TimeUnit.MILLISECONDS;
String msg() default "系统繁忙,请稍后再试";
} package com.example.demo.common.aspect;
import com.example.demo.common.annotation.Limit;
import com.example.demo.common.dto.R;
import com.example.demo.common.exception.LimitException;
import com.google.common.collect.Maps;
import com.google.common.util.concurrent.RateLimiter;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Map;
@Slf4j
@Aspect
@Component
public class LimitAspect {
private final Map
limitMap = Maps.newConcurrentMap();
@Around("@annotation(com.example.demo.common.annotation.Limit)")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
Limit limit = method.getAnnotation(Limit.class);
if (limit != null) {
String key = limit.key();
RateLimiter rateLimiter;
if (!limitMap.containsKey(key)) {
rateLimiter = RateLimiter.create(limit.permitsPerSecond());
limitMap.put(key, rateLimiter);
log.info("Created token bucket {} with capacity {}", key, limit.permitsPerSecond());
}
rateLimiter = limitMap.get(key);
boolean acquire = rateLimiter.tryAcquire(limit.timeout(), limit.timeunit());
if (!acquire) {
log.debug("Token bucket {} acquire failed", key);
throw new LimitException(limit.msg());
}
}
return pjp.proceed();
}
}Usage example:
package com.example.demo.module.test;
import com.example.demo.common.annotation.Limit;
import com.example.demo.common.dto.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@RestController
public class TestController {
@Limit(key = "cachingTest", permitsPerSecond = 1, timeout = 500, msg = "当前排队人数较多,请稍后再试!")
@GetMapping("cachingTest")
public R cachingTest() {
log.info("------读取本地------");
List
list = new ArrayList<>();
list.add("蜡笔小新");
list.add("哆啦A梦");
list.add("四驱兄弟");
return R.ok(list);
}
}Distributed Mode
Redis combined with Lua scripts provides an atomic, high‑performance distributed rate‑limiting solution, supporting both fixed‑window and token‑bucket algorithms.
package com.example.demo.common.annotation;
import com.example.demo.common.enums.LimitType;
import java.lang.annotation.*;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface RedisLimit {
String name() default "";
String key() default "";
String prefix() default "";
int period();
int count();
LimitType limitType() default LimitType.CUSTOMER;
String msg() default "系统繁忙,请稍后再试";
} package com.example.demo.common.aspect;
import com.example.demo.common.annotation.RedisLimit;
import com.example.demo.common.enums.LimitType;
import com.example.demo.common.exception.LimitException;
import com.google.common.collect.ImmutableList;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.DefaultRedisScript;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Objects;
@Slf4j
@Aspect
@Configuration
public class RedisLimitAspect {
private final RedisTemplate
redisTemplate;
public RedisLimitAspect(RedisTemplate
redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Around("@annotation(com.example.demo.common.annotation.RedisLimit)")
public Object around(ProceedingJoinPoint pjp) {
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
Method method = methodSignature.getMethod();
RedisLimit annotation = method.getAnnotation(RedisLimit.class);
LimitType limitType = annotation.limitType();
String key;
switch (limitType) {
case IP:
key = getIpAddress();
break;
case CUSTOMER:
key = annotation.key();
break;
default:
key = StringUtils.upperCase(method.getName());
}
ImmutableList
keys = ImmutableList.of(StringUtils.join(annotation.prefix(), key));
try {
String luaScript = buildLuaScript();
DefaultRedisScript
redisScript = new DefaultRedisScript<>(luaScript, Number.class);
Number number = redisTemplate.execute(redisScript, keys, annotation.count(), annotation.period());
log.info("Access try count is {} for name = {} and key = {}", number, annotation.name(), key);
if (number != null && number.intValue() == 1) {
return pjp.proceed();
}
throw new LimitException(annotation.msg());
} catch (Throwable e) {
if (e instanceof LimitException) {
log.debug("Token bucket {} acquire failed", key);
throw new LimitException(e.getLocalizedMessage());
}
e.printStackTrace();
throw new RuntimeException("服务器异常");
}
}
public String buildLuaScript() {
return "redis.replicate_commands(); local listLen,time"
+ "\nlistLen = redis.call('LLEN', KEYS[1])"
+ "\nif listLen and tonumber(listLen) < tonumber(ARGV[1]) then"
+ "\nlocal a = redis.call('TIME');"
+ "\nredis.call('LPUSH', KEYS[1], a[1]*1000000+a[2])"
+ "\nelse"
+ "\ntime = redis.call('LINDEX', KEYS[1], -1)"
+ "\nlocal a = redis.call('TIME');"
+ "\nif a[1]*1000000+a[2] - time < tonumber(ARGV[2])*1000000 then"
+ "\nreturn 0;"
+ "\nelse"
+ "\nredis.call('LPUSH', KEYS[1], a[1]*1000000+a[2])"
+ "\nredis.call('LTRIM', KEYS[1], 0, tonumber(ARGV[1])-1)"
+ "\nend"
+ "\nend"
+ "\nreturn 1;";
}
public String getIpAddress() {
HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(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-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
} package com.example.demo.module.test;
import com.example.demo.common.annotation.RedisLimit;
import com.example.demo.common.dto.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@RestController
public class TestController {
@RedisLimit(key = "cachingTest", count = 2, period = 2, msg = "当前排队人数较多,请稍后再试!")
@GetMapping("cachingTest")
public R cachingTest() {
log.info("------读取本地------");
List
list = new ArrayList<>();
list.add("蜡笔小新");
list.add("哆啦A梦");
list.add("四驱兄弟");
return R.ok(list);
}
}After starting the application, repeatedly accessing /cachingTest demonstrates that the requests are throttled according to the configured limits.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
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.