接口限流
如果从事过接口开发的时候发现过接口流量异常, 可能有针对接口进行 刷流量 的情况,
所以就需要对接口进行请求限流防止服务崩溃.
如果直接没有对接口进行限流任由网络刷接口会导致数据库|NoSQL服务直接崩溃, 比如数据库 oom kill
这里需要区分开发过程两种网络服务:
- 脚本语言: PHP等
- 编译语言: Java等
对于脚本类型的语言开发接口, 脚本服务并不是常驻内存服务就需要 Redis 做内存访问锁, 如下代码处理:
$redis = new Redis();
// 假设获取到客户端IP
$ip_address = "192.168.1.111";
// 直接判断 redis 之中Key是否存在
// 存在就是目前还在超时或者超时时间偏差值
$key = "timeout_{$ip_address}";
if($redis->exists($key)){
// 这里代表存在该Key需要判断是否超过指定时间
$value = $redis->get($key);
// 计算Redis请求时间和当前时间的误差值
$now = time();
if(is_numeric($value) && (($now - $value) > 1) ){
exit("timeout"); // 防止请求过快直接拦截
}
}
// 写入接口请求的时间戳
$now = time(),
$redis->set($key,$now); // 设置该请求时间
$redis->expire($key,1);// 设置1s之后超时销毁
上面是个简单的接口限流方式, 实际上还需要根据请求的路径 PATH_INFO 做限流让各自接口做不同限流方式.
脚本语言的因为没办法常驻内存所以需要依赖外部 Redis 这种外部内存数据库做标识,
而 Java 这种业务简单的情况可以依靠自身进程来做访问限制处理.
但是大型业务进程会采用分布式部署在不同主机, 所以单独服务进程内存限流并不是合理选择, 所以也需要依赖外部Redis这种共享访问状态.
这里采用简单进程做限流处理:
package com.meteorcat.news.now.config;
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 RestWebMvcConfigurer implements WebMvcConfigurer {
final RestInterceptor restInterceptor;
public RestWebMvcConfigurer(RestInterceptor restInterceptor) {
this.restInterceptor = restInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(restInterceptor)
.addPathPatterns("/**");
}
}
之后就是具体拦截业务:
package com.meteorcat.news.now.config;
import com.meteorcat.news.now.services.MemInterceptorService;
import com.meteorcat.news.now.utils.AccessLimitException;
import com.meteorcat.news.now.utils.IPUtility;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 访问拦截器
* 用于访问限流等处理
*/
@Log4j2
@Component
public class RestInterceptor implements HandlerInterceptor {
/**
* 外部配置的超时请求秒
*/
@Value("${rest.interceptor.access.timeout:3}")
private Integer accessTimeout;
/**
* 内存记录
*/
final MemInterceptorService memInterceptorService;
public RestInterceptor(MemInterceptorService memInterceptorService) {
this.memInterceptorService = memInterceptorService;
}
/**
* 访问拦截
*
* @param request 请求
* @param response 响应
* @param handler 句柄
* @return boolean
* @throws Exception 异常
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String ipAddress = IPUtility.GetIpAddress(request);
long timestamp = System.currentTimeMillis();
log.debug("ACCESS: {} - {} - {}", ipAddress, timestamp, accessTimeout);
// 判断获取误差, 不允许重复请求
if (memInterceptorService.expire(ipAddress, timestamp, accessTimeout)) {
throw new AccessLimitException();
}
// 写入请求记录
memInterceptorService.set(ipAddress, timestamp);
return true;
}
}
这里的 AccessLimitException 是自定义的异常, 用于给 RestControllerAdvice 配置拦截异常返回:
/**
* 拦截服务器异常响应
*/
@RestControllerAdvice
public class RestControllerAdviceConfig {
/**
* 接口限流
*/
@ResponseStatus(HttpStatus.NOT_ACCEPTABLE)
@ExceptionHandler(value = AccessLimitException.class)
public Object accessLimit(AccessLimitException ignore) {
return Response.send(HttpStatus.NOT_ACCEPTABLE, "ACCESS LIMIT", null);
}
}
最后这里就是简单配置持久化常驻内存的进程记录标识:
package com.meteorcat.news.now.services;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* 常驻内存的限流判断
*/
@Service
public class MemInterceptorService {
final Map<String, Long> access = new HashMap<>();
/**
* 判断是否请求限制
*
* @return true(需要限流) | false(直接放行)
*/
public boolean expire(String ipAddress, long timestamp, int offset) {
Long pre = access.get(ipAddress);
if (Objects.isNull(pre)) {
return false;
}
if ((timestamp - pre) < (offset * 1000L)) {
return true;
}
access.remove(ipAddress);
return false;
}
/**
* 写入请求限制器
*/
public void set(String ipAddress, long timestamp) {
access.put(ipAddress, timestamp);
}
}