分布式限流——Redis实现令牌桶算法

令牌桶算法

令牌桶算法(Token Bucket Algorithm)是一种广泛使用的流量控制(流量整形)和速率限制算法。这个算法能够控制网络数据的传输速率,确保数据传输的平滑性,防止网络拥堵,同时也被应用于软件系统中限制请求的速率,如API限流等场景。

工作原理

  1. 令牌的生成:系统以固定的速率向令牌桶中添加令牌(token),直到桶满为止。桶的容量(即最大令牌数)限制了短时间内可以发送的最大数据量。

  2. 请求的处理:每个传入的请求需要从桶中取出一定数量的令牌。如果桶中有足够的令牌,请求就被处理,消耗掉相应数量的令牌;如果桶中令牌不足,请求则根据策略(如排队、丢弃或延迟处理)来处理。

  3. 灵活控制:通过调整令牌的生成速率和桶的容量,可以灵活地控制数据传输的平均速率和允许的突发速率。

漏桶算法

漏桶算法(Leaky Bucket Algorithm)是另一种流量控制算法,用于确保数据传输以稳定的速率进行,减少或消除突发流量。与令牌桶算法相比,漏桶算法提供了一种更为严格的速率限制方式。

漏桶算法的工作原理

  1. 数据流入:数据(如网络包、API请求等)以任意速率流入漏桶。
  2. 恒定速率流出:数据以恒定的速率从桶中"漏出"(被处理)。如果桶满了(达到其容量),则新进入的数据会被丢弃或排队等待。
  3. 稳定输出:无论输入数据的速率如何变化,输出数据的速率保持不变,由漏桶的"漏洞"大小决定。

令牌桶算法与漏桶算法的主要区别

  • 速率控制灵活性:令牌桶算法允许一定程度的突发流量,因为如果桶中有足够的令牌,突发的请求可以立即被处理。而漏桶算法则以恒定的速率处理请求,输入速率超过这个恒定值的部分将被限制或丢弃,因此不允许突发流量。
  • 应对突发流量:令牌桶算法能够更好地应对突发流量。当桶中有足够令牌时,可以一次性处理较多的请求。漏桶算法则始终以固定的速率处理流入的请求,突发流量只能在桶内等待,直到它们逐渐被处理。
  • 应用场景:漏桶算法适用于需要严格控制数据传输速率的场景,确保数据处理的平稳性;令牌桶算法则更适用于需要一定程度突发性处理能力的场景,例如网络带宽控制和API限流。

总结

  • 漏桶算法通过以固定的速率"漏出"数据来控制数据的传输速率,适用于对输出速率有严格要求的场景。
  • 令牌桶算法允许在桶中累积令牌,从而应对突发流量,适用于需要较大灵活性的速率限制场景。

Redis实现令牌桶算法限流

Redis 实现令牌桶算法限流是一种非常有效的限流策略,适用于分布式系统中的接口限流。令牌桶算法的核心思想是以恒定的速率向桶中添加令牌,每个请求需要消耗一定数量的令牌才能被执行。如果桶中令牌不足,则拒绝服务。

在 Redis 中实现令牌桶算法通常可以通过 Lua 脚本来保证操作的原子性,以下是一个基于 Redis 的令牌桶限流实现示例:

Lua 复制代码
-- Lua 脚本实现令牌桶算法
-- key 为 Redis 中用于存储桶的键
-- rate 为令牌填充速率(每秒填充的令牌数)
-- capacity 为桶的容量
-- now 为当前时间戳
-- permits 为本次请求需要的令牌数

local key = KEYS[1]
local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local permits = tonumber(ARGV[4])

local bucket = redis.call('hmget', key, 'lastRefillTime', 'tokens')
local lastRefillTime = tonumber(bucket[1])
local tokens = tonumber(bucket[2])

if lastRefillTime == nil then
  lastRefillTime = now
  tokens = capacity
end

