为什么选择 Redis Streams 和 Lua?动态二人组详解
在我们深入代码之前,先来看看为什么这个组合被称为限流的“蝙蝠侠和罗宾”:
- Redis Streams:可以将其视为一个功能强大的消息队列,具备时间旅行的能力。
- Lua 脚本:Redis 的多功能工具,让你可以原子性地执行复杂逻辑。
它们就像花生酱和果冻的组合,如果花生酱能每秒处理数百万请求,而果冻能执行原子操作,那就更完美了。
蓝图:构建我们的自定义限流器
计划如下:
- 使用 Redis Streams 记录传入请求。
- 用 Lua 脚本实现滑动窗口算法。
- 根据服务器负载动态调整速率。
步骤 1:使用 Redis Streams 记录请求
首先,我们设置一个流来记录传入的请求:
import redis
r = redis.Redis(host='localhost', port=6379, db=0)
def log_request(user_id):
return r.xadd('requests', {'user_id': user_id})
很简单,对吧?我们只是将每个请求添加到名为 'requests' 的流中。流的美妙之处在于它们是按时间排序的,这对于我们的滑动窗口方法来说非常完美。
步骤 2:Lua 脚本 - 魔法发生的地方
现在,让我们编写一个 Lua 脚本,它将:
- 检查最近 X 秒内的请求数量
- 决定是否允许请求
- 清理旧条目
local function check_rate_limit(user_id, max_requests, window_size_ms)
local now = redis.call('TIME')
local now_ms = tonumber(now[1]) * 1000 + tonumber(now[2] / 1000)
local window_start = now_ms - window_size_ms
-- 移除旧条目
redis.call('XTRIM', 'requests', 'MINID', tostring(window_start))
-- 计算窗口内的请求数量
local count = redis.call('XLEN', 'requests')
if count < max_requests then
-- 允许请求
redis.call('XADD', 'requests', '*', 'user_id', user_id)
return 1
else
-- 超过限流
return 0
end
end
return check_rate_limit(KEYS[1], tonumber(ARGV[1]), tonumber(ARGV[2]))
这个脚本做了很多工作:
- 计算当前时间(毫秒)
- 修剪流以保留最近的条目
- 计算当前窗口内的请求数量
- 决定是否允许请求
步骤 3:整合所有部分
现在,让我们将其封装在一个 Python 函数中:
lua_script = """
-- 我们上面的 Lua 脚本放在这里
"""
rate_limiter = r.register_script(lua_script)
def is_allowed(user_id, max_requests=100, window_size_ms=60000):
return bool(rate_limiter(keys=[user_id], args=[max_requests, window_size_ms]))
# 使用示例
if is_allowed('user123'):
print("请求被允许!")
else:
print("限流超出!")
提升:动态速率调整
但等等,还有更多!如果我们可以根据服务器负载调整限流呢?让我们在 Lua 脚本中添加一个变化:
-- 在我们的 Lua 脚本顶部添加
local server_load = tonumber(redis.call('GET', 'server_load') or "50")
local dynamic_max_requests = math.floor(max_requests * (100 - server_load) / 100)
-- 然后在逻辑中使用 dynamic_max_requests 而不是 max_requests
现在,我们根据存储在 Redis 中的 'server_load' 值调整限流。你可以根据实际的服务器指标定期更新这个值。
陷阱:可能出错的地方
在你急于将其投入生产之前,让我们谈谈一些潜在的问题:
- 内存使用:如果不正确修剪,流可能会占用大量内存。注意 Redis 的内存使用情况。
- 时钟偏差:如果在多台服务器上运行,请确保它们的时钟同步。
- Lua 脚本复杂性:记住,Lua 脚本会阻塞 Redis。保持它们简短明了。
总结:为什么这很重要
那么,为什么要费这么大劲而不是直接使用现成的解决方案呢?原因如下:
- 灵活性:你可以根据需要调整任何限流方案。
- 性能:这个设置可以处理大量的请求。
- 学习:从头开始构建可以让你深入理解限流概念。
而且,说实话,自己构建限流器听起来就很酷。
思考的食粮
"做伟大工作的唯一方法就是热爱你所做的。" - 史蒂夫·乔布斯
在我们结束这次自定义限流的旅程时,问问自己:还有哪些“标准”组件可以从自定义的、基于 Redis 的改造中受益?可能性是无穷的,只受限于你的想象力(也许还有你的 Redis 实例的内存)。
现在,去用风格限制速率吧!你的 API 会感谢你,谁知道呢,也许你会成为下次开发者聚会的热门话题。“哦,你在用现成的限流器?真可爱。”