限流算法及实现

合法性验证限流

验证码

IP黑名单

容器限流

Tomcat

Tomcat设置最大线程数

Nginx

控制速率 rate
控制并发数

服务端限流

固定时间窗口算法

固定的时间(比如1分钟)划分窗口,每个窗口允许访问N次(限流)
缺点:流量分布不均,不好控制,临界值有问题

滑动时间窗口算法

细分为多个小窗口,多个小窗口构成一个打窗口进行限流。每个小窗口都有自己的计数器,时间跨度更小了,不存在临界值问题
缺点:实现起来较为复杂,每个小窗口都有计数

漏桶算法

漏桶(Leaky Bucket)算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率。示意图如下:
在这里插入图片描述

在这里插入图片描述

示例:

可见这里有两个变量,一个是桶的大小,支持流量突发增多时可以存多少的水(burst),另一个是水桶漏洞的大小(rate),伪代码如下:

double rate;               // leak rate in calls/s
double burst;              // bucket size in calls

long refreshTime;          // time for last water refresh
double water;              // water count at refreshTime

refreshWater() {
    long  now = getTimeOfDay();
    
    //水随着时间流逝,不断流走,最多就流干到0.
    water = max(0, water- (now - refreshTime)*rate); 
    refreshTime = now;
}

bool permissionGranted() {
    refreshWater();
    if (water < burst) { // 水桶还没满,继续加1
        water ++;
        return true;
    } else {
        return false;
    }
}

因为漏桶的漏出速率是固定的参数,所以,即使网络中不存在资源冲突(没有发生拥塞),漏桶算法也不能使流突发(burst)到端口速率。因此,漏桶算法对于存在突发特性的流量来说缺乏效率

令牌桶算法

令牌桶算法(Token Bucket)和漏桶算法的效果一样但方向相反的算法,更加容易理解。随着时间流逝,系统会按恒定1/QPS时间间隔(如果QPS=100,则间隔是10ms)往桶里加入Token(想象和漏洞漏水相反,有个水龙头在不断的加水),如果桶已经满了,令牌就溢出了。如果桶未满,令牌可以积累。新请求来临时,会各自拿走一个Token,如果没有Token可拿了就阻塞或者拒绝服务。
在这里插入图片描述

令牌桶算法:一个存放固定容量令牌的桶,按照固定速率(每秒/或者可以自定义时间)往桶里添加令牌,然后每次获取一个令牌,当桶里没有令牌可取时,则拒绝服务
令牌桶分为2个动作,动作1(固定速率往桶中存入令牌)动作2(客户端如果想访问请求,先从桶中获取token)

流入:以固定速率从桶中流入水滴
流出:按照任意速率从桶中流出水滴

技术上使用Google开源工具包Guava提供了限流工具类RateLimiter,该类基于令牌桶算法来完成限流,非常易于使用。RateLimiter是guava提供的基于令牌桶算法的实现类,可以非常简单的完成限流特技,并且根据系统的实际情况来调整生成token的速率。 RateLimiter 是单机(单进程)的限流,是JVM级别的的限流,所有的令牌生成都是在内存中,在分布式环境下不能直接这么用。
优点:支持大的并发,有效利用网络带宽

在这里插入图片描述

漏桶和令牌桶的区别

并不能说明令牌桶一定比漏洞好,她们使用场景不一样。

令牌桶算法,放在服务端,用来保护服务端(自己),主要用来对调用者频率进行限流,为的是不让自己被压垮。所以如果自己本身有处理能力的时候,如果流量突发(实际消费能力强于配置的流量限制=桶大小),那么实际处理速率可以超过配置的限制(桶大小)。
漏桶算法,放在调用方,这是用来保护他人,也就是保护他所调用的系统。主要场景是,当调用的第三方系统本身没有保护机制,或者有流量限制的时候,我们的调用速度不能超过他的限制,由于我们不能更改第三方系统,所以只有在主调方控制。这个时候,即使流量突发,也必须舍弃。因为消费能力是第三方决定的。

