在高并发场景下,不得不说三大利器:缓存、降级、限流
缓存:将数据缓存起来,减少数据库压力,保护DB和磁盘IO
降级:保护核心系统/服务,降低非核心系统业务请求响应,防止请求积压过多引发系统崩溃
限流:在某一时间段内或者某常规时间对请求进行限制访问,保护系统
微服务分布式应用中,限流、权限鉴定等一般直接在网关可以做,Spring Cloud Gateway官方就提供了RequestRateLimiterGatewayFilterFactory这个类,适用Redis和lua脚本实现了令牌桶限流的方式.nginx也可以做,在nginx limit_req模块
本文是在应用层通过Aop这种方式去做限流
撸代码之前先介绍下google的一个框架插件Guava https://www.yiibai.com/guava/ 除了Guava,还有其他限流相应的解决框架,如:阿里的sentinel(没有接触过)、spring-cloud-zuul-ratelimit(https://github.com/marcosbarbero/spring-cloud-zuul-ratelimit)等
Guava是一种基于开源的Java库,其中包含谷歌正在由他们很多项目使用的很多核心库。这个库是为了方便编码,并减少编码错误。这个库提供用于集合,缓存,支持原语,并发性,常见注解,字符串处理,I/O和验证的实用方法。原文出自【易百教程】,商业转载请联系作者获得授权,非商业转载请保留原文链接:https://www.yiibai.com/guava/
这里会用到LoadingCache和RateLimiter。涉及到一些算法知识(令牌通算法/漏桶算法),两者算法以及差异博主之前文章有介绍
LoadingCache类似ConcurrentMap,也是线程安全的。但LoadingCache增加了更多的元素失效策略,ConcurrentMap只能显示的移除元素
撸代码:
自定义注解RateLimit
package com.chwl.cn.ann;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(value={ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
double limitNum() default 100.0;//请求数
boolean ipRestricted() default true; //是否限制同一IP
double ipLimitNum() default 1.0;//同一IP访问的次数(默认每秒)
}
Controller:每秒最多两个请求
@RequestMapping(value = "/get/{id}", method = RequestMethod.GET)
@RateLimit(limitNum=2.0,ipRestricted=true,ipLimitNum=1.0)
public ProductEntity get(@PathVariable("id") long id) {
return productService.selectById(id);
}
AOP:如果在一秒内能获取到令牌,同样放行。灵活运用,如果不要,每秒最多就只有两个请求并发进入接口应用
package com.chwl.cn.aspect;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import com.chwl.cn.ann.RateLimit;
import com.chwl.cn.config.cache.ConcurrentHashMapCache;
import com.chwl.cn.utils.result.JsonMsg;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.util.concurrent.RateLimiter;
@Component
@Aspect
@Order(0)
public class RateLimitAspect {
private Logger log = LoggerFactory.getLogger(RateLimitAspect.class);
// 用来存放不同接口的RateLimiter(key为接口名称,value为RateLimiter)
private static ConcurrentHashMap map = new ConcurrentHashMap<>();
private static ObjectMapper objectMapper = new ObjectMapper();
static {
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
}
@Autowired
private HttpServletRequest request;
@Autowired
private HttpServletResponse response;
@Pointcut("@annotation(com.chwl.cn.ann.RateLimit)")
public void serviceLimit() {
}
@Around("serviceLimit()")
public Object Around(ProceedingJoinPoint joinPoint) throws NoSuchMethodException {
Object obj = null;
// 获取拦截的方法名
Signature sig = joinPoint.getSignature();
String methodName = sig.getName();
// 获取拦截的方法名
MethodSignature msig = (MethodSignature) sig;
// 返回被织入增加处理目标对象
Object target = joinPoint.getTarget();
Method method = msig.getMethod();
RateLimit annotation = method.getAnnotation(RateLimit.class);
// RateLimit annotation = target.getClass().getAnnotation(RateLimit.class);
double limitNum = annotation.limitNum();
String requestURI = request.getRequestURI();
// 避免方法名重复导致rateLimiter被覆盖
String key=requestURI+methodName;
// 获取rateLimiter
// ConcurrentHashMapCache cacheManager = ConcurrentHashMapCache.getCacheManagerInstance();
// cacheManager.init();
if (!map.containsKey(key)) {
map.put(key, RateLimiter.create(limitNum));
}
RateLimiter rateLimiter = map.get(key);
try {
// 是否能马上获取到令牌或者在1秒之内能获取到1个令牌
if (rateLimiter.tryAcquire()||rateLimiter.tryAcquire(1,1,TimeUnit.SECONDS)) {
log.info("他们真的来了");
// 放行,执行方法
obj=joinPoint.proceed();
} else {
// 限制访问 拒绝了请求(服务降级)
String result = objectMapper.writeValueAsString(JsonMsg.Error(500, "系统繁忙!"));
log.error("拒绝了请求:" + result);
outErrorResult(result);
}
} catch (Throwable e) {
}
return obj;
}
// 将结果返回
public void outErrorResult(String result) {
response.setContentType("application/json;charset=UTF-8");
try (ServletOutputStream outputStream = response.getOutputStream()) {
outputStream.write(result.getBytes("utf-8"));
} catch (IOException e) {
e.printStackTrace();
}
}
}
使用JMeter并发接口20个请求
这里是分几次并发的,部分请求被限制了
继续对IP限流,例子:接口限流每秒2个请求,IP每秒限流访问1次 在实际业务是有对IP进行限流的,并且不在少数
还是同样的controller方法,
@RequestMapping(value = "/get/{id}", method = RequestMethod.GET)
@RateLimit(limitNum=2.0,ipRestricted=true,ipLimitNum=1.0)
public ProductEntity get(@PathVariable("id") long id) {
return productService.selectById(id);
}
AOP:
package com.chwl.cn.aspect;
import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import com.chwl.cn.ann.RateLimit;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.util.concurrent.RateLimiter;
@Component
@Aspect
@Order(0)
public class RateLimitAspect {
private Logger log = LoggerFactory.getLogger(RateLimitAspect.class);
// 用来存放不同接口的RateLimiter(key为接口名称,value为RateLimiter)
private static ConcurrentHashMap map = new ConcurrentHashMap<>();
private static ObjectMapper objectMapper = new ObjectMapper();
private RateLimiter rateLimiter;
static {
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
}
@Autowired
private HttpServletRequest request;
@Autowired
private HttpServletResponse response;
@Pointcut("@annotation(com.chwl.cn.ann.RateLimit)")
public void serviceLimit() {
}
@Around("serviceLimit()")
public Object Around(ProceedingJoinPoint joinPoint) throws NoSuchMethodException {
Object obj = null;
// 获取拦截的方法名
Signature sig = joinPoint.getSignature();
String methodName = sig.getName();
// 获取拦截的方法名
MethodSignature msig = (MethodSignature) sig;
// 返回被织入增加处理目标对象
Object target = joinPoint.getTarget();
Method method = msig.getMethod();
RateLimit annotation = method.getAnnotation(RateLimit.class);
// RateLimit annotation = target.getClass().getAnnotation(RateLimit.class);
double limitNum = annotation.limitNum();
String requestURI = request.getRequestURI();
// 避免方法名重复导致rateLimiter被覆盖
String key=requestURI+methodName;
// 获取rateLimiter
// ConcurrentHashMapCache cacheManager = ConcurrentHashMapCache.getCacheManagerInstance();
// cacheManager.init();
String ip = getIpAddress(request);
if (!map.containsKey(key)) {
rateLimiter= RateLimiter.create(limitNum,1,TimeUnit.SECONDS);
map.put(key,rateLimiter);
}
rateLimiter = map.get(key);
try {
if (rateLimiter.tryAcquire()) {
log.info("他们真的来了");
boolean ipRestricted = annotation.ipRestricted();
if(ipRestricted){
double ipLimitNum = annotation.ipLimitNum();
//IP限流总次数总是不大于接口总限流次数
if(ipLimitNum>limitNum){
ipLimitNum=limitNum;
}
//组装ip次数放令牌桶
String loadingCacheKey=key+ip+"&"+ipLimitNum;
RateLimiter limiter = caches.get(loadingCacheKey);
if(limiter.tryAcquire()){
log.error("来了来了来了");
// 放行,执行方法
obj=joinPoint.proceed();
}else {
//根据实际业务处理
log.error("网络连接错误,当前IP请求错误,每个IP每秒最多只能访问"+ipLimitNum+"次");
}
}else {
obj=joinPoint.proceed();
}
} else {
// 限制访问 拒绝了请求(服务降级)
log.error("拒绝了请求");
}
} catch (Throwable e) {
}
return obj;
}
// 根据IP分不同的令牌桶 目的在于对每个IP的令牌桶RateLimiter本地缓存在loadingcach
private static LoadingCache caches = CacheBuilder.newBuilder().maximumSize(1000)
// 一秒过期
.expireAfterWrite(1, TimeUnit.SECONDS)
//通过CacheLoader构建RateLimiter在loadingchahe本地缓存起来,如果不存在则自动新建并缓存,存在直接取出
.build(new CacheLoader() {
@Override
public RateLimiter load(String key) throws Exception {
String ipLimitNum = (key.split("&"))[1];
// 新的IP初始化 (限流每秒ipLimitNum个令牌响应)
return RateLimiter.create(Double.valueOf(ipLimitNum));
}
});
public static String getIpAddress(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}
JMEter并发10个线程测试
每秒最多2次,限制IP只有一次是通的,验证成功
可以将ConcurrentHashMap换成LoadingCache也是一样
以上都是基于单体应用的接口限流或者本身提供的服务不是集群,如果是微服务分布式项目,对服务进行集群了的,以上方法行不通,RateLimiter对于分布式集群等乏力。需要其他方案,后面附上