本文目录导读:

- 方案一:基于
ConcurrentHashMap+ 滑动窗口(适合单体应用,简单无依赖) - 方案二:使用Guava的
RateLimiter(基于令牌桶算法,适合接口级限流) - 方案三:基于Redis + Lua脚本(适合分布式系统,生产环境推荐)
- 建议
- 扩展思考
在Java中实现IP限流,常见的方法有基于过滤器(Filter)、基于拦截器(Interceptor)或基于网关(Gateway),核心原理是维护一个IP到访问次数的映射,并结合时间窗口算法。
以下是几种主流实现方案,从简单到复杂,你可以根据项目需求选择。
基于ConcurrentHashMap + 滑动窗口(适合单体应用,简单无依赖)
这是最基本的实现,使用ConcurrentHashMap存储每个IP的访问记录,通过时间窗口判断是否超限。
核心思想:记录每个IP在固定时间窗口(如1秒)内的访问次数,如果超过阈值,则拒绝请求。
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
public class IpRateLimitFilter implements Filter {
// key: IP, value: 该IP的请求时间戳队列
private static final ConcurrentHashMap<String, ConcurrentLinkedQueue<Long>> IP_MAP = new ConcurrentHashMap<>();
private static final int MAX_COUNT = 10; // 最大请求数
private static final int TIME_WINDOW = 1000; // 时间窗口,毫秒
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String ip = getClientIp(request);
long currentTime = System.currentTimeMillis();
// 1. 获取或创建该IP的请求时间队列
ConcurrentLinkedQueue<Long> queue = IP_MAP.get(ip);
if (queue == null) {
queue = new ConcurrentLinkedQueue<>();
IP_MAP.put(ip, queue);
}
// 2. 清理掉超出时间窗口的旧记录
while (!queue.isEmpty() && currentTime - queue.peek() > TIME_WINDOW) {
queue.poll();
}
// 3. 检查当前窗口内的请求数是否超限
if (queue.size() >= MAX_COUNT) {
response.setStatus(429); // Too Many Requests
response.getWriter().write("Too many requests, please try later.");
return;
}
// 4. 记录当前请求时间
queue.offer(currentTime);
// 5. 放行
filterChain.doFilter(request, response);
}
// 获取客户端真实IP
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// 多个代理的情况,取第一个IP
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip;
}
}
配置web.xml(Spring Boot项目则用@WebFilter注解):
<filter>
<filter-name>ipRateLimitFilter</filter-name>
<filter-class>com.example.filter.IpRateLimitFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>ipRateLimitFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
优点:代码简单,完全自控。
缺点:内存占用随请求IP增加而增长,进程重启数据丢失,集群环境下需要外部存储(如Redis)。
使用Guava的RateLimiter(基于令牌桶算法,适合接口级限流)
Google Guava提供了令牌桶实现的RateLimiter,可以平滑限制流量。RateLimiter本身不区分IP,需要配合IP隔离。
核心思想:为每个IP创建一个独立的RateLimiter,控制该IP的访问速率。
import com.google.common.util.concurrent.RateLimiter;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
public class IpRateLimiterFilter implements Filter {
// key: IP, value: 该IP对应的RateLimiter
private static final ConcurrentHashMap<String, RateLimiter> IP_LIMITER_MAP = new ConcurrentHashMap<>();
private static final double PERMITS_PER_SECOND = 5.0; // 每秒5个请求
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String ip = getClientIp(request);
// 1. 获取或创建该IP的RateLimiter
RateLimiter limiter = IP_LIMITER_MAP.get(ip);
if (limiter == null) {
limiter = RateLimiter.create(PERMITS_PER_SECOND);
IP_LIMITER_MAP.put(ip, limiter);
}
// 2. 尝试获取令牌(非阻塞、立即返回)
if (!limiter.tryAcquire()) {
response.setStatus(429);
response.getWriter().write("Too many requests, please slow down.");
return;
}
chain.doFilter(request, response);
}
private String getClientIp(HttpServletRequest request) {
// 与方案一相同,获取客户端真实IP
// ...
}
}
优点:令牌桶算法非常平滑,支持突发流量。
缺点:同样是单机内存方案,不适用集群。
基于Redis + Lua脚本(适合分布式系统,生产环境推荐)
这是实践中最主流、最可靠的方式,利用Redis的单线程特性和Lua脚本的原子性,实现分布式环境下的精确限流。
核心思想:在Lua脚本中实现滑动窗口算法或令牌桶算法,以IP为key,在Redis中记录请求次数。
定义Lua脚本(滑动窗口)
-- 参数:KEYS[1] = 限流key (IP)
-- 参数:ARGV[1] = 当前时间戳(毫秒)
-- 参数:ARGV[2] = 时间窗口大小(毫秒)
-- 参数:ARGV[3] = 窗口内最大请求数
local key = KEYS[1]
local current_time = tonumber(ARGV[1])
local window_size = tonumber(ARGV[2])
local max_count = tonumber(ARGV[3])
-- 1. 移除时间窗口之前的元素
redis.call('ZREMRANGEBYSCORE', key, 0, current_time - window_size)
-- 2. 获取当前窗口内的请求数
local current_count = redis.call('ZCARD', key)
-- 3. 判断是否超过最大限制
if current_count >= max_count then
return 1 -- 限流
end
-- 4. 添加当前请求到有序集合(score为时间戳,member可以用时间戳+随机数确保唯一)
redis.call('ZADD', key, current_time, current_time .. '_' .. math.random())
redis.call('EXPIRE', key, window_size / 1000 + 1)
return 0 -- 允许访问
注意:这里用了有序集合(Sorted Set)来精确存储每个请求的时间戳,更适合需要严格控制窗口的场景,也可以用INCR + EXPIRE(固定窗口),但会有“毛刺”问题。
Java代码调用Redis
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
public class RedisIpRateLimiter {
private JedisPool jedisPool;
private String luaScriptSHA; // 脚本SHA缓存
// 脚本内容(上面那段Lua)
private static final String LUA_SCRIPT =
"local key = KEYS[1]\n" +
"local current_time = tonumber(ARGV[1])\n" +
"local window_size = tonumber(ARGV[2])\n" +
"local max_count = tonumber(ARGV[3])\n" +
"redis.call('ZREMRANGEBYSCORE', key, 0, current_time - window_size)\n" +
"local current_count = redis.call('ZCARD', key)\n" +
"if current_count >= max_count then\n" +
" return 1\n" +
"end\n" +
"redis.call('ZADD', key, current_time, current_time .. '_' .. math.random())\n" +
"redis.call('EXPIRE', key, window_size / 1000 + 1)\n" +
"return 0\n";
public boolean isRateLimited(String ip) {
try (Jedis jedis = jedisPool.getResource()) {
// 加载脚本(生产环境建议加载一次,缓存SHA)
if (luaScriptSHA == null) {
luaScriptSHA = jedis.scriptLoad(LUA_SCRIPT);
}
long currentTime = System.currentTimeMillis();
long windowSize = 1000; // 1秒
long maxCount = 10; // 10次
// 执行Lua脚本
Object result = jedis.evalsha(luaScriptSHA, 1,
"rate_limit:" + ip, // KEYS[1]
String.valueOf(currentTime), // ARGV[1]
String.valueOf(windowSize), // ARGV[2]
String.valueOf(maxCount) // ARGV[3]
);
// 返回1表示限流,0表示允许
return "1".equals(String.valueOf(result));
}
}
}
在Spring Boot中的使用:
@Component
public class IpRateLimitInterceptor implements HandlerInterceptor {
@Autowired
private RedisIpRateLimiter rateLimiter;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String ip = getClientIp(request);
if (rateLimiter.isRateLimited(ip)) {
response.setStatus(429);
response.getWriter().write("Too Many Requests");
return false;
}
return true;
}
// 注册拦截器
@Configuration
public static class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new IpRateLimitInterceptor())
.addPathPatterns("/api/**"); // 对哪些路径生效
}
}
}
优点:分布式、高并发、精确、内存可控。
缺点:引入外部依赖(Redis),运维成本增加。
| 方案 | 适用场景 | 复杂度 | 支持分布式 | 算法 |
|---|---|---|---|---|
| ConcurrentHashMap + 滑动窗口 | 单体应用、学习测试 | 否 | 滑动窗口 | |
| Guava RateLimiter | 单体应用、需要平滑限流 | 否 | 令牌桶 | |
| Redis + Lua | 生产环境、分布式系统 | 是 | 滑动窗口/令牌桶 |
建议
- 学习/小项目:用方案一(滑动窗口)或方案二(Guava)。
- 生产环境(单机):方案二(Guava)+ 定义合理的限流倍数。
- 生产环境(集群)/高并发:方案三(Redis+Lua),这是目前行业标准的做法,还可以配合Nginx的
limit_req_zone模块做第一层防护,Java层做精细控制。
扩展思考
- 动态配置:限流阈值最好从配置文件或配置中心(Nacos/Apollo)拉取,方便动态调整,而不用改代码重启。
- 白名单:对于内部IP或VIP用户,应该跳过限流。
- 返回值:被限流时,除了返回429状态码,建议在响应头加上
Retry-After指示客户端多久后重试,以及X-RateLimit-Limit、X-RateLimit-Remaining等信息,有助于客户端适配。