为啥需要限制接口请求频率?这个是因为防止接口一直被刷,比如发送手机验证码的接口,一直被刷的话,费钱费资源的,至少做点基本的防护工作。以下分别使用Redis和Nginx实现限制接口请求频率方案。
(1)用法:ZADD key score_1 value_1 score_2 value_2 ...
(2)作用:将一个或多个成员元素及其分数值加入到有序集当中。某个成员已经是有序集的成员,那么更新这个成员的分数值,并通过重新插入这个成员元素,来保证该成员在正确的位置上。分数值可以是整数值或双精度浮点数。
(3)返回值:被成功添加的新成员的数量,不包括那些被更新的、已经存在的成员。
(4)示例
redis > ZADD runoobkey 1 redis
(integer) 1
redis > ZADD runoobkey 2 mongodb
(integer) 1
redis > ZADD runoobkey 3 mysql
(integer) 1
redis > ZADD runoobkey 3 mysql
(integer) 0
redis > ZADD runoobkey 4 mysql
(integer) 0
redis > ZRANGE runoobkey 0 10 WITHSCORES
1) "redis"
2) "1"
3) "mongodb"
4) "2"
5) "mysql"
6) "4"
(1)用法:ZREM key value_1 value_2 ...
(2)作用:移除有序集中的一个或多个成员,不存在的成员将被忽略。
(3)返回值:被成功添加的新成员的数量,不包括那些被更新的、已经存在的成员。
(4)示例
redis > ZRANGE runoobkey 0 10 WITHSCORES
1) "redis"
2) "1"
3) "mongodb"
4) "2"
5) "mysql"
6) "4"
redis > ZREM mongodb
(integer) 1
redis > ZRANGE runoobkey 0 10 WITHSCORES
1) "redis"
2) "1"
3) "mysql"
4) "4"
(1)用法:ZCARD key
(2)作用:获取有序集合中成员的数量。
(3)返回值:当key存在且是有序集类型时,返回有序集的基数。 当key不存在时,返回0 。
(4)示例
redis > ZADD myzset 1 "one"
(integer) 1
redis > ZADD myzset 2 "two"
(integer) 1
redis > ZCARD myzset
(integer) 2
(1)用法:ZREMRANGEBYSCORE key min max
(2)作用:移除有序集中,指定分数区间内的所有成员。
(3)返回值:被移除成员的数量。
(4)示例
redis > ZRANGE salary 0 -1 WITHSCORES
1) "tom"
2) "2000"
3) "peter"
4) "3500"
5) "jack"
6) "5000"
redis > ZREMRANGEBYSCORE salary 1500 3500
(integer) 2
redis> ZRANGE salary 0 -1 WITHSCORES
1) "jack"
2) "5000"
(1)新建一个过滤器,如【/src/main/java/org/example/interceptor/RateLimiterInterceptor.java】
package org.example.interceptor;
import cn.hutool.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;
/**
* 限流拦截器
*/
@Component
public class RateLimiterInterceptor implements HandlerInterceptor {
private static final String RATE_LIMITER_PREFIX = "Rate-Limiter:";
private static final int LIMIT = 10; // 限流阈值
private static final int TIME_WINDOW = 60; // 时间窗口,单位为秒
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
response.setCharacterEncoding("utf-8");
response.setContentType("application/json");
String key = RATE_LIMITER_PREFIX + request.getRequestURI() + ":" + request.getRemoteAddr(); // Rate-Limiter:/api/sendCode:127.0.0.1
long currentTime = System.currentTimeMillis(); // 1703036748554
long beforeTime = currentTime - TIME_WINDOW * 1000; // 1703036748554 - 60000 = 1703036688554
// Long removeNum = stringRedisTemplate.opsForZSet().removeRangeByScore(K key, double min, double max); // 删除有序集合中分数在指定范围内的元素,返回删除元素的数量
stringRedisTemplate.opsForZSet().removeRangeByScore(key, 0, beforeTime); // 删除有序集合中60秒之前存进去的所有数据,如:
// Long memberNum = stringRedisTemplate.opsForZSet().size(K key); // 获取有序集合中元素的数量
long count = stringRedisTemplate.opsForZSet().size(key); // 3
if (count >= LIMIT) {
HashMap responseObj = new HashMap<>();
responseObj.put("code", HttpStatus.TOO_MANY_REQUESTS.value());
responseObj.put("success", false);
responseObj.put("msg", HttpStatus.TOO_MANY_REQUESTS.getReasonPhrase());
JSONObject json = new JSONObject(responseObj);
response.getWriter().println(json);
return false;
} else {
// Boolean addFlag = stringRedisTemplate.opsForZSet().add(K var1, V var2, double var3); // 向有序集合中添加一个或多个元素,并指定其分数
stringRedisTemplate.opsForZSet().add(key, String.valueOf(currentTime), currentTime);
// Boolean expireFlag = stringRedisTemplate.expire(K key, long timeout, TimeUnit unit); // 对指定key的数据设置过期时间
stringRedisTemplate.expire(key, TIME_WINDOW, TimeUnit.SECONDS);
return true;
}
}
}
(2)在SpringMVC配置类中注入此过滤器,如【/src/main/java/org/example/config/ResourceConfig.java】
package org.example.config;
import org.example.interceptor.RateLimiterInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
@Configuration
public class ResourceConfig extends WebMvcConfigurationSupport {
@Autowired
private RateLimiterInterceptor rateLimiterInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(rateLimiterInterceptor).addPathPatterns("/abcd/api/sendCode");
}
}
// ~
user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
include /etc/nginx/conf.d/*.conf;
# 负载均衡
upstream springboot {
server xxx.xxx.xxx.xxx:8080;
}
# limit_req_zone $binary_remote_addr zone=limit_zone:10m rate=10r/s; # 定义了一个名为limit_zone的限流区域,使用IP地址进行限流,该区域的大小为10MB,限流速率为10个请求每秒。
limit_req_zone $binary_remote_addr zone=limit_zone:10m rate=10r/m; # 定义了一个名为limit_zone的限流区域,使用IP地址进行限流,该区域的大小为10MB,限流速率为10个请求每分钟。
# 80端口的服务
server {
listen 80;
server_name xxx.xxx.xxx.xxx;
location / {
alias html;
index index.html index.htm;
try_files $uri $uri/ /love/index.html;
proxy_pass http://localhost;
}
location ^~ /love/ {
root html/love;
index index.html index.htm;
proxy_pass http://localhost;
}
location ^~ /abcd/api/sendCode {
# 在/xxx/api/sendCode接口的location中使用limit_req指令进行限流,限流区域为limit_zone,同时设置了一个瞬时突发流量为20个请求的阈值。
# 这样,当同一个IP地址在一秒钟内发送超过10个请求到此接口时,Nginx会返回503错误码,表示请求被限流了。
# limit_req zone=limit_zone burst=20;
# 在/xxx/api/sendCode接口的location中使用limit_req指令进行限流,限流区域为limit_zone,同时设置了一个瞬时突发流量为5个请求的阈值。
# 这样,当同一个IP地址在一秒钟内发送超过10个请求到此接口时,如果在一分钟内有超过10个请求,则允许其中的5个请求通过,nodelay表示不延迟响应,即立即返回503错误码。
limit_req zone=limit_zone burst=5 nodelay;
proxy_pass http://springboot;
}
}
}
// ~