news 2026/10/5 5:58:55

秒杀系统接口防刷限流:基于令牌桶算法与分布式 Redis 的滑动窗口限流落地

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
秒杀系统接口防刷限流:基于令牌桶算法与分布式 Redis 的滑动窗口限流落地

秒杀系统接口防刷限流:基于令牌桶算法与分布式 Redis 的滑动窗口限流落地

在电商大促与秒杀系统的架构设计中,最前线的网关和核心下单接口往往面临着最严酷的考验。

大促开启的那一秒,不仅有大量真实用户的涌入,更有黑产工作室编写的高频自动化抢购脚本。瞬时并发请求可能从平时的几千 QPS 暴涨至数十万甚至百万 QPS。如果不做精准的防刷限流,下游的库存扣减服务、订单服务以及底层 MySQL 数据库会在毫秒级内被直接打穿,导致全链路雪崩。

然而,在很多初中级工程师的眼里,“限流”似乎只需要在内存里放一个计数器或者用 RedisINCR加上过期时间。但在高并发的真实生产场景中,简单的设计往往漏洞百出。今天我们结合大厂生产实践,拆解从单机防御到分布式集群落地的限流防刷闭环架构。


经典限流算法的致命缺陷与选型权衡

在动手写代码前,我们必须认清不同限流算法的边界:

1. 固定窗口算法(Fixed Window):临界突变陷阱

使用简单的计数器,每秒重置一次。这种算法存在致命的“窗口临界突刺”:假设限制为 100 QPS,攻击者如果在第 0.9 秒发起 100 个请求,在第 1.1 秒又发起 100 个请求。虽然两个 1 秒窗口内都各自满足限制,但在中间仅 0.2 秒的时间区间内,系统实际上承受了 200 个请求(瞬时 QPS 达到 1000),足以击垮下游。

2. 漏桶算法(Leaky Bucket):缺乏对突发流量的弹性

请求像水一样流入水桶,水桶底部以固定速率漏水。虽然漏水速率恒定能够极好地保护下游,但它的缺点是完全丧失了对合法“突发流量(Burst)”的弹性承载能力。在秒杀场景下,系统通常希望允许短时间内有一批合法的瞬时突发通过,漏桶算法会将这批正常流量无情截断。

3. 令牌桶算法(Token Bucket):生产环境的最优选择

系统以恒定速率向容量为 $B$ 的桶中放入令牌。请求到达时先尝试从桶中获取令牌:

  • 若桶中有足够令牌,则允许通过并扣减令牌;
  • 若桶中无令牌,则拒绝或排队。
    令牌桶既能将长期平均请求速率限制在设定的速率内,又允许在桶满时应对瞬时爆发的突发流量(最大可承受 $B$ 个突发请求),是目前工业界主流网关的首选模型。

防刷两层防线设计:单机前置平滑 + 分布式细粒度拦截

在大型微服务架构中,单纯依赖集中式 Redis 做全局限流存在网络延迟与 Redis 单点压力。最优的生产实践是分层漏斗架构:

客户端请求 (数十万 QPS) ↓ 【网关层 (Kong / Spring Cloud Gateway)】 - 单机 Guava RateLimiter 前置削峰 - 粗粒度过滤恶意洪峰,保护网络出口 ↓ (已过滤 80% 恶意高频流量) 【业务拦截层 (Spring AOP + 分布式 Redis)】 - 多维度限流防刷:按 UserID / ClientIP / DeviceFingerprint - 执行分布式滑动窗口或分布式令牌桶 Lua 脚本 ↓ (合法请求) 【核心秒杀服务 (扣库存 / 生成订单)】

方案落地一:基于 Redis ZSet 的滑动窗口防刷

针对黑产多 IP 变换刷单或者针对特定接口的滑动防刷,基于 Redis 有序集合(ZSet)的滑动窗口算法能够做到极度精确的毫秒级控制。

其核心思路是:将请求的绝对时间戳作为 ZSet 的 score 与 member,每次请求进来时:

  1. 移除窗口时间以外的旧数据(ZREMRANGEBYSCORE);
  2. 统计当前窗口内的有效请求总数(ZCARD);
  3. 若未超限,则添加当前时间戳(ZADD)并设置过期时间。

为了保证原子性,必须将其封装在 Lua 脚本中执行:

-- KEYS[1]: 限流键 (例如 limit:user_10086:order) -- ARGV[1]: 当前时间戳 (毫秒) -- ARGV[2]: 窗口大小 (毫秒,例如 1000) -- ARGV[3]: 窗口内最大允许请求数 (例如 5) local key = KEYS[1] local now = tonumber(ARGV[1]) local window = tonumber(ARGV[2]) local limit = tonumber(ARGV[3]) local clearBefore = now - window -- 1. 移除滑动窗口之外的过期请求 redis.call('ZREMRANGEBYSCORE', key, 0, clearBefore) -- 2. 统计当前窗口内已有请求数量 local currentRequests = redis.call('ZCARD', key) -- 3. 判断是否超过限额 if currentRequests < limit then -- 未超限,记录当前请求 redis.call('ZADD', key, now, now) -- 设置键的过期时间,防止冷门 key 长期占用内存 redis.call('PEXPIRE', key, window) return 1 -- 放行 else return 0 -- 拦截 end

