如何使用redis实现一个分布式锁

在分布式系统中,分布式锁用于确保多个服务或节点在并发情况下能正确地同步对共享资源的访问。Redis 是实现分布式锁的一种常用方式,以下是详细的实现方案。


✅ 一、基本原理

使用 SET key value NX PX expire_time 命令实现:

SET lock_key unique_value NX PX 30000

含义如下:

  • lock_key:锁的键名;
  • unique_value:唯一标识,比如 UUID(用于后续释放锁时确认是自己加的锁);
  • NX:只有键不存在时才设置(避免覆盖);
  • PX 30000:设置过期时间(防止死锁)——单位是毫秒。

✅ 二、获取锁的逻辑(伪代码)

# 伪代码
success = redis.set("my_lock", unique_value, nx=True, px=30000)
if success:
    # 获取锁成功
    do_business()
else:
    # 获取失败,可能其他人持有锁
    return

✅ 三、释放锁(Lua 脚本原子操作)

释放时要确保只释放自己加的锁。不能简单地 DEL key,因为可能误删别人的锁

正确方式是用 Lua 脚本保证原子性:

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

Python 示例(使用 redis-py):

unlock_script = """
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end
"""
redis.eval(unlock_script, 1, "my_lock", unique_value)

✅ 四、完整流程

  1. 生成唯一 value(如 UUID);
  2. 调用 SET lock_key value NX PX expire_time
  3. 加锁成功 → 执行业务逻辑;
  4. 处理完后,使用 Lua 脚本释放锁。

✅ 五、进阶:RedLock(Redis 官方推荐)

为了应对 Redis 节点宕机的情况,Redis 作者提出了 RedLock 算法

  • 在 5 个独立 Redis 节点中设置相同的锁;
  • 至少在 3 个节点设置成功且在有效时间内,才视为加锁成功;
  • 更复杂,但容错性更好。

⚠️ 不过 RedLock 实现有争议,单实例 Redis + 设置合理超时时间 + Lua 释放锁 是大多数系统中足够的解决方案。


PHP完整示例代码

<?php

$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

// 1. 锁的键和值(唯一标识建议使用 UUID)
$lockKey = 'my_lock';
$lockValue = uniqid('', true);  // 或使用 ramsey/uuid 更安全
$lockExpire = 30000;  // 毫秒

// 2. 尝试加锁(NX PX)
$acquired = $redis->set($lockKey, $lockValue, ['nx', 'px' => $lockExpire]);

if ($acquired) {
    echo "✅ 获取锁成功,执行业务逻辑...\n";

    // 模拟业务逻辑处理
    sleep(2);

    // 3. 解锁(Lua 脚本,确保只释放自己加的锁)
    $unlockScript = <<<LUA
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end
LUA;

    $result = $redis->eval($unlockScript, [$lockKey, $lockValue], 1);
    if ($result === 1) {
        echo "✅ 成功释放锁。\n";
    } else {
        echo "⚠️ 无法释放锁,可能已过期或不是当前持有者。\n";
    }
} else {
    echo "❌ 获取锁失败,稍后重试。\n";
}

?>

常见问题

1、为何不使用setnx,而是使用set


虽然 SETNX 是 Redis 最早用于加锁的方式,但相比 SET NX PX,它有以下几个重要的不足之处,这也是推荐使用 SET key value NX PX expire_time 的根本原因。


SETNX 的问题

1. 不能原子设置过期时间

  • 使用 SETNX 加锁后,还需要手动调用 EXPIRE 设置超时时间:

    if ($redis->setnx($lockKey, $lockValue)) {
        $redis->expire($lockKey, 30); // 单位:秒
    }
    

⚠️ 这是 两个命令,不是原子操作,意味着在 setnx 成功但 expire 还没执行时,如果程序崩溃或中断,锁将永不释放,造成死锁


2. 相比之下,SET NX PX 是原子操作

SET my_lock some_value NX PX 30000

这是 一个命令、一次操作、保证原子性,即设置键的同时设置过期时间,不存在中间状态。


✅ 总结

方式 是否原子 是否支持超时设置 是否推荐
SETNX + EXPIRE ❌ 否,两步 ✅ 支持但不安全 ❌ 不推荐
SET key value NX PX ✅ 是,一步 ✅ 支持且安全 ✅ 推荐

除非 Redis 版本过低(如 <2.6.12),否则应始终使用:

$redis->set($key, $value, ['nx', 'px' => $expire])
posted @ 2025-05-10 12:26  传VV说  阅读(146)  评论(0)    收藏  举报