限流算法之漏桶算法与令牌桶算法

本文深入解析了漏桶算法和令牌桶算法在流量控制中的应用,通过具体实例展示了如何使用Google Guava库中的RateLimiter类实现接口限流,确保服务稳定性和响应效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

背景:

      在是我们实际的生产场景中,我们需要对大到整个服务网关,细到某个接口方法做流量控制,限流,解决并发请求负载过高,导致大到可能压垮服务,导致某节点不可用,所以我们需要限流,通常做法是请求超出预算后排队,或者直接拒绝请求.

漏桶算法

      漏桶算法基本思路就是当请求到一个缓冲区,然后从缓冲区流出再去处理请求,缓冲区(桶)的大小,从缓冲区(请求响应速度)流出的速度是关键。其基本可以用一个缓冲队列来实现。

 

 

上面为简单的漏桶流程图

 

令牌桶算法

      令牌桶算法是是另外一种限流算法,顾名思义,当请求需要从桶中拿到令牌才能进行接口的访问,其中大体思路是,一个令牌队列(桶),一个定时生成令牌放入桶中,请求过来需要经过这个桶,当从队列取得令牌后才能进行后续处理,否则排队,也可以丢弃请求,令牌的生成速度影响请求响应效率。

google的一个工具包中有一个RateLimiter类,它是令牌桶算法的一种实现,下面贴出它的依赖,以及使用场景

<dependency>
    <groupId>com.google.guava</groupId>
	<artifactId>guava</artifactId>
    <version>26.0-jre</version>
</dependency>

 令牌桶场景:假设我针对某个方法使用RateLimiter实现限流控制,该方法每秒只能一个一个请求进去

import java.util.UUID;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import com.space.rabbitmq.common.annotation.TokenBucketAnnotation;


@RestController
public class TokenBucketController {
    @TokenBucketAnnotation(description = "测试令牌桶")
    @GetMapping("/token")
    public String token(String message){
        String uuid = UUID.randomUUID().toString();
        return uuid;
    }
}

这个一个普通的controller, 我们针对真个controller限流,每秒只能一次访问,下面我门需要一个令牌桶注解,方便我们实现切面

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface TokenBucketAnnotation {
	String description() default "";
}

下面需要实现一个切面在调用之前判断是否有足够的令牌,如果令牌不足抛出拒绝请求

import com.google.common.util.concurrent.RateLimiter;
import com.space.rabbitmq.common.annotation.TokenBucketAnnotation;
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.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Component
@Aspect
public class TokenBucketAspect {
	
	private Map<Method, RateLimiter> tokenBucket = new ConcurrentHashMap<>();

    @Pointcut("@annotation(com.space.rabbitmq.common.annotation.TokenBucketAnnotation)")
    public void tokenBucketManager() {
    }
	
    @Around("tokenBucketManager()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {

        Class<?> aClass = joinPoint.getTarget().getClass();
        Signature signature = joinPoint.getSignature();
        String targetName = aClass.getName();
        String methodName = signature.getName();
        Class<?> targetClass = Class.forName(targetName);
        Method[] methods = targetClass.getMethods();
        String desc;
        Method method = Arrays.stream(methods).filter(m -> m.getName().equals(methodName)).findFirst().get();
        TokenBucketAnnotation annotation = method.getAnnotation(TokenBucketAnnotation.class);
        desc = annotation.description();
        System.out.println(desc);
        //log.....
        acquire(method);
        Object ret = null;
        try{
            ret = joinPoint.proceed();
        }catch (Throwable e){
        	//log......
        	//为了测试方便  可以自己自定义令牌不够丢弃请求的异常,这里为了测试方便就不写了
        	throw new Throwable(e.getMessage());
        }
        return ret;
    }
    
    private void acquire(Method method) throws Throwable{
    	RateLimiter limiter = tokenBucket.get(method);
    	if(null == limiter){
    		//假设这个方法最大并发数为1, 每秒生成一个令牌
    		limiter = RateLimiter.create(1);
    		tokenBucket.put(method, limiter);
    	}
    	//如果请求超过1,令牌不够 需要排队
    	//limiter.acquire();
    	//令牌不够的情况直接丢弃请求   tryAcquire方法会尝试获取令牌,如果令牌不足放回false
    	if(!limiter.tryAcquire()){
    		throw new Throwable("令牌不足");
    	}
    }

    @Before("tokenBucketManager()")
    public void before(){

    }
}

这样我们就可以针对某个方法实现限流,在浏览器中迅速刷新请求可以看出效果

### 令牌桶算法桶算法对比 #### 实现原理 令牌桶算法(Token Bucket Algorithm)通过维持一个固定容量的令牌桶来控制数据包的发送速率。每当有新请求到来时,会尝试消耗一定数量的令牌;如果此时有足够的令牌,则允许该操作继续执行并减少相应数量的令牌。反之则拒绝服务或延迟处理直至获得足够的资源许可[^3]。 相比之下,桶算法(Leaky Bucket Algorithm)采用了一种更为严格的流量调控机制——想象有一个带有孔洞的容器持续向外渗水,当新的水流进入此装置内部时会被暂时储存起来等待缓慢流出。这意味着即使短时间内有大量的输入到达也只会按照固定的滴速排出,从而实现了平滑的数据流输出效果[^1]。 #### 主要差异 最显著的不同之处在于两者对于突发流量的支持程度: - **突发支持**:由于其设计特性,“令牌桶”可以容纳一定程度上的瞬时高峰负荷,在满足预设条件的情况下允许一次性释放较多量级的信息传递出去; - **严格限速**:“桶”的工作方式决定了它不具备这样的灵活性,始终保持着恒定不变的速度对外提供服务而不受外界因素干扰[^2]。 #### 应用场景 基于上述特点,两种方法适用于不同类型的业务需求: - 对于那些可能经历间歇性高并发访问压力的服务端接口而言,比如社交平台的消息推送功能或是电商平台促销活动期间的商品详情页加载过程,选用“令牌桶”策略往往更加合适因为这能兼顾性能优化的同时保障用户体验不受太大影响; - 而在网络通信领域内涉及到实时性强且对延时敏感的任务时(例如VoIP通话),为了确保音视频质量稳定可靠,则更倾向于依赖“桶”来进行精准的时间间隔调整以防止抖动现象发生。 ```python import time from threading import Thread, Lock class TokenBucket: def __init__(self, rate_limit, bucket_size): self.rate_limit = rate_limit # 每秒产生的token数目 self.bucket_size = bucket_size # token桶的最大容量 self.tokens = min(bucket_size, rate_limit * (time.time() % 1)) self.lock = Lock() def consume(self, tokens_needed=1): with self.lock: now = int(time.time()) if (now > self.last_time and self.tokens < self.bucket_size): elapsed = now - self.last_time new_tokens = elapsed * self.rate_limit self.tokens = min(self.bucket_size, self.tokens + new_tokens) self.last_time = now if self.tokens >= tokens_needed: self.tokens -= tokens_needed return True return False def leaky_bucket(data_rate, max_queue_length): queue = [] while True: data_incoming = yield from get_data() if len(queue) < max_queue_length: queue.append(data_incoming) processed_item = None if queue: processed_item = queue.pop(0) send_to_next_hop(processed_item) time.sleep(1 / data_rate) # Note: The above code snippets are simplified examples to illustrate the concepts. ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值