Redis 系列(九):分布式锁与限流——用 Redis 做并发治理
核心目标掌握分布式锁的正确实现SET NX EX 持有者标识 Lua 原子释放理解锁过期、主从切换的失效边界与 Redlock 争议掌握固定窗口、滑动窗口、令牌桶三种限流算法及其 Redis 实现。前置知识完成 Part 7Lua 原子性——锁释放与令牌桶的地基与 Part 8重建锁的实战用法。验证环境Redis 8.10.0cygwin 移植版127.0.0.1:6379、redis-py 8.1.0、Python 3.11.6、Windows 11。最后复核日期2026-08-07。0. 本篇问题场景锁和限流Redis 的两副面孔同一把SET命令在两个场景里是完全不同的角色分布式锁多个进程抢一个资源下单、发券保证同一时刻只有一个执行限流把请求速率压到阈值内秒杀、防刷保证单位时间只放行 N 个。两者共同的难点正确性藏在边界里——锁的边界是过期与误删限流的边界是窗口与突发。本篇先用实验复现这些边界再给工程结论。1. 分布式锁正确实现的三要素1.1 错误的起点setnxexpire两步经典错误写法是两个独立命令SETNX拿锁再EXPIRE设过期。两步之间进程崩溃锁就永不释放。正确做法是单条原子命令r.set(lock_key,token,nxTrue,ex10)三个要素缺一不可要素作用反例NX不存在才写入互斥用SET直接覆盖会抢别人的锁EX过期兜底防死锁无 TTL持有者崩溃后锁永存唯一 token标识持有者释放时校验无 token 时释放会误删他人锁1.2 实测一错误释放 vs Lua 原子释放# 错误直接 delete——不管锁是不是自己的r.delete(lock_key)# 正确Lua 校验持有者标识后原子删除release_script if redis.call(GET, KEYS[1]) ARGV[1] then return redis.call(DEL, KEYS[1]) else return 0 end 实测第一次获取锁: True 第二次获取锁应失败: None ← 互斥生效 错误释放无校验后锁: None ← 锁没了可能删的是别人的 演示重新加锁后再验证正确释放 正确释放value 匹配: 1 ← 只有持有者能删 释放后锁: None1.3 实测二锁过期的竞态——“A 释放了 B 的锁”完整事故链复现TTL 1 秒A 拿锁token-A, TTL 1s→ A 业务超时 → 锁过期 B 获取锁: TrueB 现在持有token-B A 无校验释放后锁: None ← A 把 B 的锁删了B 的保护失效 演示B 察觉锁丢失后重新加锁 token-B A 带 value 校验释放: 0 ← 校验不通过B 的锁安然无恙 B 的锁仍然在: token-B结论锁的释放必须校验 删除原子完成Lua否则任何一次误删都会让两个进程同时进入临界区——比没有锁更糟。1.4 锁的过期时间怎么定TTL 太短 → 业务没跑完锁就过期另一个进程进来上面的事故太长 → 持有者崩溃后资源被锁住很久。工程解法TTL 业务最长执行时间经验值3-10 倍余量看门狗续期持有者定期如 TTL/3执行 Lua 续期业务结束主动释放——类似 Redisson 的 watchdog续期本身有成本与失败路径最终兜底仍是业务幂等。2. 分布式锁的边界Redlock 争议与正确认知2.1 主从切换丢锁Redis 主从复制是异步的主库SET NX成功返回后复制到从库之前主库宕机 → 哨兵把从库提升为主库 →新主库没有这把锁→ 另一个进程可以拿到同一把锁。进程A: SET lock token-A NX EX → 主库成功 主库宕机锁未复制到从库 哨兵提升从库为新主库无锁记录 进程B: SET lock token-B NX EX → 成功A、B 同时持锁2.2 Redlock 与争议antirez 提出的Redlock向 N 个独立 Redis 实例N5同时申请锁多数≥3成功且耗时 TTL 才算拿到。反对者Martin Kleppmann 等的核心论点锁的互斥依赖时钟TTL 到期依赖各节点时钟一致时钟跳跃会破坏GC 停顿进程拿到锁后 JVM GC/进程暂停暂停时间超过 TTL锁过期后他人进入——Redlock 无法阻止这不是 Redis 的问题是锁 暂停的普遍问题多数场景单实例锁 幂等兜底已足够Redlock 的复杂度与运维成本不划算。工程结论也是本系列立场分布式锁是概率性互斥——过期、主从切换、GC 停顿都可能破坏互斥。正确的架构不是追求绝对安全的锁而是分布式锁减少并发竞争 业务幂等兜底重复执行 监控告警高可用锁场景Redis 主从切换在 Part 10 哨兵/集群后能缓解丢锁但幂等永远是最底层防线。3. 限流三种算法与 Redis 实现3.1 固定窗口最简单deffixed_window(key,limit,window):curr.incr(key)ifcur1:r.expire(key,window)# 第一条命令开启窗口returncurlimit实测3 次/2 秒连续 5 次请求: [True, True, True, False, False] 并发压测限制 5/秒, 50 请求: 放行 5 个缺点窗口边界允许突发——第 1 秒末放 5 个、第 2 秒初又放 5 个2 秒内放了 10 个。3.2 滑动窗口精确到任意窗口用 ZSet 记录每次请求时间戳查询时先清掉窗口外的记录再计数defsliding_window(key,limit,window,member):nowtime.time()*1000pipe.zremrangebyscore(key,0,now-window*1000)# 清理过期pipe.zadd(key,{member:now})pipe.zcard(key)pipe.expire(key,window)_,_,count,_pipe.execute()returncountlimit实测3 次/2 秒连续 5 次请求: [True, True, True, False, False]代价每个请求一条 ZSet 记录内存与 ZSet 大小随 QPS 增长可配合ZREMRANGEBYSCORE定期清理。3.3 令牌桶平滑突发桶容量 突发上限速率 稳定速率。Lua 实现原子维护令牌数 时间戳localtokenstonumber(redis.call(GET,KEYS[1])orcapacity)locallasttonumber(redis.call(GET,KEYS[1]..:ts)ornow)tokensmath.min(capacity,tokens(now-last)*rate)redis.call(SET,KEYS[1],tokens)redis.call(SET,KEYS[1]..:ts,now)iftokens1thenredis.call(SET,KEYS[1],tokens-1)return1elsereturn0end实测容量 5、速率 2/秒突发 8 个请求: [True×5, False×3] ← 桶满 5突发 5 个 等 1 秒后2 令牌: [True, True, False] ← 平滑补充对比算法实现成本突发控制内存固定窗口最低1 个键无边界可翻倍最低滑动窗口中ZSet精确随 QPS 增长令牌桶中Lua平滑容量即突发上限低2 个键4. shop-lab 实战锁 幂等 限流三层4.1 分布式锁封装shop-lab/src/shop_lab/lock.pydefacquire(lock_name,ttl10):keyflock:{lock_name}tokenuuid.uuid4().hexifr.set(key,token,nxTrue,exttl):returnkey,token# 返回锁与持有者标识returnNonedefrelease(lock_key,token):returnr.eval(_RELEASE_SCRIPT,1,lock_key,token)1# Lua 原子释放4.2 限流封装shop-lab/src/shop_lab/ratelimit.py提供fixed_window与token_bucket§3 的实现。秒杀接口组合使用订单幂等acquire(order:{order_no}) → 执行业务 → release 唯一索引兜底 秒杀限流token_bucket(seckill:{sku}, capacity100, rate20)4.3 测试tests/test_lock.py互斥、误删他人锁被拒、上下文管理器、获取失败抛异常与tests/test_ratelimit.py窗口边界、窗口重置、令牌桶突发/重填$ cd redis/shop-lab python -m pytest ......................................... [100%] 41 passed in 33.28s5. 版本与环境差异差异点说明SET NX EX2.6.12 稳定redis-py 8.x 失败返回None非 False锁实现选择单实例锁 幂等兜底是默认Redlock 需要 5 个独立实例仅高安全场景看门狗续期redis-py 无内建 watchdogRedisson 有需要可自实现 Lua 续期限流算法与版本无关INCR/ZADD/Lua 在 6.x 一致6. 测试与验收锁测试必须覆盖互斥、误删他人锁被拒持有者校验、上下文释放、获取失败限流测试用固定窗口 短 sleep 控制时序断言放行/拒绝序列并发验证用线程 统计断言限制 N 放行 ≤ N固定窗口允许边界误差。本篇验收清单能写出正确的加锁SET NX EX token与解锁Lua 校验并解释每一步为什么能复述锁过期 → 他人持锁 → 误删事故链并说出 value 校验的作用能解释主从切换丢锁与 Redlock 争议的核心论点并说出锁 幂等 监控的立场能实现固定窗口、滑动窗口、令牌桶并说出各自边界突发/内存/平滑cd redis/shop-lab python -m pytest41 passed。7. 常见误区“setnxexpire两步没问题”——两步之间崩溃即死锁必须原子 SET§1.1。“释放锁就是 delete”——必须校验持有者否则 A 会删掉 B 的锁§1.3。“Redlock 绝对安全”——它解决主从丢锁但解决不了时钟跳跃与 GC 停顿争议的本质是锁的绝对性不存在§2.2。“固定窗口就够精确了”——窗口边界可放行 2 倍流量§3.1。“限流返回 429 就行”——服务端限流还要配合客户端退避、降级否则重试风暴会放大压力。8. 本篇小结分布式锁与限流本质都是用 Redis 的原子性换并发可控锁SET NX EX获取 Lua 校验释放 幂等兜底边界它是概率性互斥不是数学证明限流固定窗口简单/ 滑动窗口精确/ 令牌桶平滑按突发容忍度选型组合拳锁处理谁来做限流处理能做多快幂等处理做了两次怎么办——三者缺一不可。下一篇进入高可用篇Part 10主从复制与哨兵回答单机挂了怎么办——复制原理、哨兵故障转移与脑裂防护用真实的一主两从三哨兵环境演练一次主库宕机。9. 官方资料SET 命令NX/EX 选项https://redis.io/docs/latest/commands/set/Redlock 原稿antirezhttps://redis.io/docs/latest/develop/use/patterns/distributed-locks/分布式锁争议Kleppmannhttps://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.htmlINCR / ZADD / EVALhttps://redis.io/docs/latest/commands/