使用 Redis + Lua 实现分布式限流

在线工具站
  • 推荐一个程序员在线工具站:程序员常用工具http://cxytools.com),有时间戳、JSON格式化、文本对比、HASH生成、UUID生成等常用工具,效率加倍嘎嘎好用。
程序员资料站
小报童专栏精选Top100
  • 推荐一个小报童专栏导航站:小报童精选Top100http://xbt100.top),收录了生财有术项目精选、AI海外赚钱、纯银的产品分析等专栏,陆续会收录更多的专栏,欢迎体验~

分布式系统中,限流是保证系统稳定性和可用性的重要措施之一。通过限流,可以避免流量突增对系统带来的冲击,保证系统的高可用性。Redis 作为一个高性能的内存数据库,不仅可以用来缓存数据,还可以用来实现分布式限流。而通过结合 Lua 脚本,可以更高效地实现复杂的限流逻辑。本文将介绍如何使用 Redis 和 Lua 脚本实现分布式限流。

为什么选择 Redis + Lua 实现限流

Redis 是一个高性能的键值存储系统,具有以下特点使其成为实现分布式限流的理想选择:

  1. 高性能:Redis 以内存存储为基础,读写速度极快,能够处理高并发请求。
  2. 丰富的数据结构:Redis 提供了多种数据结构,如字符串、列表、集合、哈希等,便于灵活实现各种限流策略。
  3. 持久化支持:Redis 支持数据持久化,可以在重启后恢复数据。
  4. Lua 脚本支持:Redis 内置 Lua 脚本引擎,可以在一次请求中执行一系列操作,保证原子性,避免并发问题。

限流策略介绍

常见的限流策略包括以下几种:

  1. 固定窗口限流:在固定时间窗口内限制请求数量,超过限制则拒绝请求。
  2. 滑动窗口限流:相对于固定窗口,滑动窗口限流更加精细化,可以更平滑地限制请求。
  3. 令牌桶算法:以固定速率生成令牌,请求需要消耗令牌,没有令牌则拒绝请求。
  4. 漏桶算法:以固定速率处理请求,超过速率的请求则排队等待。

本文将以固定窗口限流和令牌桶算法为例,介绍如何使用 Redis + Lua 实现分布式限流。

实现固定窗口限流

固定窗口限流是一种简单的限流策略,在固定时间窗口内限制请求数量。可以通过 Redis 的 INCREXPIRE 命令配合 Lua 脚本实现。

Lua 脚本实现

lua 复制代码
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local current = tonumber(redis.call('GET', key) or "0")

if current + 1 > limit then
    return 0
else
    redis.call("INCRBY", key, 1)
    redis.call("EXPIRE", key, ARGV[2])
    return 1
end

使用示例

假设限流策略为每分钟最多允许 100 次请求:

python 复制代码
import redis

client = redis.StrictRedis(host='localhost', port=6379, db=0)

# Lua script for fixed window rate limiting
lua_script = """
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local current = tonumber(redis.call('GET', key) or "0")

if current + 1 > limit then
    return 0
else
    redis.call("INCRBY", key, 1)
    redis.call("EXPIRE", key, ARGV[2])
    return 1
end
"""

rate_limit_script = client.register_script(lua_script)

key = "rate_limit:fixed_window"
limit = 100
expiry = 60  # time window in seconds

result = rate_limit_script(keys=[key], args=[limit, expiry])
if result == 1:
    print("Request allowed")
else:
    print("Request rate-limited")

实现令牌桶算法限流

令牌桶算法是另一种常见的限流策略,可以较好地平衡突发流量和长期流量。通过定期向桶中添加令牌,请求时消耗令牌,没有令牌则拒绝请求。

Lua 脚本实现

lua 复制代码
local key = KEYS[1]
local rate = tonumber(ARGV[1])
local burst = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])

local fill_time = burst / rate
local ttl = math.floor(fill_time * 2)
local last_tokens = tonumber(redis.call("GET", key .. ":tokens"))
if last_tokens == nil then
    last_tokens = burst
