Redis 分布式锁

20 阅读5分钟

如果所有服务同时争抢一个资源,系统会怎样?

想象这样一个场景:你的电商平台正在搞“秒杀”活动,库存只有100件商品,却有上万用户在同一毫秒点击“立即购买”。如果没有有效的协调机制,多个服务实例可能同时读取到“还有库存”,然后各自扣减,最终导致超卖——卖出200件、300件,甚至更多。这不仅造成业务损失,还可能引发客户投诉和信任危机。

在分布式系统中,这种对共享资源的并发访问问题尤为棘手。单机锁(如 synchronizedReentrantLock)只在单个 JVM 内有效,无法跨节点同步。这时候,Redis 分布式锁就成了解决这类问题的关键工具之一。


什么是 Redis 分布式锁?

简单来说,Redis 分布式锁是一种利用 Redis 的原子操作特性,在多个分布式节点之间实现互斥访问共享资源的机制。它的核心思想是:谁先成功在 Redis 中“占位”,谁就获得执行权,其他节点必须等待锁释放后才能尝试获取。

为什么选择 Redis?原因有三:

  1. 高性能:Redis 是内存数据库,读写速度极快,适合高并发场景。
  2. 原子操作支持:如 SET key value NX PX 命令,能一次性完成“设值+过期时间+仅当不存在时设置”的操作。
  3. 部署广泛:大多数微服务架构中已集成 Redis,无需额外引入新组件。

如何正确实现一个 Redis 分布式锁?

很多人第一反应是用 SETNX(Set if Not eXists)命令:

SETNX lock_key my_value

如果返回 1,说明加锁成功;返回 0,则说明锁已被占用。看起来没问题?但这里有两个致命缺陷:

  1. 没有自动过期机制:如果持有锁的服务宕机,锁永远不会释放,导致死锁。
  2. 解锁不安全:任意客户端都能执行 DEL lock_key 删除锁,可能误删别人的锁。

正确姿势:带唯一标识 + 自动过期

Redis 官方推荐使用 SET 命令的扩展参数:

SET lock_key unique_value NX PX 30000
  • NX:仅当 key 不存在时才设置(相当于 SETNX)
  • PX 30000:设置 30 秒自动过期(防止死锁)
  • unique_value:每个客户端使用唯一标识(如 UUID),用于后续安全解锁

安全解锁:必须校验 value

不能直接 DEL,而要用 Lua 脚本保证原子性:

if redis.call("GET", KEYS[1]) == ARGV[1] then
    return redis.call("DEL", KEYS[1])
else
    return 0
end

这段脚本确保:只有持有相同 value 的客户端才能删除锁,避免误删。

在 Java 中调用示例(使用 Jedis):

String lockKey = "order:lock";
String requestId = UUID.randomUUID().toString();
int expireTime = 30000; // 30秒

// 尝试加锁
Boolean locked = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
if (locked != null && locked) {
    try {
        // 执行业务逻辑:如扣库存、创建订单
        processOrder();
    } finally {
        // 安全释放锁
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
    }
}

实践中的常见陷阱与解决方案

1. 锁过期时间设置不当

  • 问题:业务执行时间超过锁的过期时间,锁提前释放,其他线程进入,导致并发。
  • 对策
    • 预估业务最大耗时,适当延长过期时间。
    • 使用 锁续期(Watchdog 机制):如 Redisson 的 RLock 会在后台定期延长锁的有效期,只要业务还在运行,锁就不会失效。

2. 主从切换导致锁丢失

  • 问题:Redis 主节点加锁后未同步到从节点,主挂了,从升主,新主没有锁信息,导致多个客户端同时获得锁。
  • 对策
    • 使用 Redlock 算法(由 Redis 作者提出):向多个独立 Redis 节点同时加锁,只有多数成功才算获取锁。
    • 但 Redlock 实现复杂,且在极端网络分区下仍有争议。更推荐使用强一致性的 ZooKeeper 或 etcd 来实现高可靠锁。

大多数业务场景下,单 Redis 实例 + 合理过期时间 + 唯一 ID 解锁,已经足够。除非对一致性要求极高(如金融交易),否则不必过度设计。

3. 忙等待 vs 阻塞等待

  • 简单实现中,客户端通常采用“轮询重试”方式获取锁,浪费 CPU 和网络。
  • 更优雅的方式是使用 Redis 的 Pub/Sub 机制Redisson 的 wait/notify 模型,让等待者在锁释放时被唤醒。

Redisson:生产级分布式锁的首选

对于 Java 开发者,Redisson 是一个成熟的 Redis 客户端,内置了多种分布式锁实现:

RLock lock = redisson.getLock("myLock");
lock.lock(); // 默认30秒自动续期
try {
    // 业务逻辑
} finally {
    lock.unlock();
}

它自动处理了:

  • 唯一 ID 生成
  • Lua 脚本解锁
  • Watchdog 自动续期
  • 可重入锁支持(同一线程可多次加锁)

大大降低了出错概率,建议在生产环境中优先考虑。


总结与思考

Redis 分布式锁不是银弹,但它是在性能与一致性之间取得良好平衡的实用方案。关键在于理解其原理,避开常见陷阱,并根据业务场景合理选择实现方式。

值得思考的是:是否真的需要分布式锁? 有时通过业务设计(如将库存分片、使用消息队列削峰)可以避免竞争,比加锁更高效、更可靠。

技术没有绝对的对错,只有是否适合。在追求高并发的同时,别忘了系统的可维护性和容错能力——毕竟,一个能稳定运行的系统,远比一个理论上完美的设计更有价值。