在掌握了分布式 Session 之后,我们进入 Redis 的另一种经典应用:计数器与限流。访问计数(如阅读量、点赞数)需要在高并发下保证「加一」的原子性与一致性;接口限流则需要在分布式环境下按时间窗口或速率限制请求量,保护下游与系统稳定性。二者都依赖 Redis 的原子操作与数据结构能力;本讲将围绕访问计数、接口限流与滑动窗口思想展开,帮助你在工程中正确使用 Redis 做计数与限流。
访问计数的典型需求是「某资源被访问一次,计数加一」:文章阅读量、视频播放次数、点赞数、分享数等。在单机环境下,可以用内存变量自增;但在多实例、高并发下,若每个实例各自计数再汇总,会存在并发与一致性问题,且汇总延迟与复杂度都会上升。Redis 的 INCR 命令提供了原子自增:将 Key 对应的整数值加一,若 Key 不存在则先初始化为 0 再加一;该操作在 Redis 内单线程执行,天然原子,无需应用层加锁。因此,只需为每个计数对象分配一个 Key(如 article:42:views、video:100:plays),每次访问时执行一次 INCR,即可在分布式环境下得到一致且准确的计数。
与 INCR 对应的是 DECR:将整数值减一,常用于「可撤销」的计数,如点赞后取消点赞、收藏后取消收藏。此时 Key 的设计可以是「资源维度」的计数(如 article:42:likes),每次点赞 INCR、取消点赞 DECR;也可以配合 Set 记录「谁已点赞」,避免重复计数,计数取 Set 的 SCARD。若同一资源有多个计数维度(如阅读量、点赞数、收藏数),可以用 Hash 将多个计数值放在同一个 Key 下:例如 article:42:counts,field 为 views、likes、collects,使用 HINCRBY 分别自增,既减少 Key 数量,也便于批量读取。
|INCR article:42:views DECR article:42:likes HINCRBY article:42:counts views 1 HINCRBY article:42:counts likes 1

与此密切相关的是计数的持久化与冷热分离。若计数只存 Redis,实例重启或故障会丢失(除非开启 AOF/RDB);对于「允许短暂误差」的展示型计数,可以定期将 Redis 中的计数值同步到数据库,或由数据库作为底表、Redis 作为增量缓存,定时合并。对于「强一致」的计数(如库存、余额),则不能仅依赖 Redis 计数,需配合数据库事务或分布式锁,Redis 在此处更多用于「限流」「防重」等辅助能力,计数本身以数据库为准。
接口限流的目的是在分布式环境下按时间或速率限制请求量,防止单用户、单 IP 或全局限流过高导致下游被打挂或系统过载。限流通常需要「在某一时间窗口内,请求数不超过 N」或「每秒/每分钟不超过 M 个请求」的语义;实现方式有多种,常见的有固定窗口、滑动窗口、令牌桶、漏桶等,Redis 常被用作限流计数的存储后端,保证多实例共享同一份「已请求数」或「剩余令牌数」。
固定窗口限流将时间划分为等长区间(如每分钟一个窗口),每个窗口内单独计数;例如 Key 为 ratelimit:user:10001:minute,Value 为当前分钟内的请求次数,每次请求时 INCR,若 INCR 结果为 1 则同时 EXPIRE 60,若结果大于阈值则拒绝。实现简单,但存在边界问题:相邻两个窗口的交界处,例如第 59 秒和第 61 秒各来 100 个请求,则两秒内实际通过了 200 个请求,而单窗口限制可能是 100,导致限流在边界处「漏过」流量。因此固定窗口适合对边界突增不敏感的场景,或作为第一层粗限流。
|INCR ratelimit:user:10001:60 EXPIRE ratelimit:user:10001:60 60
滑动窗口限流不按固定区间计数,而是「以当前时刻为右边界、向前推一个窗口长度」的时间段内计数。例如窗口长度为 60 秒,则任意时刻的计数都是「过去 60 秒内的请求数」;新请求到来时,先移除窗口外的旧记录,再统计窗口内数量,若未超限则加入当前请求。这样,无论请求如何分布,任意连续 60 秒内的请求数都不会超过阈值,避免了固定窗口的边界突增问题。实现上可以用 Sorted Set:将每次请求的时间戳作为 score,member 可为时间戳或唯一 ID;每次请求时用 ZREMRANGEBYSCORE 删除窗口外的记录,再用 ZCARD 统计窗口内数量,若小于阈值则 ZADD 当前请求并允许通过,否则拒绝。为保证原子性,通常将「删旧记录、统计、加新记录」放在同一 Lua 脚本中执行。

滑动窗口的核心思想是用「当前时刻向前推一段长度」作为有效区间,而不是固定的日历区间。在限流场景下,这段长度就是「窗口大小」(如 60 秒),有效区间内的请求数就是「当前窗口内的计数」;每次新请求到来时,窗口的右边界更新为当前时刻,左边界为「当前时刻减窗口大小」,落在左边界之前的记录被视为过期并移除,只统计区间内的记录数。这样,限流语义变为「任意连续窗口长度内,请求数不超过 N」,比固定窗口更平滑、更符合「速率」的直觉。
用 Redis 实现滑动窗口限流时,Sorted Set 的 score 存时间戳(毫秒或秒),member 存请求标识(时间戳或 UUID,用于去重);Key 可按「用户/IP/接口」区分,例如 ratelimit:api:user:10001:60。流程为:先 ZREMRANGEBYSCORE 删除 score 小于「当前时间减窗口大小」的成员,再 ZCARD 得到当前窗口内数量,若小于阈值则 ZADD 当前请求(score 为当前时间戳,member 为唯一值),并设置 Key 的 EXPIRE 避免冷 Key 长期占用内存;若大于等于阈值则拒绝。上述步骤需在 Lua 中原子执行,否则并发请求可能同时通过检查后都执行 ZADD,导致实际超过限制。
|local key = KEYS[1] local now = tonumber(ARGV[1]) local window = tonumber(ARGV[2]) local limit = tonumber(ARGV[3]) redis.call('ZREMRANGEBYSCORE', key, 0, now - window) local count = redis.call(

综合来看,访问计数依赖 Redis 的 INCR/DECR 或 HINCRBY 的原子性,在分布式环境下实现一致计数;接口限流则依赖「时间窗口内的计数」或「令牌/漏桶」等算法,Redis 作为共享存储保证多实例限流一致。滑动窗口通过「以当前时刻为右边界向前推窗口」的计数方式,避免了固定窗口的边界突增问题,用 Sorted Set + Lua 即可在 Redis 上实现精确、平滑的分布式限流。在下一讲中,我们将进入 Redis 的另一种经典应用:排行榜与排序,届时会深入 Sorted Set 的威力、实时排行榜的设计以及分数、权重与时间的建模。