Redis 锁是并发安全的吗?怎么保证不被抢夺?

#中间件#面试八股

Redis 锁是并发安全的吗?怎么保证不被抢夺?

结论:单条 SET NX PX 是原子的,但"持锁期间的安全"不是一个命令能保证的。真实风险有四类:解锁误删别人的锁、业务超时导致锁提前释放、主从切换丢锁、客户端 GC/网络抖动造成的"以为自己还持锁"。要保证不被抢夺,需要唯一 value + 安全解锁 + 续期 + fencing token + 业务幂等这一整套组合。

一、风险清单与对策

风险触发条件后果对策
误删他人锁A 超时后 B 加锁成功,A 再 DEL两个消费者同时执行value 存唯一 token,Lua 比对后删
锁提前过期业务耗时 > TTL并发进入临界区看门狗续期 + TTL 留足余量
主从丢锁主加锁后未同步即宕机,从升主两客户端同时持锁fencing token / 单点 / 换 CP 系统
客户端假活长时间 Full GC、网络分区持锁方已停但锁还在续期由独立线程做,失败即主动放弃
锁粒度错用全局锁保护单订单吞吐骤降,但不安全按资源 ID 加锁,避免共享锁
重入误判同一线程 flush 前释放提前释放外层锁计数器 / Redisson 可重入实现

二、fencing token:让"被抢"变得可检测

假设主从丢锁,A、B 同时认为持锁:
   A 拿到 token=33,B 拿到 token=34
        │
        ▼
   资源侧(DB / 对象存储 / 下游接口)记录 last_token
   if (token <= last_token) 拒绝本次写
   else last_token = token,允许写入

结果:A 的写入被拒绝,即使 A 以为自己持锁,也不会造成双写。

这就是 fencing token 的价值:锁服务可能会骗你,但资源侧的单调递增令牌不会。实现上可以用 Redis 的 INCR 生成全局令牌,写入时把令牌一起带给下游。

long token = redis.opsForValue().increment("lock:seq:stock:" + skuId);
if (!lock.tryLock(1, TimeUnit.SECONDS)) { return; }
try {
    // 下游 SQL:WHERE sku_id = ? AND lock_token < ?
    int n = jdbc.update("UPDATE stock SET qty = qty - 1, lock_token = ? "
                      + "WHERE sku_id = ? AND qty > 0 AND lock_token < ?",
                        token, skuId, token);
    if (n == 0) { log.warn("fencing 拒绝,token={}", token); }
} finally {
    lock.unlock();
}

三、安全解锁必须用 Lua

# 错误示范:GET 与 DEL 之间可能被其他命令插入
GET lock:order:1
DEL lock:order:1

# 正确:比对 + 删除在一个脚本里完成,Redis 单线程保证原子
EVAL "if redis.call('get', KEYS[1]) == ARGV[1] then \
        return redis.call('del', KEYS[1]) \
      else return 0 end" 1 lock:order:1 8f3c-4a1e-...
// 集群模式下,锁 key 与业务 key 若需在同一脚本操作,用 hash tag 保证同槽
// lock:{order:1} 与 order:{order:1}:qty 会落到同一个 slot
String lockKey = "lock:{order:" + orderId + "}";

四、常见追问

  • 问:先 SETNX 再 EXPIRE 为什么会死锁?答:两条命令不原子,加锁成功但进程在 EXPIRE 前崩溃,key 永不过期,后续所有请求都拿不到锁。
  • 问:看门狗能解决"锁被抢"吗?答:只能解决"业务没跑完锁先过期",解决不了主从丢锁与客户端假活;后者要靠 fencing 或幂等。
  • 问:为什么续期要放在独立线程?答:业务线程可能被阻塞(死循环、慢 SQL),独立线程才能真实反映"进程是否活着",续期失败就说明该让位。
  • 问:网络分区时客户端该怎么做?答:给锁操作设短超时,失败就快速失败而不是无限重试;重试要带退避与最大次数,避免分区恢复瞬间把 Redis 打满。
  • 问:业务已经幂等了,还需要锁吗?答:锁防的是"并发重复执行",幂等防的是"重复请求结果一致";能幂等就不必强求锁,锁能减少无效计算。二者常配合使用。
  • 问:锁的 TTL 设多长合适?答:覆盖 P99 业务耗时并留 2~3 倍余量,同时不超过看门狗默认 30s 太多;宁可依赖续期,也不要把 TTL 设成几分钟。
  • 问:集群模式下锁 key 有什么坑?答:多 key 的 Lua 脚本要求同 slot,锁与业务数据若不在同槽必须拆成两次操作,或统一用 hash tag 固定槽位。
  • 问:如何验证锁真的生效?答:压测时统计临界区内的并发数(打点计数,进入 +1、退出 -1,最大值应恒为 1),并在主从切换演练中观察是否出现双持锁。

小结

  • 原子加锁只是起点,安全的关键是"解锁认主、到期可控、被抢可检测"。
  • 单机 Redis 锁 + 业务幂等能覆盖绝大多数场景;跨机房强一致诉求应换 CP 系统(etcd/ZooKeeper)。
  • 记住 fencing token 这个思路:不信任锁,信任资源侧的单调版本号。
笔记加载中…