一個基於Redis實現的接口限流方案,先說要實現的功能
- 可以限制指定的接口,在一定時間內,只能被請求N次,超過次數就返回異常信息
- 可以通過配置文件,或者管理后台,動態的修改限流配置
實現的思路
使用 Hash 存儲接口的限流配置
request_limit_config "/api2" : {"limit": 10, "time": 1, "timeUnit": "SECONDS"}
hash中的key就是請求的uri路徑,value是一個對象。通過3個屬性,描述限制策略
- limit 最多請求次數
- time 時間
- timeUnit 時間單位
使用普通kv,存儲api的請求次數
request_limit:/api 1
處理請求的時候,通過increment
對該key進行 +1 操作,如果返回1,則表示是第一次請求,此時設置它的過期時間。為限制策略中定義時間限制信息。再通過命名的返回值,判斷是否超出了限制。
increment
指令是線程安全的,不用擔心並發的問題。
使用SpringBoot實現
創建SpringBoot工程,添加spring-boot-starter-data-redis
依賴,並且給出正確的配置。
這里不做工程的創建,配置,以及其他額外代碼的演示,僅僅給出關鍵的代碼。
RedisKeys
定義兩個Key,限流用到的2個Key
public interface RedisKeys {
/**
* api的限制配置,hash key
*/
String REQUEST_LIMIT_CONFIG = "request_limit_config";
/**
* api的請求的次數
*/
String REQUEST_LIMIT = "request_limit";
}
ObjectRedisTemplate
為了提高hash value的序列化效率,自定義一個RedisTemplate的實現。使用jdk的序列化,而不是json。
import org.springframework.data.redis.core.RedisTemplate;
public class ObjectRedisTemplate extends RedisTemplate<String, Object> {
}
RedisConfigration
把自定義的ObjectRedisTemplate配置到IOC
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.RedisSerializer;
import io.springboot.jwt.redis.ObjectRedisTemplate;
@Configuration
public class RedisConfiguration {
@Bean
public ObjectRedisTemplate objectRedisTemplate(@Autowired RedisConnectionFactory redisConnectionFactory) {
ObjectRedisTemplate objectRedisTemplate = new ObjectRedisTemplate();
objectRedisTemplate.setConnectionFactory(redisConnectionFactory);
objectRedisTemplate.setKeySerializer(RedisSerializer.string());
objectRedisTemplate.setValueSerializer(RedisSerializer.java());
// hash的key使用String序列化
objectRedisTemplate.setHashKeySerializer(RedisSerializer.string());
// hash的value使用jdk的序列化
objectRedisTemplate.setHashValueSerializer(RedisSerializer.java());
return objectRedisTemplate;
}
}
RequestLimitConfig
用於描述限制策略的對象。
import java.io.Serializable;
import java.util.concurrent.TimeUnit;
public class RequestLimitConfig implements Serializable {
/**
*
*/
private static final long serialVersionUID = 1101875328323558092L;
// 最大請求次數
private long limit;
// 時間
private long time;
// 時間單位
private TimeUnit timeUnit;
public RequestLimitConfig() {
super();
}
public RequestLimitConfig(long limit, long time, TimeUnit timeUnit) {
super();
this.limit = limit;
this.time = time;
this.timeUnit = timeUnit;
}
public long getLimit() {
return limit;
}
public void setLimit(long limit) {
this.limit = limit;
}
public long getTime() {
return time;
}
public void setTime(long time) {
this.time = time;
}
public TimeUnit getTimeUnit() {
return timeUnit;
}
public void setTimeUnit(TimeUnit timeUnit) {
this.timeUnit = timeUnit;
}
@Override
public String toString() {
return "RequestLimitConfig [limit=" + limit + ", time=" + time + ", timeUnit=" + timeUnit + "]";
}
}
RequestLimitInterceptor
通過攔截器,來完成限流的實現。
import java.nio.charset.StandardCharsets;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import io.springboot.jwt.redis.ObjectRedisTemplate;
import io.springboot.jwt.redis.RedisKeys;
import io.springboot.jwt.web.RequestLimitConfig;
public class RequestLimitInterceptor extends HandlerInterceptorAdapter {
private static final Logger LOGGER = LoggerFactory.getLogger(RequestLimitInterceptor.class);
@Autowired
private ObjectRedisTemplate objectRedisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
/**
* 獲取到請求的URI
*/
String contentPath = request.getContextPath();
String uri = request.getRequestURI().toString();
if (!StringUtils.isEmpty(contentPath) && !contentPath.equals("/")) {
uri = uri.substring(uri.indexOf(contentPath) + contentPath.length());
}
LOGGER.info("uri={}", uri);
/**
* 嘗試從hash中讀取得到當前接口的限流配置
*/
RequestLimitConfig requestLimitConfig = (RequestLimitConfig) this.objectRedisTemplate.opsForHash().get(RedisKeys.REQUEST_LIMIT_CONFIG, uri);
if (requestLimitConfig == null) {
LOGGER.info("該uri={}沒有限流配置", uri);
return true;
}
String limitKey = RedisKeys.REQUEST_LIMIT + ":" + uri;
/**
* 當前接口的訪問次數 +1
*/
long count = this.objectRedisTemplate.opsForValue().increment(limitKey);
if (count == 1) {
/**
* 第一次請求,設置key的過期時間
*/
this.objectRedisTemplate.expire(limitKey, requestLimitConfig.getTime(), requestLimitConfig.getTimeUnit());
LOGGER.info("設置過期時間:time={}, timeUnit={}", requestLimitConfig.getTime(), requestLimitConfig.getTimeUnit());
}
LOGGER.info("請求限制。limit={}, count={}", requestLimitConfig.getLimit(), count);
if (count > requestLimitConfig.getLimit()) {
/**
* 限定時間內,請求超出限制,響應客戶端錯誤信息。
*/
response.setContentType(MediaType.TEXT_PLAIN_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.getWriter().write("服務器繁忙,稍后再試");
return false;
}
return true;
}
}
Controller
一個用於測試的接口類
import java.util.Collections;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping
public Object test () {
return Collections.singletonMap("success", true);
}
}
WebMvcConfigration
攔截器的配置
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import io.springboot.jwt.web.interceptor.RequestLimitInterceptor;
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(this.requestLimitInterceptor())
.addPathPatterns("/test");
}
@Bean
public RequestLimitInterceptor requestLimitInterceptor() {
return new RequestLimitInterceptor();
}
}
通過@Test測試,初始化一個限流配置
@Autowired
private ObjectRedisTemplate objectRedisTemplate;
@Test
public void test () {
// 3秒內,只能請求2次
RequestLimitConfig requestLimitConfig = new RequestLimitConfig(2, 3, TimeUnit.SECONDS);
// 限制的uri是 /test
this.objectRedisTemplate.opsForHash().put(RedisKeys.REQUEST_LIMIT_CONFIG, "/test", requestLimitConfig);
}
使用瀏覽器演示
最后一些問題
怎么靈活的配置
都寫到這個份兒上了,如果熟悉Redis以及客戶端,我想提供一個“限流管理”接口的並不是難事兒。
針對指定的用戶限流
這里演示的方法是,針對接口的限流。有時候,也有一些特殊的需求,需要“針對不同”的用戶來做限流。打個比方。針對A用戶,允許有他1分鍾請求20次接口,針對B用戶,允許他1分鍾請求10次接口。
這個其實也簡單,只需要修改一下上面的兩個限制key,在key中添加用戶的唯一標識(例如:ID)
request_limit_config "/api2:{userId}" : {"limit": 10, "time": 1, "timeUnit": "SECONDS"}
request_limit:{userId}:/api 1
在攔截器中獲取到用戶的ID,加上用戶ID進行檢索和判斷,就可以完成針對用戶的限流。
Restful 接口的問題
@GetMapping("/user/{id}") // restful的檢索接口,往往把ID信息放在了URI中
這就會導致上面的代碼有問題,因為這里采用的是根據URI來完成的限流操作。檢索不同ID的用戶,會導致URI不同。
解決辦法我認為也很簡單。那就不要使用URI,可以通過 自定義注解,方式,不同的接口,定義不同的唯一標識。在攔截器中獲取到注解,讀取到唯一的編碼,代替原來的URI,即可。