限流实现

1、 RateLimiter简介(guava的令牌桶实现) – 单机版
Google开源工具包Guava提供了限流工具类RateLimiter,该类基于令牌桶算法(Token Bucket)来完成限流,非常易于使用.RateLimiter经常用于限制对一些物理资源或者逻辑资源的访问速率.它支持两种获取permits接口,一种是如果拿不到立刻返回false,一种会阻塞等待一段时间看能不能拿到。原理见《guava–RateLimiter源码分析》

RateLimiter和Java中的信号量(java.util.concurrent.Semaphore)类似,Semaphore通常用于限制并发量.

源码注释中的一个例子,比如我们有很多任务需要执行,但是我们不希望每秒超过两个任务执行,那么我们就可以使用RateLimiter:

final RateLimiter rateLimiter = RateLimiter.create(2.0);
void submitTasks(List<Runnable> tasks, Executor executor) {
    for (Runnable task : tasks) {
        rateLimiter.acquire(); // may wait
        executor.execute(task);
    }
}

另外一个例子,假如我们会产生一个数据流,然后我们想以每秒5kb的速度发送出去.我们可以每获取一个令牌(permit)就发送一个byte的数据,这样我们就可以通过一个每秒5000个令牌的RateLimiter来实现:

final RateLimiter rateLimiter = RateLimiter.create(5000.0);
void submitPacket(byte[] packet) {
    rateLimiter.acquire(packet.length);
    networkService.send(packet);
}

另外,我们也可以使用非阻塞的形式达到降级运行的目的,即使用非阻塞的tryAcquire()方法:

if(limiter.tryAcquire()) { //未请求到limiter则立即返回false
    doSomething();
}else{
    doSomethingElse();
}

AOP实现限流

1、JDK动态代理

@Aspect
public class ApiRateLimiterAspect extends ApiRateLimiterAbstractImpl {
	
	@Pointcut("@annotation(com.jd.wx.common.api.limit.ApiRateLimiter)")
	public void annotationPoint() {
	}

	@Around("annotationPoint()")
	public Object execAnnotation(ProceedingJoinPoint jp) throws Throwable {
		try {
	        if (jp.getSignature() instanceof MethodSignature) {
	            MethodSignature methodSignature = (MethodSignature) jp.getSignature();
	            Method method = methodSignature.getMethod();
	            ApiRateLimiter apiRateLimiter = method.getAnnotation(ApiRateLimiter.class);
	            if (!tryAcquire(apiRateLimiter, methodSignature.getMethod(), jp.getArgs())) {
	            	return generateReturnObject(apiRateLimiter, method.getReturnType());
                }
	        }
        
    	} catch (TryAcquireException t) {
    		throw t;
		} catch (Throwable t) {
		}
        return jp.proceed();
	}
}

2、cglib动态代理

public class ApiRateLimiterInterceptor extends ApiRateLimiterAbstractImpl implements MethodInterceptor {
	public Object invoke(MethodInvocation invocation) throws Throwable {		
		try {
			Method method = invocation.getMethod();
			if (method != null) {
				ApiRateLimiter apiRateLimiter = method.getAnnotation(ApiRateLimiter.class);
				
				if (!tryAcquire(apiRateLimiter, method, invocation.getArguments())) {
					return generateReturnObject(apiRateLimiter, method.getReturnType());
                }
			}
    	} catch (TryAcquireException t) {
    		throw t;
		} catch (Throwable t) {
		}
		
		return invocation.proceed();
	}
}

公用的tryAcquire方法:

public class ApiRateLimiterAbstractImpl extends AbstractCommonImpl {
	/** 各接口限流RateLimiter集合   */
    private static final ConcurrentMap<String, RateLimiter> API_RESOURCE_LIMITER_MAP = Maps.newConcurrentMap();
    