-- 计算自上次填充以来经过的时间
local delta = math.max(0, now - lastRefillTime)
-- 计算应该填充的令牌数
local refillTokens = math.floor(delta * rate)
tokens = math.min(capacity, tokens + refillTokens)
lastRefillTime = now

local enoughTokens = false
if tokens >= permits then
  enoughTokens = true
  tokens = tokens - permits
end

-- 更新桶的状态
redis.call('hmset', key, 'lastRefillTime', lastRefillTime, 'tokens', tokens)

-- 设置过期时间防止无限增长
redis.call('expire', key, math.ceil(capacity/rate)*2)

if enoughTokens then
  return 1
else
  return 0
end

具体的 key-value 结构

在这个场景中,hash 的键(key)是用来唯一标识一个令牌桶的,而这个 hash 包含了两个字段(field),分别存储了令牌桶的两个重要属性:

  1. lastRefillTime:最后一次填充令牌的时间戳。
  2. tokens:当前桶中的令牌数量。

具体的 key-value 结构如下所示:

  • Key :令牌桶的唯一标识符,例如 "my_bucket_key",根据具体业务标识有所不同
  • Value :是一个 hash 结构,包含以下字段:
    • lastRefillTime:桶最后一次填充令牌的时间戳,用来计算距离上次填充令牌过去了多少时间,以及这段时间内应该填充多少新的令牌。
    • tokens:表示当前桶内剩余的令牌数量。

为什么用Hash

在 Redis 中使用 hash 数据结构来实现令牌桶算法的原因主要包括以下几点:

  1. 空间效率 :使用 hash 可以将与令牌桶相关的多个属性(如 lastRefillTimetokens)存储在同一个键下。这比为每个属性分别使用一个键要节省空间,因为 Redis 的每个键都会有额外的内存开销。

  2. 原子操作 :Redis 提供了对 hash 类型的一系列原子操作命令,如 HSET, HGET, HMSET, HMGET 等。这意味着可以在一个命令中更新令牌桶的多个属性,而无需担心并发访问导致的数据不一致问题。

  3. 性能优势 :相对于单独存储每个属性,hash 数据结构减少了存储开销和访问时间。对于频繁更新和查询的令牌桶状态信息,这种优势尤为重要。

  4. 方便管理 :使用 hash 数据结构可以将所有与令牌桶相关的信息组织在一起,这使得管理(如查看、更新和删除等操作)更为方便。

  5. 扩展性 :如果将来需要在令牌桶中添加更多的属性(例如,添加一个 rate 字段来存储令牌填充速率),使用 hash 结构可以很容易地进行扩展,而无需改变现有的数据结构或逻辑。

  6. 利用 Lua 脚本进行复杂操作 :通过在 Redis 上运行 Lua 脚本,可以在服务器端直接执行复杂的逻辑(如计算新的令牌数量和更新最后填充时间),而不需要在客户端和服务器之间进行多次通信。hash 数据结构支持通过一个命令对多个字段进行操作,这与 Lua 脚本的使用非常契合。

相关推荐
考虑考虑2 小时前
Jpa使用union all
java·spring boot·后端
用户3721574261352 小时前
Java 实现 Excel 与 TXT 文本高效互转
java
浮游本尊3 小时前
Java学习第22天 - 云原生与容器化
java
佛祖让我来巡山4 小时前
深入理解JVM内存分配机制:大对象处理、年龄判定与空间担保
jvm·内存分配·大对象处理·空间担保·年龄判定
渣哥5 小时前
原来 Java 里线程安全集合有这么多种
java
间彧5 小时前
Spring Boot集成Spring Security完整指南
java
间彧5 小时前
Spring Secutiy基本原理及工作流程
java
Java水解6 小时前
JAVA经典面试题附答案(持续更新版)
java·后端·面试
洛小豆9 小时前
在Java中,Integer.parseInt和Integer.valueOf有什么区别
java·后端·面试
前端小张同学9 小时前
服务器上如何搭建jenkins 服务CI/CD😎😎
java·后端