非Spring Boot/Cloud环境下Spring MVC应用REST API限流实现问询
传统Spring(非Boot/Cloud)+ Tomcat应用的REST API限流实现方案
一、本地限流(单Tomcat实例场景)
适合单实例部署的应用,用本地缓存实现限流逻辑,无需额外中间件。
方案1:Spring HandlerInterceptor + Guava RateLimiter
利用Spring MVC的拦截器机制,结合Guava的令牌桶实现精细化接口限流。
- 引入Guava依赖(Maven)
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>32.1.3-jre</version> <!-- 选择适配项目的版本 --> </dependency>
- 定义限流注解
用注解标记需要限流的接口,灵活配置限流规则:
import java.lang.annotation.*; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RateLimit { // 每秒允许的请求次数 double permitsPerSecond(); }
- 实现限流拦截器
import com.google.common.util.concurrent.RateLimiter; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class RateLimitInterceptor implements HandlerInterceptor { // 存储不同接口的限流器,key为方法全限定名 private final Map<String, RateLimiter> rateLimiterMap = new ConcurrentHashMap<>(); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException { if (!(handler instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod = (HandlerMethod) handler; RateLimit rateLimit = handlerMethod.getMethodAnnotation(RateLimit.class); if (rateLimit == null) { // 无注解的接口跳过限流 return true; } String methodKey = handlerMethod.getBeanType().getName() + "." + handlerMethod.getMethod().getName(); RateLimiter rateLimiter = rateLimiterMap.computeIfAbsent(methodKey, k -> RateLimiter.create(rateLimit.permitsPerSecond())); if (!rateLimiter.tryAcquire()) { // 触发限流,返回429状态码 response.setStatus(HttpServletResponse.SC_TOO_MANY_REQUESTS); response.getWriter().write("Too many requests, please try again later."); return false; } return true; } }
- 注册拦截器
如果用XML配置(spring-servlet.xml):
<mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/api/**"/> <!-- 仅对API路径生效 --> <bean class="com.yourpackage.RateLimitInterceptor"/> </mvc:interceptor> </mvc:interceptors>
如果用Java配置:
import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new RateLimitInterceptor()) .addPathPatterns("/api/**"); } }
- 在接口上使用注解
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/user") public class UserController { @RateLimit(permitsPerSecond = 5) // 每秒允许5次请求 @GetMapping("/info") public String getUserInfo() { return "User info"; } }
方案2:Servlet Filter实现限流
更底层的Servlet级限流,适合不需要Spring拦截器的场景:
import com.google.common.util.concurrent.RateLimiter; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @WebFilter(filterName = "rateLimitFilter", urlPatterns = "/api/**") public class RateLimitFilter implements Filter { private final Map<String, RateLimiter> rateLimiterMap = new ConcurrentHashMap<>(); @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletResponse httpResponse = (HttpServletResponse) response; String requestURI = ((javax.servlet.http.HttpServletRequest) request).getRequestURI(); // 按请求URI分组限流,每秒10次 RateLimiter rateLimiter = rateLimiterMap.computeIfAbsent(requestURI, k -> RateLimiter.create(10)); if (!rateLimiter.tryAcquire()) { httpResponse.setStatus(HttpServletResponse.SC_TOO_MANY_REQUESTS); httpResponse.getWriter().write("Too many requests"); return; } chain.doFilter(request, response); } @Override public void init(FilterConfig filterConfig) throws ServletException {} @Override public void destroy() {} }
如果不用@WebFilter,可在web.xml中配置:
<filter> <filter-name>rateLimitFilter</filter-name> <filter-class>com.yourpackage.RateLimitFilter</filter-class> </filter> <filter-mapping> <filter-name>rateLimitFilter</filter-name> <url-pattern>/api/**</url-pattern> </filter-mapping>
二、分布式限流(多Tomcat实例场景)
多实例部署时,本地限流无法全局生效,需用Redis+Lua脚本实现分布式原子限流。
- 引入Jedis依赖(Maven)
<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>4.4.6</version> </dependency>
- 实现Redis限流工具类
用Lua脚本保证计数和过期时间的原子性:
import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import java.util.Collections; public class RedisRateLimiter { private static final String LIMIT_KEY_PREFIX = "rate_limit:"; private static final String REJECTED = "0"; private static final String ALLOWED = "1"; private final JedisPool jedisPool; public RedisRateLimiter(JedisPool jedisPool) { this.jedisPool = jedisPool; } // key:限流标识(如用户ID+接口路径);maxRequests:周期内最大请求数;periodSeconds:周期(秒) public boolean tryAcquire(String key, int maxRequests, int periodSeconds) { try (Jedis jedis = jedisPool.getResource()) { String limitKey = LIMIT_KEY_PREFIX + key; String script = """ local current = redis.call('incr', KEYS[1]) if current == 1 then redis.call('expire', KEYS[1], ARGV[1]) end if current > tonumber(ARGV[2]) then return %s end return %s """.formatted(REJECTED, ALLOWED); String result = jedis.eval(script, Collections.singletonList(limitKey), Collections.singletonList(String.valueOf(periodSeconds)), Collections.singletonList(String.valueOf(maxRequests))); return ALLOWED.equals(result); } } }
- 实现分布式限流拦截器
import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class DistributedRateLimitInterceptor implements HandlerInterceptor { private final RedisRateLimiter redisRateLimiter; public DistributedRateLimitInterceptor(RedisRateLimiter redisRateLimiter) { this.redisRateLimiter = redisRateLimiter; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException { if (!(handler instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod = (HandlerMethod) handler; RateLimit rateLimit = handlerMethod.getMethodAnnotation(RateLimit.class); if (rateLimit == null) { return true; } // 用用户ID+方法签名作为限流key,避免不同用户互相影响 String userId = request.getHeader("X-User-Id"); String methodKey = handlerMethod.getBeanType().getName() + "." + handlerMethod.getMethod().getName(); String limitKey = userId != null ? userId + ":" + methodKey : methodKey; boolean allowed = redisRateLimiter.tryAcquire(limitKey, (int) rateLimit.permitsPerSecond(), 1); if (!allowed) { response.setStatus(HttpServletResponse.SC_TOO_MANY_REQUESTS); response.getWriter().write("Too many requests, please try again later."); return false; } return true; } }
- 注册Redis连接池和拦截器
XML配置示例:
<bean id="jedisPool" class="redis.clients.jedis.JedisPool"> <constructor-arg index="0" value="your-redis-host"/> <constructor-arg index="1" value="6379"/> </bean> <bean id="redisRateLimiter" class="com.yourpackage.RedisRateLimiter"> <constructor-arg ref="jedisPool"/> </bean> <mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/api/**"/> <bean class="com.yourpackage.DistributedRateLimitInterceptor"> <constructor-arg ref="redisRateLimiter"/> </bean> </mvc:interceptor> </mvc:interceptors>
内容的提问来源于stack exchange,提问作者shree
相关产品推荐
相关产品推荐

