Java案例怎么实现IP限流?

wen java案例 22

本文目录导读:

Java案例怎么实现IP限流?

  1. 方案一:基于ConcurrentHashMap + 滑动窗口(适合单体应用,简单无依赖)
  2. 方案二:使用Guava的RateLimiter(基于令牌桶算法,适合接口级限流)
  3. 方案三:基于Redis + Lua脚本(适合分布式系统,生产环境推荐)
  4. 建议
  5. 扩展思考

在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 生产环境、分布式系统 滑动窗口/令牌桶

建议

  1. 学习/小项目:用方案一(滑动窗口)或方案二(Guava)。
  2. 生产环境(单机):方案二(Guava)+ 定义合理的限流倍数。
  3. 生产环境(集群)/高并发方案三(Redis+Lua),这是目前行业标准的做法,还可以配合Nginx的limit_req_zone模块做第一层防护,Java层做精细控制。

扩展思考

  • 动态配置:限流阈值最好从配置文件或配置中心(Nacos/Apollo)拉取,方便动态调整,而不用改代码重启。
  • 白名单:对于内部IP或VIP用户,应该跳过限流。
  • 返回值:被限流时,除了返回429状态码,建议在响应头加上Retry-After指示客户端多久后重试,以及X-RateLimit-LimitX-RateLimit-Remaining等信息,有助于客户端适配。

抱歉,评论功能暂时关闭!