Skip to content

Redis 分布式锁原理?

约 1283 字大约 4 分钟

Redis阿里美团

2025-4-15

⭐ 题目日期:

阿里 - 2024/8/21,美团 - 2025/4/12

📝 题解:

Redis 分布式锁是分布式系统中实现互斥访问共享资源的常用方案,其核心目标是通过 Redis 的单线程原子性操作保证多个进程/服务对共享资源的互斥访问。以下是 Redis 分布式锁的原理、实现细节及常见问题的解析:


1. 基本原理

Redis 分布式锁的本质是利用 Redis 的 SET 命令的原子性(结合 NXPX 参数)实现锁的互斥性,并通过唯一标识(如 UUID)保证锁的归属权。

关键命令

SET lock_key unique_value NX PX 30000
  • NX:仅当键不存在时才设置(保证互斥性)。
  • PX:设置键的过期时间(防止死锁)。
  • unique_value:唯一标识(如 UUID),用于验证锁的归属,避免误删其他客户端的锁。

2. 实现步骤

(1) 获取锁

客户端尝试通过 SET 命令申请锁:

  • 若返回 OK,表示获取成功,获得锁并设置过期时间。
  • 若返回 nil,表示锁已被其他客户端持有,需等待或重试。

(2) 释放锁

客户端释放锁时需验证锁的归属权(通过唯一标识),并通过 Lua 脚本保证原子性:

if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end
  • 为何需要 Lua 脚本?防止 GETDEL 操作之间的锁过期或被其他客户端修改,导致误删。

(3) 锁超时与续期

  • 自动超时:Redis 锁默认有过期时间(如 30 秒),即使客户端崩溃,锁也会自动释放。
  • 手动续期(看门狗机制):客户端在持有锁期间定期(如每隔 10 秒)重置过期时间,避免业务未完成时锁过期。

3. 核心问题与解决方案

(1) 死锁问题

  • 原因:客户端获取锁后崩溃,未释放锁。
  • 解决:为锁设置合理的过期时间(PX 参数)。

(2) 误删锁

  • 原因:客户端 A 释放锁时误删客户端 B 的锁(如 A 因 GC 停顿导致锁过期后被 B 获取)。
  • 解决:使用唯一标识验证锁的归属权。

(3) 锁续期问题

  • 原因:业务执行时间超过锁的过期时间。
  • 解决:实现看门狗机制(如 Redisson 库的 LockWatchdog),后台线程定期续期。

(4) 集群环境下的问题

  • 主从切换风险:Redis 主节点宕机后,从节点可能未同步锁信息,导致锁丢失。
  • 解决:使用 RedLock 算法(Redis 官方推荐的分布式锁算法)。

4. RedLock 算法(增强版分布式锁)

RedLock 旨在解决 Redis 单节点或主从架构下的锁不可靠问题,核心思想是在多个独立的 Redis 节点上获取锁,确保多数节点成功才算锁获取成功。

步骤

  1. 获取当前时间戳(精确到毫秒)。
  2. 依次向 N 个独立 Redis 节点请求锁,使用相同的 keyunique_value,设置相同的超时时间(如 5 秒)。
  3. 统计成功获取锁的节点数
    • 若多数节点(≥ N/2 + 1)成功,且总耗时小于锁的有效时间,则锁获取成功。
    • 否则,向所有节点发送释放锁的请求。
  4. 锁的有效时间 = 初始锁超时时间 - 获取锁的总耗时。

优缺点

  • 优点:提高锁的可靠性,容忍部分节点故障。
  • 缺点
    • 实现复杂,需管理多个 Redis 实例。
    • 性能较低(需多次网络通信)。
    • 仍无法完全解决网络分区(脑裂)问题。

5. 常见实现库

  • Redisson(Java):提供完善的分布式锁实现,支持自动续期、可重入锁等特性。
  • StackExchange.Redis(.NET):结合 Lua 脚本实现基本分布式锁。
  • Python (redis-py):通过 SET 命令和 Lua 脚本手动实现。

6. 示例代码(Python)

import redis
import uuid
import time

class RedisLock:
    def __init__(self, redis_client, lock_key, expire_time=30000):
        self.redis = redis_client
        self.lock_key = lock_key
        self.expire_time = expire_time
        self.identifier = str(uuid.uuid4())

    def acquire(self, timeout=10):
        end = time.time() + timeout
        while time.time() < end:
            # 尝试获取锁
            if self.redis.set(self.lock_key, self.identifier, nx=True, px=self.expire_time):
                return True
            time.sleep(0.001)  # 短暂等待后重试
        return False

    def release(self):
        # 使用 Lua 脚本保证原子性
        script = """
        if redis.call("get", KEYS[1]) == ARGV[1] then
            return redis.call("del", KEYS[1])
        else
            return 0
        end
        """
        return self.redis.eval(script, 1, self.lock_key, self.identifier)

7. 使用场景与权衡

  • 适用场景
    • 高并发下对共享资源的互斥访问(如秒杀库存扣减)。
    • 分布式任务调度(确保任务仅被执行一次)。
  • 不适用场景
    • 对一致性要求极高的场景(如金融交易)建议使用 ZooKeeper 或 etcd。
    • 对性能要求极高的场景(Redis 锁的网络开销可能成为瓶颈)。

总结

Redis 分布式锁的核心是通过原子操作和唯一标识实现互斥性与安全性,但在实际应用中需注意 锁续期、集群可靠性、时钟漂移 等问题。对于高可用场景,建议使用 RedLock 算法或成熟的库(如 Redisson),并根据业务需求权衡一致性与性能。