分布式限流——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 脚本的使用非常契合。

相关推荐
暗黑起源喵3 分钟前
设计模式-工厂设计模式
java·开发语言·设计模式
WaaTong7 分钟前
Java反射
java·开发语言·反射
Troc_wangpeng9 分钟前
R language 关于二维平面直角坐标系的制作
开发语言·机器学习
努力的家伙是不讨厌的10 分钟前
解析json导出csv或者直接入库
开发语言·python·json
ketil2713 分钟前
Ubuntu 安装 redis
redis
Envyᥫᩣ24 分钟前
C#语言:从入门到精通
开发语言·c#
九圣残炎41 分钟前
【从零开始的LeetCode-算法】1456. 定长子串中元音的最大数目
java·算法·leetcode
wclass-zhengge43 分钟前
Netty篇(入门编程)
java·linux·服务器
童先生1 小时前
Go 项目中实现类似 Java Shiro 的权限控制中间件?
开发语言·go
lulu_gh_yu1 小时前
数据结构之排序补充
c语言·开发语言·数据结构·c++·学习·算法·排序算法