生产踩坑预警:基于 ZSet 的滑动窗口虽然极其精准,但每个请求都会在 Redis 中写入一个 member。如果某个黑客用脚本在 1 秒内疯狂发起 10 万次请求,这个 ZSet 会瞬间膨胀为一个巨大的 BigKey,在执行ZREMRANGEBYSCORE时会引发 Redis 主线程毫秒级甚至秒级卡顿。因此,ZSet 滑动窗口只适用于小容量、低频次(如 60 秒限 10 次短信验证码)的防刷场景。


方案落地二:高性能无状态分布式令牌桶(生产推荐)

在数十万 QPS 的秒杀接口限流中,基于惰性计算的分布式令牌桶是真正的性能之王。

我们不需要在后台起定时器去周期性往 Redis 塞令牌,而是利用数学公式:当请求到来时,根据“当前时间戳”与“上一次补充令牌的时间戳”的时间差,动态计算出这段时间内应该新补充的令牌数量。

在 Redis 中,只需一个 Hash 结构维护两个字段:tokens(当前剩余令牌数)和last_updated(上次更新时间戳)。

以下是经过大厂高并发压测验证的生产级 Lua 脚本:

-- KEYS[1]: 限流唯一标识 (如 {order:limit}:user:12345) -- ARGV[1]: 桶容量上限 (Capacity) -- ARGV[2]: 令牌生成速率 (Tokens per millisecond) -- ARGV[3]: 当前时间戳 (毫秒) -- ARGV[4]: 本次申请的令牌数 (通常为 1) local key = KEYS[1] local capacity = tonumber(ARGV[1]) local rate = tonumber(ARGV[2]) local now = tonumber(ARGV[3]) local requested = tonumber(ARGV[4]) -- 获取当前桶状态 local data = redis.call('HMGET', key, 'tokens', 'last_updated') local tokens = tonumber(data[1]) local last_updated = tonumber(data[2]) if tokens == nil then -- 首次访问,初始化满桶 tokens = capacity last_updated = now else -- 计算时间差内应该新补充的令牌数 local delta = math.max(0, now - last_updated) local generated = delta * rate tokens = math.min(capacity, tokens + generated) last_updated = now end -- 判断令牌是否充足 if tokens >= requested then tokens = tokens - requested redis.call('HMSET', key, 'tokens', tokens, 'last_updated', last_updated) -- 设置合理的 TTL(例如将填满整个桶所需时间的 2 倍作为生命周期) local fill_time = math.ceil(capacity / rate / 1000) redis.call('EXPIRE', key, math.max(60, fill_time * 2)) return 1 -- 允许请求通过 else -- 令牌不足,仅更新时间戳,拦截请求 redis.call('HSET', key, 'last_updated', last_updated) return 0 -- 限流拦截 end

生产落地的三大防御性设计

在实际将这套限流方案推向线上时,还有三处直接决定系统生死的工程细节:

  1. Redis Cluster 模式下的 Hash Tag 约束:
    在 Redis 分布式集群中,Lua 脚本操作的所有 Key 必须位于同一个 Slot(分片哈希槽)上,否则 Redis 会直接抛出CROSSSLOT Keys in request don't hash to the same slot异常。
    因此,限流 Key 必须合理设计大括号 Hash Tag,例如:{rate_limit:user_8848}:token_bucket,确保同一用户的限流元数据固定落入单一节点。

  2. NTP 时钟同步抖动防范:
    惰性计算依赖传入的应用服务器当前时间戳。如果应用集群各节点的 NTP 时间发生较大漂移,会导致计算出的delta为负数或突发暴增。在 Lua 脚本中必须加上math.max(0, now - last_updated)防御逻辑,避免时间倒流导致令牌被清空。

  3. 限流降级与客户端柔性反馈:
    一旦请求被 Lua 脚本判定为限流,业务服务必须在网关层立即短路返回 HTTP 429(Too Many Requests),并在响应头中附带Retry-After: 3,配合前端 App 弹出柔性提示:“当前排队人数较多,请稍候重试”,同时触发滑块验证码二次校验,彻底粉碎脚本的自动化重试循环。

高可用系统设计的真谛,从来不是盲目堆砌昂贵的服务器硬件,而是用清晰的数据结构与优雅的数学模型,在流量风暴到达核心脆弱节点前,将其平稳化解于无形。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/5 5:58:27

LIME低光图像增强:光照图估计与优化求解全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 5:58:22

IT66220硬件HDCP引擎与预烧密钥:HDMI合规认证省心方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 5:57:52

工业嵌入式存储选型:MRAM与PIC32MX795F512L的SPI读写实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 5:57:41

深度度量学习提升蛋白质二级结构预测:PSSM与三元组损失实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 5:57:40

StarNet图像分类实战:星运算轻量网络原理与代码实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 5:57:18

工业级MRAM与MSP432P401R的SPI存储方案实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华