    private static final String GLOBAL_SWITCH = "isSwitchOn";
    private static final String KEY_SUFFIX_SWITCH = "switch";
    private static final String KEY_SUFFIX_QPS = "qps";
    private static final long DEFAULT_HOST_COUNT = 1;
    
    protected Object generateReturnObject(ApiRateLimiter apiRateLimiter, Class<?> returnType) throws TryAcquireException {
    	
    	if (apiRateLimiter.returnException()) {
    		throw new TryAcquireException("Your method triggers current limit");
        }
    	
    	Object codeValue = apiRateLimiter.codeValue();    	
    	if(apiRateLimiter.codeItemType().isAssignableFrom(Integer.class)){
    		codeValue = Integer.parseInt(apiRateLimiter.codeValue());
    	}
    	
    	return generateReturnObject(returnType, apiRateLimiter.codeItem(), codeValue, apiRateLimiter.msgItem(), apiRateLimiter.msgValue(), apiRateLimiter.returnJson());    	
	}
    
    protected boolean tryAcquire(ApiRateLimiter apiRateLimiter, Method method, Object[] args) {
    	if (apiRateLimiter == null) {
			return true;
		}
    	
    	if (method == null) {
			return true;
		}
    	
    	try {	    	
	    	
	    	final String source = getSource(apiRateLimiter, args);
	    	final String propertyName = getPropertyName(apiRateLimiter, source);
			if (StringUtils.isEmpty(propertyName)){
				return true;
			}
			
			final Map<String, String> duccConfigMap = getDuccPropertyByMap(apiRateLimiter.duccResource(), propertyName);
			if(duccConfigMap == null){
	        	return true;
	        }
			
			final String methodName = getMethodName(apiRateLimiter, method);
			final long qps = getQps(apiRateLimiter, duccConfigMap, methodName);
			final boolean isSwitchOn = getIsSwitchOn(apiRateLimiter, duccConfigMap, methodName);
			
			if (isSwitchOn && qps > 0) {
	        	final String limiterName = propertyName + "." + methodName;
	            RateLimiter limiter = API_RESOURCE_LIMITER_MAP.get(limiterName);
	            if (limiter == null) {
	                limiter = RateLimiter.create(qps);
	                API_RESOURCE_LIMITER_MAP.put(limiterName, limiter);
	            }
	            if (limiter.getRate() != qps) {
	                limiter.setRate(qps);
	            }
	            return limiter.tryAcquire();
	        }
    	} catch (Throwable t) {
		}
        return true;
    }

    private String getSource(ApiRateLimiter apiRateLimiter, Object[] args) {
    	if (args == null){
    		return null;
    	}
    	if(args.length < apiRateLimiter.argumentIndex() + 1){
    		return null;
    	}
    	    	
        return getObjectItemValue(args[apiRateLimiter.argumentIndex()], apiRateLimiter.sourceItem());
    }
    
    private String getPropertyName(ApiRateLimiter apiRateLimiter, String source) {
    	if (StringUtils.isEmpty(source)){
        	return apiRateLimiter.duccPropertyPrefix();
        }
        return apiRateLimiter.duccPropertyPrefix() +  "_" + source;
    }
    
    private String getMethodName(ApiRateLimiter apiRateLimiter, Method method) {
    	if (StringUtils.isNotEmpty(apiRateLimiter.methodName())){
    		return apiRateLimiter.methodName();
    	}
    	
    	if(method == null){
    		return null;    		
    	}
        return method.getDeclaringClass().getName() + "." + method.getName();
    }
    
    private long getHostCount(ApiRateLimiter apiRateLimiter) {    	
    	Long hostCount = getDuccPropertyByLong(apiRateLimiter.duccResource(), apiRateLimiter.duccPropertyHostCount());
    	if(hostCount == null){
    		return DEFAULT_HOST_COUNT;
    	}    	
    	if(hostCount.longValue() < DEFAULT_HOST_COUNT){
    		return DEFAULT_HOST_COUNT;
		}    	
    	
        return hostCount.longValue();
    }
    
