ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

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

秒杀系统接口防刷限流:基于令牌桶算法与分布式 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每次请求进来时移除窗口时间以外的旧数据ZREMRANGEBYSCORE统计当前窗口内的有效请求总数ZCARD若未超限则添加当前时间戳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生产落地的三大防御性设计在实际将这套限流方案推向线上时还有三处直接决定系统生死的工程细节Redis Cluster 模式下的 Hash Tag 约束在 Redis 分布式集群中Lua 脚本操作的所有 Key 必须位于同一个 Slot分片哈希槽上否则 Redis 会直接抛出CROSSSLOT Keys in request dont hash to the same slot异常。因此限流 Key 必须合理设计大括号 Hash Tag例如{rate_limit:user_8848}:token_bucket确保同一用户的限流元数据固定落入单一节点。NTP 时钟同步抖动防范惰性计算依赖传入的应用服务器当前时间戳。如果应用集群各节点的 NTP 时间发生较大漂移会导致计算出的delta为负数或突发暴增。在 Lua 脚本中必须加上math.max(0, now - last_updated)防御逻辑避免时间倒流导致令牌被清空。限流降级与客户端柔性反馈一旦请求被 Lua 脚本判定为限流业务服务必须在网关层立即短路返回 HTTP 429Too Many Requests并在响应头中附带Retry-After: 3配合前端 App 弹出柔性提示“当前排队人数较多请稍候重试”同时触发滑块验证码二次校验彻底粉碎脚本的自动化重试循环。高可用系统设计的真谛从来不是盲目堆砌昂贵的服务器硬件而是用清晰的数据结构与优雅的数学模型在流量风暴到达核心脆弱节点前将其平稳化解于无形。
返回列表