基于接口级别的限流

在高并发场景下,不得不说三大利器:缓存、降级、限流

缓存:将数据缓存起来,减少数据库压力,保护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<String, RateLimiter> 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<String, RateLimiter> 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<String, RateLimiter> caches = CacheBuilder.newBuilder().maximumSize(1000)
			// 一秒过期
			.expireAfterWrite(1, TimeUnit.SECONDS)
			//通过CacheLoader构建RateLimiter在loadingchahe本地缓存起来,如果不存在则自动新建并缓存,存在直接取出
			.build(new CacheLoader<String, RateLimiter>() {
				@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对于分布式集群等乏力。需要其他方案,后面附上

发布了288 篇原创文章 · 获赞 88 · 访问量 43万+

猜你喜欢

转载自blog.csdn.net/ypp91zr/article/details/90678462