Redis限流那些事儿,官方文档里没说全的细节和用法解析
- 问答
- 2025-12-27 20:06:55
- 6
Redis限流那些事儿,官方文档里没说全的细节和用法解析
提到用Redis做限流,大部分人第一个想到的可能是INCR命令,这个思路很简单:一个键(key)代表一个限流资源,每次请求过来,就给这个键的值加1,如果值超过了限制,就拒绝请求,这个方法很直观,但官方文档可不会告诉你,这里有个大坑:它不准确,为啥呢?因为Redis的命令是原子性的没错,但你的应用程序逻辑不是,想象一下这个场景:你先用GET命令去读取当前计数,判断还没超限,然后才发出INCR命令,就在你GET之后、INCR之前的那个微小间隙,另一个请求也可能完成了GET,也认为没超限,结果就是,你们两个都通过了检查,计数一下子加了2,可能就超过了限额,这叫“先读后写”的并发问题,单纯靠应用层组合GET和INCR是靠不住的。
那怎么办?官方文档会指引你用INCR和EXPIRE组合,但它可能没强调必须用Lua脚本来保证原子性,正确的姿势是,把整个逻辑写在一个Lua脚本里,脚本大概是这样的:先INCR键,如果返回的计数是1(说明是第一次访问),就同时给这个键设置一个过期时间(比如1分钟),如果计数已经大于1,就检查是否超限,整个脚本在Redis服务器端是单线程执行的,所以绝对不会出现上面说的那种并发问题,这是实现一个简单计数器的关键细节。
比简单计数器更常用的是滑动窗口限流,网上很多文章讲这个,但有个细节容易被忽略:内存占用和性能,一种经典实现是使用有序集合(ZSET),把每次请求的时间戳作为分数(score)存进去,每次请求来时,删除窗口时间(比如1分钟内)之前的旧数据,然后统计集合大小看是否超限,听着很完美,对吧?但问题来了,这个ZSET可能会一直变大,因为你是靠每次请求来“清理”过期数据的,如果长时间没有请求,这个集合就永远不会被清理,占着内存,更糟的是,在流量高峰时,每次请求都要执行一个ZREMRANGEBYSCORE命令来删除旧元素,如果这个集合很大,这个操作可能会比较耗时,影响Redis的性能,文档不会提醒你,这个方案在长期低流量或突发高流量场景下可能有风险。
那有没有更好的办法?有,就是使用多个时间片(桶),比如把1分钟的窗口分成6个10秒的桶,当前时间落在哪个桶,就给哪个桶计数,限流时,只统计最近6个桶(即1分钟)的计数总和,这样做的好处是,清理过期数据变得非常简单:每个桶自然过期即可,不需要昂贵的删除操作,缺点是精度有所下降,是10秒级别的,但通常可以接受,这种方法的实现细节在于如何优雅地管理和轮转这些桶,确保时间同步。
还有一个文档里不太会展开讲,但实践中很重要的点是:限流键(Key)的设计,你不能只用一个固定的键,比如rate_limit:,你必须把限流对象(比如用户ID、IP地址)和限流时间窗口都编码进去,比如rate_limit:user123:1667890200,后面的数字可能是一个整分钟或整小时的时间戳,这样设计,键会自动过期被清理,但这里有个小技巧,如果你用小时作为窗口,那么每个键会存在一小时,即使那个用户后来没有再请求,如果用户量巨大,可能会产生大量僵尸键,一种优化是使用更短的基础窗口(比如分钟),或者考虑使用哈希表(Hash)来存储多个用户的计数,但要小心哈希表的大Key问题。
关于“限流后怎么办”,文档通常只说“返回错误”,但实际业务中,你可能需要更灵活的策略,可以把被拒绝的请求放入一个延迟队列,稍后重试;或者给不同类型的请求设置不同的限流阈值,这些都需要在应用层结合Redis的命令进行二次开发,Redis只是提供了原子计数和过期这些基础能力,真正的限流策略需要根据业务场景精心设计,比如在Github的API限流中,他们会在响应头中告诉你还剩多少请求次数,何时重置,这种用户体验就很好,实现它就需要在Redis中存储重置时间点并返回给客户端。
Redis限流入门容易,但想把细节做好,避免坑,就需要理解其内部原理和在不同场景下的表现,从保证原子性,到选择合适的数据结构控制内存和性能,再到键的设计和过期策略,最后到结合业务设计友好的限流响应,每一步都有文档之外需要琢磨的地方。

本文由钊智敏于2025-12-27发表在笙亿网络策划,如有疑问,请联系我们。
本文链接:http://waw.haoid.cn/wenda/69606.html
