使用Redis實(shí)現(xiàn)限流
- 原理:使用Redis的Hash數(shù)據(jù)結(jié)構(gòu),把對(duì)應(yīng)的key蘸炸、接口url、限流規(guī)則存放進(jìn)redis尖奔,在攔截器中對(duì)接口進(jìn)行攔截搭儒,獲取到有配置的url,進(jìn)行規(guī)則獲取提茁,獲取到規(guī)則之后淹禾,對(duì)redis對(duì)應(yīng)接口對(duì)應(yīng)key進(jìn)行increment自增的操作(increment 指令是線程安全的,不用擔(dān)心并發(fā)的問題),如果是第一次的話,設(shè)置該key的過期時(shí)間茴扁,過期時(shí)間為配置時(shí)間铃岔,單位為配置單位,下次調(diào)用的時(shí)候峭火,如果當(dāng)前接口對(duì)應(yīng)key的自增數(shù)大于配置的limit數(shù)則進(jìn)行請(qǐng)求超出限制的提示毁习。
-
具體步驟
- 引入Redis依賴包,和其他工具包
<!-- 阿里json -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.41</version>
</dependency>
<!-- 整合Redis start -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 整合Redis end -->
- 在yml文件中編寫限流接口和限流規(guī)則
request_limit:
# 限流的接口
url: /utils/redis,/demo/user/account
rules:
# 限流規(guī)則智嚷,每秒3次調(diào)用
limit: 3
time: 1
timeUnit: SECONDS
- 編寫限流配置類
public class RequestLimitConfig implements Serializable {
private static final long serialVersionUID = 1101875328323558092L;
// 最大請(qǐng)求次數(shù)
private long limit;
// 時(shí)間
private long time;
// 時(shí)間單位
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 + "]";
}
}
- 繼承
HandlerInterceptorAdapter
類,實(shí)現(xiàn)其方法編寫限流攔截器
import com.alibaba.fastjson.JSONObject;
import com.example.demo.config.RequestLimitConfig;
import com.example.demo.constants.GlobalConstants;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
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 java.nio.charset.StandardCharsets;
/**
* 接口限流攔截器
*/
public class RequestLimitInterceptor extends HandlerInterceptorAdapter {
private static final Logger log = LoggerFactory.getLogger(RequestLimitInterceptor.class);
@Autowired
private RedisTemplate redisTemplate;
/**
* 在方法被調(diào)用前執(zhí)行。在該方法中可以做類似校驗(yàn)的功能纺且。如果返回true盏道,則繼續(xù)調(diào)用下一個(gè)攔截器
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
/**
* 獲取到請(qǐng)求的URI
*/
String contentPath = request.getContextPath();
String uri = request.getRequestURI();
if (!StringUtils.isEmpty(contentPath) && !contentPath.equals("/")) {
uri = uri.substring(uri.indexOf(contentPath) + contentPath.length());
}
log.info("uri={}", uri);
/**
* 嘗試從hash中讀取得到當(dāng)前接口的限流配置
*/
String str = this.redisTemplate.opsForHash().get(GlobalConstants.REQUEST_LIMIT_CONFIG, uri).toString();
RequestLimitConfig requestLimitConfig = JSONObject.parseObject(str, RequestLimitConfig.class);
if (requestLimitConfig == null) {
log.info("該uri={}沒有限流配置", uri);
return true;
}
String limitKey = GlobalConstants.REQUEST_LIMIT + ":" + uri;
/**
* 當(dāng)前接口的訪問次數(shù) +1 increment 指令是線程安全的,不用擔(dān)心并發(fā)的問題
*/
long count = this.redisTemplate.opsForValue().increment(limitKey);
if (count == 1) {
/**
* 第一次請(qǐng)求载碌,設(shè)置key的過期時(shí)間
*/
this.redisTemplate.expire(limitKey, requestLimitConfig.getTime(), requestLimitConfig.getTimeUnit());
log.info("設(shè)置過期時(shí)間:time={}, timeUnit={}", requestLimitConfig.getTime(), requestLimitConfig.getTimeUnit());
}
log.info("請(qǐng)求限制猜嘱。limit={}, count={}", requestLimitConfig.getLimit(), count);
if (count > requestLimitConfig.getLimit()) {
/**
* 限定時(shí)間內(nèi),請(qǐng)求超出限制嫁艇,響應(yīng)客戶端錯(cuò)誤信息泉坐。
*/
response.setContentType(MediaType.TEXT_PLAIN_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.getWriter().write("服務(wù)器繁忙,稍后再試");
return false;
}
return true;
}
/**
* 在方法執(zhí)行后調(diào)用(暫未使用)
*
* @param request
* @param response
* @param handler
* @param modelAndView
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
super.postHandle(request, response, handler, modelAndView);
}
}
- 實(shí)現(xiàn)
WebMvcConfigurer
接口,編寫資源配置類
/**
* 資源配置器
*/
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
@Value("${request_limit.url}")
private String url;
// 添加攔截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 添加限流的接口
registry.addInterceptor(this.requestLimitInterceptor())
.addPathPatterns(url.split(","));
}
@Bean
public RequestLimitInterceptor requestLimitInterceptor() {
return new RequestLimitInterceptor();
}
}
- 編寫服務(wù)啟動(dòng)時(shí)注入限流接口和規(guī)則
import com.alibaba.fastjson.JSONObject;
import com.example.demo.constants.GlobalConstants;
import com.example.demo.exception.BusinessException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
/**
* 初始化運(yùn)行方法
*/
@Component
public class ApplicationRunnerImpl implements ApplicationRunner {
private static final Logger log = LoggerFactory.getLogger(ApplicationRunnerImpl.class);
// 引入redis
@Autowired
private RedisTemplate redisTemplate;
@Value("${request_limit.url}")
private String url;
@Value("${request_limit.rules.limit}")
private String limit;
@Value("${request_limit.rules.time}")
private String time;
@Value("${request_limit.rules.timeUnit}")
private String timeUnit;
@Override
public void run(ApplicationArguments args) throws Exception {
// 已經(jīng)寫好了方法
//TestCron.init();
JSONObject rulesJson = new JSONObject();
rulesJson.put("limit", limit);
rulesJson.put("time", time);
rulesJson.put("timeUnit", timeUnit);
try {
String[] urlArr = url.split(",");
// 初始化在Redis中存入接口限流規(guī)則
for (String item : urlArr) {
redisTemplate.opsForHash().put(GlobalConstants.REQUEST_LIMIT_CONFIG, item, rulesJson);
log.info("Redis存放成功,接口地址:" + item + " 限流規(guī)則:" + " 時(shí)間:" + time + " 單位:" + timeUnit + " 次數(shù):" + limit);
}
} catch (Exception e) {
e.printStackTrace();
throw new BusinessException("限流規(guī)則配置錯(cuò)誤,請(qǐng)使用逗號(hào)分割");
}
}
}
- Controller接口寫入配置的路徑即可
- 進(jìn)行請(qǐng)求,快速刷新瀏覽器裳仆,結(jié)果如圖