Redis 进阶 05:分布式锁与限流

实验基线:Redis 7.4.11,先完成环境准备。除标注为宿主机终端的代码外,Redis 命令在该课指定的 redis-cli 中执行;不复制输出结果作为命令。

学习目标#

实现有到期时间和所有权校验的单实例锁,理解它无法保证跨系统业务安全;用短 Lua 脚本实现固定窗口限流。

原理:锁是租约,不是永久互斥证明#

SET key token NX PX timeout 能原子地完成“仅不存在时占有 + 有效期”。token 应是每次请求独立生成的高熵随机值;下面固定 token 只便于观察,不能直接用于生产。

锁过期后其他客户端可以拿到新锁。旧持有者仍可能运行,直接 DEL 会删掉别人的锁。必须原子比较 token 后释放。即使正确释放,也不能阻止超时的旧持有者继续写外部数据库。

主从异步复制时,锁写入尚未复制就故障切换,新主可能让另一个客户端成功拿锁。因此本课展示的是单实例租约模式,不承诺故障切换时的严格互斥;关键业务使用幂等、唯一约束或下游认可的 fencing token。

实验:两个持有者#

UNLINK learn:lock:order:42
SET learn:lock:order:42 token-A NX PX 30000
SET learn:lock:order:42 token-B NX PX 30000
PTTL learn:lock:order:42

在 30 秒内依次运行,预期 OK、nil、正的剩余毫秒数。第二个客户端失败后不能无限忙等,可以随机退避并设置总等待上限。

先用错误 token 释放,再用正确 token 释放:

EVAL "if redis.call('GET',KEYS[1])==ARGV[1] then return redis.call('DEL',KEYS[1]) else return 0 end" 1 learn:lock:order:42 token-B
EVAL "if redis.call('GET',KEYS[1])==ARGV[1] then return redis.call('DEL',KEYS[1]) else return 0 end" 1 learn:lock:order:42 token-A

锁尚未到期时预期 0、1。若已到期,两次都可能为 0,重新运行获取实验即可。不要把“释放返回 0”隐藏为释放成功;它可能表示锁已丢失。

实验:固定窗口限流#

假设用户 alice 每个从首请求起的 60 秒窗口最多 3 次请求。脚本返回 1 表示允许,0 表示拒绝:

UNLINK learn:limit:alice
EVAL "local n=redis.call('INCR',KEYS[1]); if n==1 then redis.call('EXPIRE',KEYS[1],ARGV[1]); end; if n>tonumber(ARGV[2]) then return 0 else return 1 end" 1 learn:limit:alice 60 3
EVAL "local n=redis.call('INCR',KEYS[1]); if n==1 then redis.call('EXPIRE',KEYS[1],ARGV[1]); end; if n>tonumber(ARGV[2]) then return 0 else return 1 end" 1 learn:limit:alice 60 3
EVAL "local n=redis.call('INCR',KEYS[1]); if n==1 then redis.call('EXPIRE',KEYS[1],ARGV[1]); end; if n>tonumber(ARGV[2]) then return 0 else return 1 end" 1 learn:limit:alice 60 3
EVAL "local n=redis.call('INCR',KEYS[1]); if n==1 then redis.call('EXPIRE',KEYS[1],ARGV[1]); end; if n>tonumber(ARGV[2]) then return 0 else return 1 end" 1 learn:limit:alice 60 3
TTL learn:limit:alice

一分钟内执行预期 1、1、1、0,TTL 为正。拒绝请求也计数,本例对受控 key 和正整数参数有效。固定窗口在边界附近可能出现两倍额度的短时突发;更平滑的控制可选择滑动窗口或令牌桶。

常见错误#

  • 锁没有 TTL:进程崩溃后可能永久占用。
  • 锁值都写 1:无法辨认所有权。
  • 业务最长耗时大于租约却没有终止机制:超时后的并发仍可能发生。
  • 限流 Redis 故障时无条件放行:应按业务明确 fail-open 或 fail-closed,并记录降级。

练习与参考答案#

模拟旧持有者在新锁生成后释放:

UNLINK learn:lock:order:42
SET learn:lock:order:42 token-B NX PX 30000
EVAL "if redis.call('GET',KEYS[1])==ARGV[1] then return redis.call('DEL',KEYS[1]) else return 0 end" 1 learn:lock:order:42 token-A
GET learn:lock:order:42

预期旧 token 释放返回 0,新锁仍为 token-B。答案中还必须指出:token 校验只保护释放,不能保护外部业务提交。

清理#

UNLINK learn:lock:order:42 learn:limit:alice

官方参考#

分布式锁 · SET · 限流计数示例

学习导航#

上一课:Redis 进阶 04:缓存故障与数据一致性 · 学习目录 · 下一课:Redis 进阶 06:Pub/Sub 与 Streams

Redis 进阶 05:分布式锁与限流
作者:LcooYa
CC BY-NC-SA 4.0
相关文章
随机文章