    private long getTotalQps(ApiRateLimiter apiRateLimiter, Map<String, String> duccConfigMap, String methodName) {
    	long qps = apiRateLimiter.defaultQps();    	
    	if(duccConfigMap != null){
    		String qpsKey = methodName + "." + KEY_SUFFIX_QPS;
    		
    		String qpsStr = duccConfigMap.get(qpsKey);        	
    		if(qpsStr == null){
    			qpsStr = duccConfigMap.get(methodName);
    		}
    		
    		if(qpsStr != null){
        		qps = Long.valueOf(qpsStr.trim());
    		}
    	}
   	
        return qps;
    }
    
    private long getQps(ApiRateLimiter apiRateLimiter, Map<String, String> duccConfigMap, String methodName) {
    	long qps = getTotalQps(apiRateLimiter, duccConfigMap, methodName);
    	if(qps <= 0){
    		return 0;
    	}
    	
    	long hostCount = getHostCount(apiRateLimiter);
    	
    	long oneHostQps = qps/hostCount;    	
    	if(oneHostQps <1 ){
    		oneHostQps = 1;
		}
    	
    	return oneHostQps;
    }

    private boolean getIsSwitchOn(ApiRateLimiter apiRateLimiter, Map<String, String> duccConfigMap, String methodName) {
        if(duccConfigMap == null){
        	return apiRateLimiter.defaultIsSwitchOn();
        }
        
        String isSwitchOnKey = methodName + "." + KEY_SUFFIX_SWITCH;        
        String isSwitchOnStr = duccConfigMap.get(isSwitchOnKey);
        if(isSwitchOnStr == null){
        	isSwitchOnStr = duccConfigMap.get(GLOBAL_SWITCH);
		}
        
        if(isSwitchOnStr == null){
    		return apiRateLimiter.defaultIsSwitchOn();
		}else{
			return BooleanUtils.toBoolean(isSwitchOnStr.trim());
		}        
    }    
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
令牌桶算法是一种常见的限流算法,它可以控制请求的速率,防止系统被过多的请求压垮。下面是Java实现令牌桶算法的步骤和代码逻辑: 1. 定义一个令牌桶类,包含以下属性: - 最后一次令牌发放时间 - 桶的容量 - 令牌生成速度 - 当前令牌数量 2. 实现一个获取令牌的方法,该方法会在每次请求到来时被调用,具体实现如下: - 计算当前令牌数量 - 判断当前令牌数量是否足够 - 如果令牌数量不足,则拒绝请求 - 如果令牌数量足够,则领取令牌,并执行业务逻辑 3. 使用ScheduledExecutorService定时生成令牌,具体实现如下: - 每隔一段时间生成一定数量的令牌 - 如果令牌数量超过桶的容量,则不再生成令牌 下面是Java实现令牌桶算法的代码逻辑: ``` @Slf4j public class TokensLimiter { private ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5); // 最后一次令牌发放时间 public long timeStamp = System.currentTimeMillis(); // 桶的容量 public int capacity = 10; // 令牌生成速度10/s public int rate = 10; // 当前令牌数量 public int tokens; public void acquire() { scheduledExecutorService.scheduleWithFixedDelay(() -> { long now = System.currentTimeMillis(); // 当前令牌数 tokens = Math.min(capacity, (int) (tokens + (now - timeStamp) * rate / 1000)); // 每隔0.5秒发送随机数量的请求 int permits = (int) (Math.random() * 9) + 1; log.info("请求令牌数:" + permits + ",当前令牌数:" + tokens); timeStamp = now; if (tokens < permits) { // 若不到令牌,拒绝 log.info("限流了"); } else { // 还有令牌,领取令牌 tokens -= permits; log.info("剩余令牌=" + tokens); } }, 1000, 500, TimeUnit.MILLISECONDS); } public static void main(String[] args) { TokensLimiter tokensLimiter = new TokensLimiter(); tokensLimiter.acquire(); } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值