Redis 实现分布式锁

Redis 实现分布式锁是一个经典且广泛应用的方案,其核心是利用 Redis 单线程执行命令的特性(对于单实例或正确配置的分片集群)和原子性操作来保证互斥性。

一、 核心思想

  1. 互斥性: 同一时刻,只有一个客户端能成功获取锁。
  2. 安全性: 锁只能由加锁的客户端释放。
  3. 容错性: 即使持有锁的客户端崩溃,也要有机制(通常是超时)保证锁最终能被释放,避免死锁。
  4. 避免误删: 释放锁时需验证持有者身份。

二、推荐实现方式 (基于 SET 命令的 NXPX 选项)

这是目前最可靠、最被广泛接受的 Redis 分布式锁实现方式(Redlock 算法适用于更严格场景,但更复杂)。

加锁
bash 复制代码
SET lock_key unique_value NX PX milliseconds
  • lock_key: 锁的名称(字符串)。
  • unique_value: 一个唯一 的随机值(如 UUID)。这是保证安全释放锁的关键,用于标识锁的持有者。
  • NX: 表示 "Set if Not eXists"。只有 lock_key 不存在时,设置才会成功(获取锁)。
  • PX milliseconds: 设置锁的过期时间(毫秒)。这是避免死锁的关键。即使客户端崩溃,锁也会在过期后自动释放。

返回值:

  • OK: 表示加锁成功。
  • (nil): 表示加锁失败(锁已被其他客户端持有)。

示例:

bash 复制代码
SET myLock 8b1e6c53-ae2a-4fe6-875d-106e3d7a9e01 NX PX 10000
解锁 (使用 Lua 脚本保证原子性)

解锁操作必须是原子的:需要先检查 unique_value 是否匹配,再删除 key。这必须通过 Lua 脚本完成,因为 Redis 命令本身不具备条件删除的原子性。

Lua 脚本 (unlock.lua):

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

执行解锁:

bash 复制代码
EVAL "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 lock_key unique_value
  • KEYS[1]: 锁的名称(lock_key)。
  • ARGV[1]: 加锁时设置的 unique_value
  • 1: 表示后面有 1 个 key (KEYS[1])。

返回值:

  • 1: 解锁成功(找到 key 且 value 匹配,并成功删除)。
  • 0: 解锁失败(key 不存在或 value 不匹配)。这通常发生在锁已过期被释放,或尝试释放非自己持有的锁。

三、关键注意事项与优化

  1. 过期时间 (PX) 设置:

    • 设置得太短:业务逻辑还没执行完,锁就过期了,可能导致其他客户端获取锁并操作共享资源,造成数据不一致。
    • 设置得太长:持有锁的客户端崩溃后,其他客户端需要等待更长时间才能获取锁。
    • 建议: 根据业务逻辑的最坏情况执行时间来设置,并适当增加一些缓冲时间。评估业务耗时非常重要。
  2. 锁续期 (Watchdog):

    • 如果业务逻辑执行时间可能超过锁的初始过期时间,需要实现锁续期机制。
    • 客户端在获取锁成功后,启动一个后台线程(看门狗),定期(例如在过期时间的 1/3 处)检查锁是否仍持有(通过 GET lock_key 并与自己的 unique_value 比较),如果持有,则重新设置过期时间(EXPIRE lock_key new_ttlPEXPIRE lock_key new_ttl)。
    • 库支持: Redisson 等库内置了看门狗机制。手动实现需注意线程管理和续期失败处理。
  3. 获取锁失败处理:

    • 直接返回失败: 简单,适用于非关键或快速重试场景。
    • 循环重试: 在一定的超时时间内,间隔一定时间(如 50-200ms,最好带随机抖动)不断尝试获取锁。需要设置最大重试次数或总超时时间,避免长时间阻塞。
    • 订阅通知 (Pub/Sub): 监听锁释放事件 (DEL lock_key),收到通知后再尝试获取。效率更高,但实现稍复杂,且存在通知丢失风险(客户端在等待通知期间断开连接)。Redisson 使用了这种方式。
  4. 集群环境 (Redis Sentinel / Redis Cluster):

    • 主从异步复制: 在主节点获取锁成功后,如果主节点在将锁信息同步给从节点之前发生故障切换,新的主节点可能没有这个锁的信息,导致另一个客户端也能在新主上获取同一个锁,破坏互斥性。
    • Redlock 算法: Redis 作者 Antirez 提出的算法,旨在在非强一致性保证 的 Redis 集群(如 Sentinel、Cluster)中提供更可靠的分布式锁。它要求客户端依次尝试向 N 个 (通常为 5,且为奇数) 独立的 Redis 主节点 申请锁(使用相同的 SET NX PX 命令),当且仅当从超过半数 (N/2 + 1) 的节点上都成功获取锁,并且总耗时小于锁的有效时间时,才算获取成功。释放锁时也需要向所有节点发送释放请求。
    • Redlock 争议: Redlock 的实现复杂,性能开销大,且在极端网络分区场景下仍存在争议(如 GC 停顿导致多个客户端同时认为自己持有锁)。对于大多数要求不是极端严格的场景,使用单 Redis 实例或 Sentinel/Cluster 配合上述 SET NX PX + Lua 解锁方案,并接受在主从切换时有极低概率失效的风险,通常是可接受的权衡。 如果业务要求绝对可靠,需考虑更严谨的协调服务如 ZooKeeper 或 etcd。
  5. unique_value 的重要性: 绝对不能用固定值、线程ID或进程ID。必须使用足够随机且全局唯一的标识(如 UUID)。这是防止客户端误删其他客户端锁的唯一保障。

