Java干货丨限流常规设计和实例( 二 )

以上实现使用了链表的特性 , 在一定窗口时是将前段删除后段加上的方式移动的 。移动的操作是请求进入时操作的 , 没有使用单独线程移动窗口 。并且按照前面讲的 , 两个维度控制流量一个时窗口的总请求数 , 一个是每个单独窗口的请求数 。

令牌桶
原理如图:

Java干货丨限流常规设计和实例

文章插图
 
目前使用令牌桶实现的限流有以下几个:
  • Spring Cloud Gateway RateLimiter
  • Guava RateLimiter
  • Bucket4j
Spring Cloud Gateway RateLimiter
zuul2跳票后Spring Cloud 自己开发的网关 , 内部也实现了限流器
Spring Cloud Gateway RedisRateLimiter实现原理
因为是微服务架构 , 多服务部署是必然场景 , 所以默认提供了redis为存储载体的实现 , 所以直接看rua脚本是怎么样的就可以知道它的算法是怎么实现的了:
local tokens_key = KEYS[1]local timestamp_key = KEYS[2]--redis.log(redis.LOG_WARNING, "tokens_key " .. tokens_key)// 速率local rate = tonumber(ARGV[1])// 桶容量local capacity = tonumber(ARGV[2])// 发起请求时间local now = tonumber(ARGV[3])// 请求令牌数量 现在固定是1local requested = tonumber(ARGV[4])// 存满桶耗时local fill_time = capacity/rate// 过期时间local ttl = math.floor(fill_time*2)--redis.log(redis.LOG_WARNING, "rate " .. ARGV[1])--redis.log(redis.LOG_WARNING, "capacity " .. ARGV[2])--redis.log(redis.LOG_WARNING, "now " .. ARGV[3])--redis.log(redis.LOG_WARNING, "requested " .. ARGV[4])--redis.log(redis.LOG_WARNING, "filltime " .. fill_time)--redis.log(redis.LOG_WARNING, "ttl " .. ttl)// 上次请求的信息 存在redislocal last_tokens = tonumber(redis.call("get", tokens_key))// 可任务初始化桶是满的if last_tokens == nil then last_tokens = capacityend--redis.log(redis.LOG_WARNING, "last_tokens " .. last_tokens)// 上次请求的时间local last_refreshed = tonumber(redis.call("get", timestamp_key))if last_refreshed == nil then last_refreshed = 0end--redis.log(redis.LOG_WARNING, "last_refreshed " .. last_refreshed)// 现在距离上次请求时间的差值 也就是距离上次请求过去了多久local delta = math.max(0, now-last_refreshed)// 这个桶此时能提供的令牌是上次剩余的令牌数加上这次时间间隔按速率能产生的令牌数 最多不能超过片桶大小local filled_tokens = math.min(capacity, last_tokens+(delta*rate))// 能提供的令牌和要求的令牌比较local allowed = filled_tokens >= requestedlocal new_tokens = filled_tokenslocal allowed_num = 0if allowed then // 消耗调令牌 new_tokens = filled_tokens - requested allowed_num = 1end--redis.log(redis.LOG_WARNING, "delta " .. delta)--redis.log(redis.LOG_WARNING, "filled_tokens " .. filled_tokens)--redis.log(redis.LOG_WARNING, "allowed_num " .. allowed_num)--redis.log(redis.LOG_WARNING, "new_tokens " .. new_tokens)// 存储剩余的令牌redis.call("setex", tokens_key, ttl, new_tokens)redis.call("setex", timestamp_key, ttl, now)return { allowed_num, new_tokens }Spring Cloud Gateway RedisRateLimiter总结
1 , 在请求令牌时计算上次请求和此刻的时间间隔能产生的令牌 , 来刷新桶中的令牌数 , 然后将令牌提供出去 。
2 , 如果令牌不足没有等待 , 直接返回 。
3 , 实现的方式是将每个请求的时间间隔为最小单位 , 只要小于这个单位的请求就会拒绝 , 比如100qps的配置 , 1允许10ms一个 , 如果两个请求比较靠近小于10ms , 后一个会被拒绝 。
Guava RateLimiter
一个Guava实现的令牌桶限流器 。
Guava RateLimiter类关系
Java干货丨限流常规设计和实例

文章插图
 
Guava RateLimiter使用
RateLimiter就是一个根据配置的rate分发permits的工具 。每一次调用acquire()都会阻塞到获得permit为止 , 一个permit只能用一次 。RateLimiter是并发安全的 , 它将限制全部线程的全部请求速率 , 但是RateLimiter并不保证是公平的 。
限速器经常被使用在限制访问物理或逻辑资源的速率 , 和JAVA.util.concurrent.Semaphore对比 , Semaphore用于限制同时访问的数量 , 而它限制的是访问的速率(两者有一定关联:就是little's law(律特法则)) 。
RateLimiter主要由发放许可证的速度来定义 。如果没有其他配置 , 许可证将以固定的速率分发 , 以每秒许可证的形式定义 。许可证将平稳分发 , 各许可证之间的延迟将被调整到配置的速率 。(SmoothBursty)


推荐阅读