Redis分布式锁怎么实现?

wen python案例 186

本文目录导读:

Redis分布式锁怎么实现?

  1. 核心基础:SET NX + EX/PX(最简方案)
  2. 生产级推荐:Redisson(Java生态)或类似客户端
  3. 高可用场景:Redlock 算法
  4. 总结建议

实现Redis分布式锁,最经典且目前被广泛推荐的方式是基于 Redlock 算法的变体,或者直接使用单节点 Redis 配合恰当的原子命令。

下面我为你详细介绍两种主流实现方式:最简可行方案(基于 SET NX EX) 和 官方推荐的 Redisson 方案,最后分析高可用场景下的 Redlock 算法逻辑。

核心基础:SET NX + EX/PX(最简方案)

这是最基础的实现,也是理解所有高级方案的前提,核心是利用 Redis 的原子命令 SET key value NX EX seconds。

  • 命令含义:
    • NX: Not eXists,只有 key 不存在时才设置成功,这是“互斥”。
    • EX/PX: 设置过期时间,这是“自动解锁”,防止死锁。
  • 为什么不能用SETNX + Expire分开执行? 因为这两个操作不是原子的,如果设置完 SETNX 后,程序崩溃了,Expire 没执行,锁就永远不会释放,造成死锁。

简单代码实现(伪代码)

import redis
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
lock_key = "my_lock"
lock_value = "unique_request_id_123"  # 非常重要:每个客户端生成的唯一值
timeout = 10  # 锁自动过期时间,单位秒
def acquire_lock():
    # 原子操作:只在key不存在时设置,并指定过期时间
    result = r.set(lock_key, lock_value, nx=True, ex=timeout)
    if result:
        print("获取锁成功")
        return True
    else:
        print("获取锁失败")
        return False
def release_lock():
    # 重要:使用Lua脚本保证原子性,只释放自己的锁
    lua_script = """
    if redis.call('get', KEYS[1]) == ARGV[1] then
        return redis.call('del', KEYS[1])
    else
        return 0
    end
    """
    # 执行Lua脚本,传递key和唯一值
    result = r.eval(lua_script, 1, lock_key, lock_value)
    if result == 1:
        print("释放锁成功")
    else:
        print("释放锁失败,可能已经超时或不是自己的锁")
# 使用示例
if acquire_lock():
    try:
        # 执行业务逻辑...
        pass
    finally:
        release_lock()

需要解决的几个关键问题

  1. 防止误删锁: 锁的 Value 必须是一个唯一标识(如 UUID + 线程ID),释放锁时,需要先检查 Value 是否是自己设置的值,再删除,这正是 Lua 脚本的作用。
  2. 原子性: 获取锁和检查+释放锁都必须是原子操作。
  3. 锁的超时与业务执行时间:
    • 问题: 如果业务执行时间超过了锁的过期时间,锁会自动释放,其他线程就会拿到锁,导致并发问题。
    • 解决方案: 锁续期(Watch Dog),启动一个后台线程,在锁快过期时(比如过期时间过了1/3),自动为锁续期,这在对时间敏感的业务中至关重要,Redisson 客户端内置了这个功能。

生产级推荐:Redisson(Java生态)或类似客户端

手动实现 Lua 脚本和看门狗(Watch Dog)并不简单,官方推荐使用 Redisson(Java)这样的成熟客户端,它封装了分布式锁的所有细节。

Redisson 的核心优势

  1. 自动续期(Watch Dog): 默认的锁超时是 30 秒,只要业务线程没有结束,看门狗会自动每 10 秒将锁的过期时间重置为 30 秒,有效防止业务执行过长导致锁自动释放。
  2. 重入锁(Reentrant Lock): 同一个线程可以多次获取同一个锁,支持重入。
  3. 多种锁模式: 公平锁、读写锁、红锁等。

Redisson 代码示例(Java)

// 1. 创建配置
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
// 2. 创建 RedissonClient
RedissonClient redisson = Redisson.create(config);
// 3. 获取分布式锁
RLock lock = redisson.getLock("myLock");
try {
    // 4. 加锁(默认等待时间0,锁有效期30秒,会自动续期)
    //    也可以指定等待时间:lock.tryLock(100, 10, TimeUnit.SECONDS);
    lock.lock();
    // 5. 执行业务逻辑
    System.out.println("执行业务...");
    Thread.sleep(60000); // 假设业务需要60秒,锁会自动续期
} finally {
    // 6. 释放锁
    lock.unlock();
}

在 Java 生态中,直接用 Redisson 是最省心、最安全的做法,它已经处理好了原子的加解锁、唯一标识、自动续期等问题。


高可用场景:Redlock 算法

上面的方案基于单点 Redis Master,如果这个 Master 宕机,锁就丢失了,为了在 Redis 集群(Master/Replica)中保证锁的高可用,Redis 作者提出了 Redlock 算法。

Redlock 算法的核心逻辑

它不是在一个 Redis 实例上加锁,而是在 N 个(奇数,5 个) 独立、不相关的 Redis Master 实例上同时加锁,所有 Master 之间没有数据同步。

  1. 获取当前时间 T1。
  2. 依次向 5 个 Master 发送加锁请求,每个请求带有相同的 Key 和 Value,以及一个很小的超时时间(远小于锁的自动过期时间)。
  3. 判断是否加锁成功: 如果客户端在大多数(N/2 + 1,即 3/5) 实例上成功加锁,且总耗时(当前时间 - T1) 小于锁的过期时间,则认为锁被成功获取。
  4. 释放锁: 向所有 5 个实例发送删除锁命令(即使之前加锁失败的也要发,清除残留)。

适用场景与争议

  • 适用场景: 对锁的安全性要求极高,且 Redis 作为基础设施必须保证高可用的场景。
  • 争议: 分布式系统专家 Martin Kleppmann 曾撰文指出 Redlock 存在时钟跳跃、GC 暂停等导致锁失效的安全隐患,Redis 作者也进行了反驳。
  • 实际建议: 对于绝大部分业务,单 Master 配合合适的策略(如 Redisson 看门狗)已经足够,只有当你的系统极度敏感、需要跨主从自动故障转移仍保证锁互斥时,再考虑 Redlock。

总结建议

场景 推荐方案 理由
大多数业务 单 Master + Redisson(Java)/ 或自己实现的 Lua 脚本 简单、高效、性能好,利用看门狗解决超时问题,利用Lua解决原子性问题。
高可用、对安全要求极高 Redlock 算法(或 Redisson MultiLock) 跨多个独立实例,容忍单点故障,但实现复杂,有一定理论争议,只在必要时使用。

核心要点回顾:

  1. 加锁用 SET NX EX 保持原子性。
  2. Value 用唯一 ID,释放锁用 Lua 脚本检查身份。
  3. 用看门狗(Watch Dog) 避免业务超时导致锁释放。
  4. 优先使用成熟的客户端库(如 Redisson)。

抱歉,评论功能暂时关闭!

友情链接攻略窝 · 礼包窝 · 配置窝 · 换算窝 · 算法窝 · 查文窝 · 手册窝 · 问答窝 · 全文搜 · 手册库
相关站点礼包窝 · 查文窝