本文最初写于2017年,2026年根据当前Redis文档重新整理。
限流看起来只是计数,更需要注意的是多条命令之间的边界。假设应用先判断键是否存在,再执行自增和设置过期时间,并发请求就可能在这些步骤之间交错;如果客户端在自增后、设置过期时间前异常退出,还可能留下一个不会自动清理的键。
Redis Lua的价值,是把这段逻辑放到服务端一次执行。脚本执行期间具有原子性,其他命令不会插入其中。
一个固定窗口计数器
下面的脚本接收一个限流键、窗口秒数和最大请求数:
| |
调用约定:
KEYS[1]:限流维度,例如用户或接口对应的键;ARGV[1]:窗口时长,单位为秒;ARGV[2]:窗口内允许的最大次数;- 返回
1表示允许,返回0表示拒绝。
第一次自增后立即设置过期时间,整个过程在同一次脚本执行中完成。它减少了客户端与Redis之间的往返,也避免了多条命令被并发请求穿插执行。
原子不等于没有代价
Redis执行脚本时会阻塞其他服务端活动,因此脚本应保持短小、确定,并避免大范围扫描或耗时计算。把复杂业务全部塞进Lua,只是把应用层的复杂度转移到了一个更敏感的位置。
固定窗口本身也有边界效应:两个相邻窗口交界处可能在很短时间内通过接近两倍的请求。如果业务不能接受,需要改用滑动窗口、令牌桶等策略,而不是继续扩充这段脚本。
脚本的加载方式
频繁调用时,可以先通过SCRIPT LOAD加载脚本,再使用EVALSHA按摘要执行。但脚本缓存并不持久,重启、故障转移或SCRIPT FLUSH之后都可能丢失,客户端仍需处理重新加载。
Redis 7开始提供Functions,适合需要随数据持久化和复制的服务端逻辑。无论选择Eval脚本还是Functions,都更适合用于确实需要数据局部性和原子组合的场景。
这个例子的重点不是“如何写Lua”,而是如何把一个存在并发窗口的客户端流程,收拢成边界清楚的服务端操作。