四、总结最佳实践

  1. 使用 SET key random_value NX PX ttl 命令进行加锁。
  2. 使用 Lua 脚本(比较 value 并删除)进行解锁。
  3. 为每个锁设置一个合理的过期时间
  4. 对于可能长时间持有锁 的业务,实现锁续期 (看门狗) 机制。
  5. 处理获取锁失败:根据业务场景选择重试、等待或直接失败
  6. 理解集群环境的局限性: 在主从切换时存在失效风险。评估业务是否可接受此风险,如不能则考虑 Redlock(复杂且有争议)或其他分布式协调系统。
  7. 使用成熟的客户端库(如 Java 的 Redisson)可以简化上述所有复杂逻辑的实现。

五、简单 Java 示例 (使用 Jedis)

java 复制代码
import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;
import java.util.UUID;

public class RedisDistributedLock {

    private Jedis jedis;
    private String lockKey;
    private String lockValue; // 唯一标识
    private long expireTime; // 锁过期时间(ms)

    public RedisDistributedLock(Jedis jedis, String lockKey, long expireTime) {
        this.jedis = jedis;
        this.lockKey = lockKey;
        this.expireTime = expireTime;
        this.lockValue = UUID.randomUUID().toString(); // 生成唯一标识
    }

    // 尝试获取锁 (非阻塞)
    public boolean tryLock() {
        SetParams params = SetParams.setParams().nx().px(expireTime);
        String result = jedis.set(lockKey, lockValue, params);
        return "OK".equals(result);
    }

    // 释放锁
    public boolean unlock() {
        // 使用 Lua 脚本保证原子性: 检查值并删除
        String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(luaScript, 1, lockKey, lockValue);
        return Long.valueOf(1L).equals(result); // 1 表示删除成功
    }

    // 简单重试获取锁 (示例,生产环境需更完善)
    public boolean lockWithRetry(int maxRetries, long sleepMillis) throws InterruptedException {
        int retryCount = 0;
        while (retryCount < maxRetries) {
            if (tryLock()) {
                return true;
            }
            retryCount++;
            Thread.sleep(sleepMillis);
        }
        return false;
    }
}

重要提醒: 此示例非常基础。生产环境请使用成熟的库(如 Redisson)或仔细处理边界条件、异常、连接管理、集群模式、锁续期等复杂问题。

相关推荐
黄雪超2 小时前
Kafka——Kafka 线上集群部署方案怎么做?
大数据·分布式·kafka
LucianaiB2 小时前
AI 时代的分布式多模态数据处理实践:我的 ODPS 实践之旅、思考与展望
大数据·数据仓库·人工智能·分布式·odps
Kookoos3 小时前
ABP VNext + 多级缓存架构:本地 + Redis + CDN
redis·缓存·微服务·架构·abp vnext
长风破浪会有时呀4 小时前
Redis 命令总结
数据库·redis·缓存
lifallen4 小时前
Flink Exactly Once 和 幂等
java·大数据·数据结构·数据库·分布式·flink
张先shen5 小时前
Redis的高可用性与集群架构
java·redis·面试·架构
码里看花‌7 小时前
基于 Redis 实现高并发滑动窗口限流:Java实战与深度解析
java·开发语言·redis
程序员JerrySUN7 小时前
一文理解缓存的本质:分层架构、原理对比与实战精粹
java·linux·开发语言·数据库·redis·缓存·架构
西岭千秋雪_7 小时前
RabbitMQ队列的选择
笔记·分布式·学习·rabbitmq·ruby