连载中 6/20

分布式限流:Redis 加 Lua 的原子计数

2026-07-08 · 2192 阅读 · 0 评论 · 0 赞

竞态跟着计数器一起搬家

上一篇的结论:要精确控制总量与每人配额,计数器必须集中到 Redis。但「读计数、判断、写计数」三个动作一旦分开执行,竞态就来了——十个实例同时读到 199,判断都通过,都放行,限额 200 放进 210。这套剧情读者在分布式锁系列第 2 篇已经见过(校验和删除分两步的坑),解法也一样:把判断与计数打包成 Lua 脚本,在 Redis 单线程里原子执行。

三个算法的 Redis 版

固定窗口版第 2 篇已给(INCR 加 PEXPIRE 打包),不再重复。滑动窗口版第 3 篇也给了(ZSET 加 ZREMRANGEBYSCORE),适合低频高价值接口。这里重点补令牌桶的分布式版——工程里最常用又最讲究的一个:

-- KEYS[1]=桶key  ARGV[1]=速率  ARGV[2]=桶容量  ARGV[3]=本次申请数
local time = redis.call("TIME")                 -- 用 Redis 的时钟!
local now = time[1] * 1000 + math.floor(time[2] / 1000)
local bucket = redis.call("HMGET", KEYS[1], "tokens", "ts")
local tokens = tonumber(bucket[1]) or ARGV[2]   -- 首次:满桶
local ts = tonumber(bucket[2]) or now
local delta = math.max(0, now - ts)             -- 距上次的时间差
tokens = math.min(tonumber(ARGV[2]), tokens + delta / 1000 * tonumber(ARGV[1]))
if tokens >= tonumber(ARGV[3]) then
    tokens = tokens - tonumber(ARGV[3])
    redis.call("HMSET", KEYS[1], "tokens", tokens, "ts", now)
    redis.call("PEXPIRE", KEYS[1], 60000)       -- 兜底过期
    return 1
end
redis.call("HMSET", KEYS[1], "tokens", tokens, "ts", now)
return 0

细节一:时钟必须统一

脚本第一行就是本篇最重要的细节:令牌补充按时间差计算,时间差的两端必须来自同一个时钟。如果应用传自己的 now,实例 A 的时钟快 2 秒、实例 B 的慢 1 秒,同一个桶的时间差算出来忽大忽小,令牌凭空多发或少发。用 Redis 的 TIME 命令取时间(Redis 3.2+ 允许脚本里调用),所有实例对齐到 Redis 的时钟——分布式系统里「逻辑时钟对齐」永远优于「信任各节点物理时钟」,RedLock 论战(分布式锁系列第 5 篇)讲的时钟漂移问题,在这里提前绕开了。

细节二:Redis 挂了,放行还是拒绝

限流依赖 Redis,Redis 不可用时怎么办?两个方向都有道理,按业务性质选:fail-open(放行)——限流器失效宁可超卖,也不能让全站跟着限流器一起瘫,适合保护自家资源的场景:超卖一点总好过全站不可用;fail-close(拒绝)——限不住就坚决不放,适合花钱的出口:短信、支付调第三方,刷穿就是真金白银。折中方案是本地兜底限流:Redis 挂了自动降级到单机 RateLimiter(阈值按总量除以实例数),可用性由单机限流接力——粗糙但保命。

细节三:高并发下的 RT 优化

每次请求一次 Redis 往返(约 1 毫秒),万级 QPS 下 Redis 网络成为新瓶颈,两个经典优化:批量取牌——一次 Lua 取 10 个令牌回本地,本地逐个消费,RT 降到十分之一,代价是突发粒度变粗(本实例可能囤牌);两层限流——本地先用宽松阈值(总量/实例数再加余量)拦截明显超标的,通过的才去 Redis 精确校验,九成的拒绝在本机完成,Redis 只受理边缘流量。

方案精度Redis 压力适用
固定窗口边界突刺低(两个命令)粗粒度总量控制
令牌桶速率精确,允许突发中(读写桶状态)通用默认,接口级限流
ZSET 滑动窗口零误差高(与请求量成正比)低频高价值接口

分布式限流的内核到此完整:原子脚本、统一时钟、故障策略、性能分层。但还有一个位置没安放——这些闸门该装在系统的哪一层?Nginx 前置在流量入口,网关聚合一揽子规则,应用内精细到方法——下一篇从最外层的网关讲起。

☕
503

10 年全栈工程师 · 503咖啡馆主理人

#分布式限流#Redis#Lua#令牌桶#时钟统一#fail-open

评论 (0)

热门推荐

连载中 11/22

主从搭建实操:从零配出一主两从

光讲原理不过瘾?手把手搭一主两从:my.cnf 六个参数、复制账号、GTID、CHANGE REPLICATION SOURCE TO、SHOW REPLICA STATUS 验收,附翻车排查清单。

#MySQL#主从复制#GTID#主从搭建#高可用
2026-05-07 · 10101 阅读 · 0 评论 · 0 赞
连载中 16/22

连接池:HikariCP 参数与连接风暴

连接池不是越大越好:8 核机器配 1000 连接反而更慢的数学原理,HikariCP 四个必调参数,maxLifetime 与 wait_timeout 的隐形陷阱。

#MySQL#连接池#HikariCP#maxLifetime#连接风暴
2026-05-10 · 9872 阅读 · 0 评论 · 0 赞
连载中 4/16

缓存穿透:恶意 ID 打穿 MySQL 的四道防线

请求的数据在缓存和数据库里都不存在时,缓存形同虚设。聊聊参数校验、空值缓存、布隆过滤器、限流熔断四道防线的原理与组合打法。

#Redis#缓存穿透#布隆过滤器#高可用
2026-05-16 · 9293 阅读 · 21 评论 · 287 赞