Redis - 如何使用 Redis 实现分布式锁

文章目录

在分布式系统里,多个进程需要协调访问共享资源时,分布式锁几乎是绕不开的工具。Redis 因为性能好、部署简单、命令丰富,成为实现分布式锁的最常见选择。但简单不等于容易------一把"看起来能用"的 Redis 分布式锁,真正用到生产环境,常常因为细节问题踩坑。

分布式锁的基本要求

任何一把分布式锁,都必须满足三个性质:

  1. 互斥性:同一时刻只有一个客户端能持有锁。
  2. 死锁避免:持有锁的客户端崩溃,锁要能自动释放。
  3. 解铃还须系铃人:客户端只能释放自己持有的锁,不能误删别人的。

Redis 实现分布式锁的难点,就在这三点上。

单机版:SET NX PX 一行搞定

最基础的实现:

bash 复制代码
SET lock_key unique_value NX PX 30000

三个关键参数:

  • NX:key 不存在才设置,保证互斥。
  • PX 30000:30 秒后自动过期,防止死锁。
  • unique_value:客户端唯一标识,释放时校验。

加锁成功返回 OK,失败返回 nil

释放锁要用 Lua

释放锁不是简单的 DEL。考虑这个场景:

复制代码
客户端 A 加锁,PX=30s
客户端 A 业务执行 35s(GC 卡顿等原因)
锁过期,客户端 B 加锁成功
客户端 A 完成业务,DEL lock_key  ← 删的是 B 的锁!

要避免这个问题,释放时必须先校验持有者。但"GET + DEL"是两条命令,中间可能被打断。所以释放必须用 Lua 脚本保证原子性:

lua 复制代码
if redis.call('GET', KEYS[1]) == ARGV[1] then
    return redis.call('DEL', KEYS[1])
else
    return 0
end

锁过期的另一个问题:业务超时

PX 30s 是个两难的选择:

  • 太短:业务还没做完就过期,锁失效。
  • 太长:进程崩溃后,锁很久才能释放,影响其他客户端。

成熟的方案是引入"看门狗"(watchdog)机制:客户端启一个后台线程,定期延长锁的过期时间。Redisson 就是这么实现的------默认每 10 秒续期一次,把锁的过期时间重置回 30 秒。这样只要持有者还活着,锁就不会过期;一旦持有者挂了,看门狗也停了,锁会自然过期。

集群版的 RedLock 算法

单机 Redis 实现的锁有个根本性问题:主库挂了。如果加锁后主库还没把锁同步到从库就崩溃,哨兵切换到从库,新主库上根本没这个锁,互斥性就被打破了。

Redis 作者 Antirez 提出了 RedLock 算法,思路是:

  1. 部署 N 个独立的 Redis 实例(推荐 5 个),互不主从。
  2. 客户端依次向所有实例申请同一个锁,记录开始时间。
  3. 如果超过 N/2+1 个实例加锁成功,且总耗时小于锁的过期时间,则认为加锁成功。
  4. 如果失败,向所有实例发送释放请求(不管之前加锁是否成功)。

只要多数派实例存活且没被网络隔离,锁就能保持互斥性。

RedLock 的争议

分布式系统专家 Martin Kleppmann 写过一篇著名文章质疑 RedLock:在 GC 暂停、时钟漂移等场景下,RedLock 仍然不能保证安全性。Antirez 也回应过,但这个争论至今没有定论。

实际工程上的建议:

  • 普通业务:单机 Redis + 短 TTL + 看门狗(如 Redisson 默认实现)已经足够,简单可靠。
  • 金融级强一致:直接用 ZooKeeper 或 etcd,它们的设计就是为分布式协调而生。
  • 真要用 RedLock:考虑清楚是不是真的需要这种复杂度。

Redisson:开箱即用的方案

自己实现分布式锁很容易踩坑。Java 生态里 Redisson 是事实标准,封装好了所有细节:

java 复制代码
RedissonClient redisson = Redisson.create(config);
RLock lock = redisson.getLock("myLock");

try {
    lock.lock();    // 阻塞获取,自动续期
    // 业务逻辑
} finally {
    lock.unlock();
}

Redisson 提供了:

  • 自动续期(看门狗)
  • 可重入(同一线程多次获取不阻塞)
  • 公平锁(按申请顺序授予)
  • 读写锁、信号量、CountDownLatch 等更复杂的同步原语
  • 集群模式下的 RedLock 实现

Python 用 redis-py 自带的 redis.lock,Go 用 redsync,都能省掉不少基础工作。

常见坑点

1. 锁粒度太大

把整个业务都放在锁里,锁的持有时间过长,吞吐量直线下降。锁应该只保护真正有竞争的那段代码。

2. 锁粒度太小

为了优化性能把锁拆得太细,结果加多把锁反而引入死锁风险。粒度要适中。

3. 忘了 unique_value

释放锁时不校验持有者,可能误删别人的锁。这是最常见的隐藏 bug。

4. 没有重试机制

加锁失败直接报错,对业务不友好。一般要带退避重试,但要设上限避免无限等待。

5. 用普通 SET 代替 SETNX

SET key value 会无条件覆盖,根本没有互斥语义。必须带 NX。

实践建议

  1. 能不用锁就不用,优先考虑用 Redis 原子操作或乐观锁解决。
  2. 生产环境用成熟库(Redisson / redis-py 内置 lock),别自己造轮子。
  3. TTL 要合理,业务正常耗时的 2-3 倍即可,配合看门狗续期。
  4. 释放锁必须用 Lua,校验持有者后再删除。
  5. 强一致场景换工具,ZooKeeper / etcd 比 Redis 更适合做分布式协调。
  6. 监控锁的等待时间,等待时间长说明竞争激烈,要么优化业务,要么调整锁粒度。

分布式锁是个看起来简单实际复杂的话题。Redis 提供的命令很基础,但要把锁用对,必须考虑过期、续期、释放、容错等一系列问题。理解这些细节,才能在真实业务里写出经得起考验的代码。

相关推荐
老纪3 小时前
Redis分布式锁进第九零篇
数据库·redis·分布式
放下华子我只抽RuiKe54 小时前
FastAPI 全栈后端(三):数据库与 ORM
前端·数据库·react.js·oracle·性能优化·前端框架·fastapi
x***r1514 小时前
linux安装 redis-8.6.0.tar.gz 详细步骤(源码编译、配置、启动)
redis
一个天蝎座 白勺 程序猿4 小时前
从300秒到3秒:我在KES上“干掉“标量子查询的性能优化实践
性能优化·量子计算·kingbasees·向量化执行
Jinkxs4 小时前
Rust 性能优化全流程:从 flamegraph 定位瓶颈到 unsafe 与 SIMD 加速,响应快 2 倍
开发语言·性能优化·rust
醉颜凉5 小时前
Elasticsearch性能优化:JVM GC调优全攻略,彻底解决集群卡顿、吞吐量下降问题
jvm·elasticsearch·性能优化
梵得儿SHI5 小时前
Vue 项目实战与性能优化全攻略:从代码、渲染到首屏,一站式解决卡顿慢加载
前端·vue.js·性能优化·vite·前端面试·前端优化·首屏优化
Swift社区7 小时前
鸿蒙游戏为什么掉帧?60FPS性能优化实战指南
游戏·性能优化·harmonyos