跳转到主内容
websoft网络软件专家 - 深耕网络技术,打造实用软件!

如何在Golang中实现基于Redis的分布式限流 Go语言Lua脚本令牌桶

不能直接用Go原生Redis客户端做原子限流,因为GET+INCR+EXPIRE三步非原子,易被并发插队导致超发;必须用Lua脚本将令牌桶逻辑(校准时间、补充令牌、判断放行)封装为单次原子操作。 为什么不能直接用 Go 原生 Redis 客户端做原子限流 因为
GET
+
INCR
+
EXPIRE
三步在并发下不是原子的,中间可能被其他请求插队,导致超发。Redis 单线程执行 Lua 脚本才是唯一靠谱的原子边界。 常见错误现象:
rate limit exceeded
报错不稳定,压测时偶尔放行超额请求;或者反过来,同一用户连续请求被误拦截。 必须把令牌桶逻辑(计算剩余令牌、更新时间戳、判断是否允许)全部写进一个 Lua 脚本里 Go 端只负责传参(key、capacity、rate、window)和接收返回值(0 或 1) 不要在 Go 里做任何“先查再判再写”的分支逻辑 怎么写一个安全的令牌桶 Lua 脚本 脚本要解决三个问题:时间漂移校准、令牌动态补充、过期自动重建。别抄网上简单版
DECR
脚本——它不处理漏桶/令牌补充,也不兼容 Redis 集群模式下的 key hash tag。 示例脚本核心逻辑(传入参数:
KEYS[1]
是限流 key,
ARGV[1]
是容量,
ARGV[2]
是每秒补充令牌数,
ARGV[3]
是窗口秒数): 立即学习 “ go语言免费学习笔记(深入) ”;
local key = KEYS[1] local capacity = tonumber(ARGV[1]) local rate = tonumber(ARGV[2]) local window = tonumber(ARGV[3])

local now = tonumber(redis.call('TIME')[1]) local bucket = redis.call('HMGET', key, 'last_time', 'tokens') local last_time = tonumber(bucket[1]) or now local tokens = math.min(capacity, tonumber(bucket[2]) or capacity)

-- 补充令牌:按时间差 × rate,但不超过 capacity local delta = math.max(0, now - last_time) local new_tokens = math.min(capacity, tokens + delta * rate)

-- 判断是否允许 if new_tokens >= 1 then redis.call('HMSET', key, 'last_time', now, 'tokens', new_tokens - 1) redis.call('EXPIRE', key, window) return 1 else return 0 end

用
HMGET
/
HMSET
而不是
GET
/
SET
,避免 key 冲突和字段扩展困难
redis.call('TIME')
比客户端传时间更可靠,规避系统时钟不同步问题 必须加
EXPIRE
,否则冷 key 永久残留;且 expire 时间要覆盖整个窗口,不能只设几秒 Go 侧如何正确调用并处理返回值 别用
Eval
硬拼字符串,要用
Script.Load
预编译,提升性能并避免注入风险。返回值是整数 1 或 0,不是字符串。 go语言参考手册 中文CHM版 Go 是一个开源的编程语言,它能让构造简单、可靠且高效的软件变得容易。本文给大家带来Go参考手册,需要的可以来下载! Go是从2007年末由Robert Griesemer, Rob Pike, Ken Thompson主持开发,后来还加入了Ian Lance Taylor, Russ Cox等人,并最终于2009年11月开源,在2012年早些时候发布了Go 1稳定版本。现在Go的开发已经是完全开放的,并且拥有一个活跃的社区。 Go 语言特色 简洁、快速、安全 并行、有趣、开源 内存管理、v数组安全、编译 下载 典型错误:把
redis.Int(cmd.Result())
写成
redis.String(cmd.Result())
,导致类型断言 panic。 用
redis.NewScript
加载 Lua 脚本,复用 script 实例,不要每次新建 调用时用
script.Eval(ctx, rdb, []string{key}, capacity, rate, window)
,注意参数顺序和类型 检查
err
是否为
redis.Nil
(key 不存在)或真实网络错误,二者处理方式不同 如果 Redis 返回
(nil)
,说明脚本没执行完或被中断,应视为限流拒绝,不要重试 集群模式下 key 设计和性能陷阱 Redis Cluster 要求同一个 slot 处理所有相关操作,所以 key 必须带 hash tag,比如
rate_limit:{user_123}
,否则
HMGET
和
HMSET
可能落在不同节点上失败。 性能影响明显:每秒几千次限流调用时,Lua 脚本本身耗时约 0.1–0.3ms,但网络 RTT 占大头。别在关键路径上额外加
pipeline
包裹这个脚本——它本身已是原子单位。 key 名必须包含
{...}
hash tag,且 tag 内容要足够分散(比如用 user_id 而非固定字符串) 不要给每个接口单独建 key,按业务维度聚合(如
api:{/v1/pay}
),否则 key 数量爆炸 监控
redis_script_error_total
指标,Lua 报错通常意味着参数类型错或 key 结构损坏 真正难的是时间精度和分布式时钟偏差的平衡——
TIME
返回秒级,对毫秒级限流不够,但自己算又容易出错。生产环境建议 window ≥ 1 秒,避免在这种边界反复折腾。

相关文章