end
local last_refreshed = tonumber(redis.call("GET", key .. ":timestamp"))
if last_refreshed == nil then
    last_refreshed = 0
end

local delta = math.max(0, now - last_refreshed)
local filled_tokens = math.min(burst, last_tokens + (delta * rate))
local allowed = filled_tokens >= requested
local new_tokens = filled_tokens
if allowed then
    new_tokens = filled_tokens - requested
end

redis.call("SETEX", key .. ":tokens", ttl, new_tokens)
redis.call("SETEX", key .. ":timestamp", ttl, now)

return allowed and 1 or 0

使用示例

假设限流策略为每秒生成 10 个令牌,桶容量为 20:

python 复制代码
import redis
import time

client = redis.StrictRedis(host='localhost', port=6379, db=0)

# Lua script for token bucket rate limiting
lua_script = """
local key = KEYS[1]
local rate = tonumber(ARGV[1])
local burst = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])

local fill_time = burst / rate
local ttl = math.floor(fill_time * 2)
local last_tokens = tonumber(redis.call("GET", key .. ":tokens"))
if last_tokens == nil then
    last_tokens = burst
end
local last_refreshed = tonumber(redis.call("GET", key .. ":timestamp"))
if last_refreshed == nil then
    last_refreshed = 0
end

local delta = math.max(0, now - last_refreshed)
local filled_tokens = math.min(burst, last_tokens + (delta * rate))
local allowed = filled_tokens >= requested
local new_tokens = filled_tokens
if allowed then
    new_tokens = filled_tokens - requested
end

redis.call("SETEX", key .. ":tokens", ttl, new_tokens)
redis.call("SETEX", key .. ":timestamp", ttl, now)

return allowed and 1 or 0
"""

token_bucket_script = client.register_script(lua_script)

key = "rate_limit:token_bucket"
rate = 10  # tokens per second
burst = 20  # bucket capacity
now = int(time.time())
requested = 1

result = token_bucket_script(keys=[key], args=[rate, burst, now, requested])
if result == 1:
    print("Request allowed")
else:
    print("Request rate-limited")

优化和扩展

监控与报警

为了及时发现限流策略的异常情况,可以结合监控工具如 Prometheus 和 Grafana,对 Redis 的限流情况进行监控和报警。通过定期统计限流命中次数和请求数量,可以分析限流效果,并及时调整限流策略。

动态调整限流策略

在实际应用中,可能需要根据流量情况动态调整限流策略。可以结合 Redis 的 Pub/Sub 功能,通过发布和订阅消息来实时调整限流参数。

集群环境中的限流

在 Redis 集群环境中,可以将限流脚本分发到各个节点,确保限流逻辑在各个节点上执行。可以使用 Redis 的主从复制和分片机制,实现全局的分布式限流。

总结

通过结合 Redis 和 Lua 脚本,可以高效实现多种限流策略,如固定窗口限流和令牌桶算法。Redis 的高性能和丰富功能使其成为实现分布式限流的理想选择。

相关推荐
痞老板A小安装C413 分钟前
redis的大key和热key问题解决方案
数据库·redis·bootstrap
飞升不如收破烂~13 分钟前
Redis的String类型和Java中的String类在底层数据结构上有一些异同点
java·数据结构·redis
feilieren16 分钟前
DataGrip 连接 Redis、TongRDS
数据库·redis·缓存
液态不合群19 分钟前
Redis中常见的数据类型及其应用场景
数据库·redis·wpf
孙克旭_20 分钟前
第三章 分布式缓存Redis
redis·分布式·缓存
Allen Bright20 分钟前
Jedis存储一个-以String的形式的对象到Redis
数据库·redis·缓存
Allen Bright23 分钟前
Jedis连接池的操作
java·redis
Allen Bright2 小时前
Jedis存储一个以byte[]的形式的对象到Redis
数据库·redis·缓存
NiNg_1_2342 小时前
Redis中的zset用法详解
数据库·redis·缓存
jlting1954 小时前
《智慧教育实时数据分析推荐项目》详细分析
大数据·redis·sql·kafka·database