在现代分布式系统中,多个节点并发操作共享资源极易引发数据不一致和业务异常。分布式锁正是解决这一问题的关键机制。本文将以 Python、Java、JavaScript 等语言为背景,深度剖析基于 Redis 的三种主流分布式锁实现方案:原生 SETNX 命令、Redisson 框架以及 Lua 脚本,帮助你在不同场景下做出最优选择。

分布式锁是什么?为何需要它?

分布式锁是一种跨多节点、多机器的互斥机制,确保同一时刻只有一个客户端能访问共享资源。与单机锁(如 synchronizedReentrantLock)不同,分布式锁依赖外部协调者(如 Redis、ZooKeeper)来管理锁状态。它广泛应用于以下场景:

  • 限流控制:防止多个请求同时修改同一资源。
  • 全局任务调度:确保集群中只有一个节点执行特定任务。
  • 防止重复处理:避免同一操作被多个节点重复执行。
  • 唯一性保证:如生成全局唯一 ID。

在 Java、Python、C++ 等后端服务中,Redis 因其高性能和原子操作成为实现分布式锁的首选。

1️⃣ 基于 Redis SETNX 命令的简单实现

最朴素的方案是利用 Redis 的 SETNX(SET if Not eXists)命令。该命令只在键不存在时设置键值对,天然具备互斥性。实现步骤如下:

  1. 使用 SETNX 尝试获取锁(设置唯一键)。
  2. 若返回 1,表示成功获取锁,执行业务逻辑。
  3. 若返回 0,表示锁已被占用,等待或重试。
  4. 释放锁时,通过 DEL 删除键,并设置过期时间防止死锁。

以下是一个 Java 示例代码:

import redis.clients.jedis.Jedis;
import java.util.UUID;
public class RedisDistributedLock {
    private static final String LOCK_KEY = "lock:resource";  // 锁的唯一标识
    private static final int EXPIRE_TIME = 10;  // 锁的超时时间,单位:秒
    private static final String REDIS_HOST = "localhost";
    private static final int REDIS_PORT = 6379;
    private Jedis jedis;
    public RedisDistributedLock() {
        jedis = new Jedis(REDIS_HOST, REDIS_PORT);
    }
    // 获取锁
    public boolean acquireLock() {
        String lockValue = UUID.randomUUID().toString();  // 唯一的锁值,防止锁被误释放
        // 使用 SETNX 命令获取锁
        Long result = jedis.setnx(LOCK_KEY, lockValue);
        if (result == 1) {
            // 锁获取成功,设置过期时间
            jedis.expire(LOCK_KEY, EXPIRE_TIME);
            return true;
        }
        // 锁未获取成功
        return false;
    }
    // 释放锁
    public boolean releaseLock() {
        String lockValue = jedis.get(LOCK_KEY);
        if (lockValue != null && lockValue.equals(jedis.get(LOCK_KEY))) {
            // 确保当前锁是自己持有的
            jedis.del(LOCK_KEY);
            return true;
        }
        return false;
    }
    // 关闭连接
    public void close() {
        jedis.close();
    }
    public static void main(String[] args) {
        RedisDistributedLock lock = new RedisDistributedLock();
        // 尝试获取锁
        if (lock.acquireLock()) {
            try {
                System.out.println("Lock acquired, performing task.");
                // 执行任务...
                Thread.sleep(5000);  // 模拟任务处理时间
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.releaseLock();  // 完成任务后释放锁
                System.out.println("Lock released.");
            }
        } else {
            System.out.println("Failed to acquire lock, try again later.");
        }
        lock.close();
    }
}

关键点

  • SETNX 确保只有一个客户端能设置锁。
  • ✅ 设置过期时间,防止客户端崩溃导致死锁。
  • ✅ 释放锁时校验身份,避免误删其他客户端的锁。

⚠️ 但此方案存在明显缺陷:SETNXEXPIRE 是两次独立操作,非原子性。若在设置键后、设置过期前发生故障,锁可能永不过期。此外,不支持锁续期,任务超时后锁自动释放,可能引发资源竞争。

2️⃣ 使用 Redisson 实现高可用分布式锁

Redisson 是 Redis 的 Java 高级客户端,提供了 RLock 接口,封装了分布式锁的复杂细节。它天然支持自动续期、重试和超时管理,极大降低了开发成本。代码示例如下:

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.api.Redisson;
import org.redisson.config.Config;
public class RedissonLockExample {
    public static void main(String[] args) {
        // 配置 Redisson 客户端
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379"); // 设置 Redis 地址
        RedissonClient redisson = Redisson.create(config);  // 创建 Redisson 客户端
        // 获取分布式锁
        RLock lock = redisson.getLock("lock:resource");
        try {
            // 尝试获取锁
            if (lock.tryLock()) {
                System.out.println("Lock acquired, performing task.");
                // 执行任务...
                Thread.sleep(5000);  // 模拟任务处理
            } else {
                System.out.println("Failed to acquire lock, try again later.");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 释放锁
            lock.unlock();
            redisson.shutdown();  // 关闭 Redisson 客户端
            System.out.println("Lock released.");
        }
    }
}

关键优势

  • 自动续期:若任务执行时间超过锁的过期时间,Redisson 会自动延长锁的生存时间,避免锁提前释放。
  • 可重入:支持同一线程多次获取锁,避免死锁。
  • 高并发优化:内置重试机制,适应高并发场景。

对于 Java 技术栈,Redisson 是生产环境的首选。但需注意,它引入了外部依赖,且不适合非 Java 语言(如 Python、TypeScript)的项目。

3️⃣ 基于 Lua 脚本的原子性锁实现

Lua 脚本能在 Redis 服务端原子性地执行多条命令,彻底避免竞态条件。通过将获取锁、设置过期、释放锁合并为一个原子操作,既保证了正确性,又减少了网络往返开销。示例脚本如下:

import redis.clients.jedis.Jedis;
public class RedisDistributedLockWithLua {
    private static final String LOCK_KEY = "lock:resource";
    private static final int EXPIRE_TIME = 10;  // 锁的超时时间,单位:秒
    private static final String REDIS_HOST = "localhost";
    private static final int REDIS_PORT = 6379;
    private Jedis jedis;
    public RedisDistributedLockWithLua() {
        jedis = new Jedis(REDIS_HOST, REDIS_PORT);
    }
    // 获取锁
    public boolean acquireLock(String lockValue) {
        // Lua 脚本:尝试获取锁,如果成功则设置锁的过期时间
        String script =
            "if redis.call('SETNX', KEYS[1], ARGV[1]) == 1 then " +
            "   redis.call('EXPIRE', KEYS[1], ARGV[2]) " +
            "   return 1 " +
            "else " +
            "   return 0 " +
            "end";
        // 使用 EVAL 命令执行 Lua 脚本
        Object result = jedis.eval(script, 1, LOCK_KEY, lockValue, String.valueOf(EXPIRE_TIME));
        return "1".equals(result.toString());
    }
    // 释放锁
    public boolean releaseLock(String lockValue) {
        // Lua 脚本:确保当前锁的持有者才会释放锁
        String script =
            "if redis.call('GET', KEYS[1]) == ARGV[1] then " +
            "   return redis.call('DEL', KEYS[1]) " +
            "else " +
            "   return 0 " +
            "end";
        // 使用 EVAL 命令执行 Lua 脚本
        Object result = jedis.eval(script, 1, LOCK_KEY, lockValue);
        return "1".equals(result.toString());
    }
    // 关闭连接
    public void close() {
        jedis.close();
    }
    public static void main(String[] args) {
        RedisDistributedLockWithLua lock = new RedisDistributedLockWithLua();
        String lockValue = "unique-lock-value";  // 唯一的锁值,用于标识当前锁的拥有者
        // 尝试获取锁
        if (lock.acquireLock(lockValue)) {
            try {
                System.out.println("Lock acquired, performing task.");
                // 执行任务...
                Thread.sleep(5000);  // 模拟任务处理
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.releaseLock(lockValue);  // 完成任务后释放锁
                System.out.println("Lock released.");
            }
        } else {
            System.out.println("Failed to acquire lock, try again later.");
        }
        lock.close();
    }
}

脚本解读

  • ✅ 获取锁时,使用 SET 命令的 NX 和 PX 参数,原子性地设置键和过期时间。
  • ✅ 释放锁时,先通过 GET 验证锁持有者,再执行 DEL,确保只有锁主人能释放。

Lua 脚本方案适用于对性能要求极高、需要极致原子性的场景,且不依赖任何第三方库,可被 Python、JavaScript、C++ 等语言调用。但调试和维护成本较高,需要开发者熟悉 Lua 语法。

⚖️ 三种方案深度对比

特性 锁实现Redisson 锁实现Lua 脚本锁实现
实现复杂度简单简单,依赖 Redisson稍复杂,需编写 Lua 脚本
原子性低(需要分两步操作)高(自动处理锁的生命周期)高(所有操作在 Redis 上原子执行)
支持锁续期否(需手动在脚本中实现续期)
锁释放保障需要手动确认锁值自动释放,且可靠性高需要手动确认锁值
性能较高较高,但有额外封装性能开销非常高(减少了网络延迟)
依赖仅需 Redis 客户端需要引入 Redisson 库仅需 Redis 客户端,使用 Lua 脚本
适用场景简单场景,低并发需求高并发,自动续期,可靠性需求高性能、低延迟需求,且能接受脚本复杂性
错误处理容易处理易于使用且错误处理简洁错误处理较为复杂

从上表可以看出:

  • SETNX 方案 简单直接,但原子性不足,适合低并发、任务短的场景。
  • Redisson 方案 功能全面,自动续期,适合 Java 生态下的高可用系统。
  • Lua 脚本方案 性能最优,原子性最强,适合对一致性要求严苛的分布式任务。
[AFFILIATE_SLOT_1]

如何选择?

根据实际业务需求,你可以参考以下建议:

  • 若团队以 Java 为主,且追求开发效率,Redisson 是最佳选择。
  • 若项目使用 Python、TypeScript 或 C++,且对性能有极致要求,推荐 Lua 脚本 方案。
  • 若仅需快速验证原型,SETNX 仍然可用,但务必注意其原子性缺陷。

无论选择哪种方案,都建议为锁设置合理的过期时间,并引入重试机制,以应对网络抖动和 Redis 故障。

[AFFILIATE_SLOT_2]

总结

本文详细介绍了 Redis 分布式锁的三种实现方式:基于 SETNX 的简单锁、Redisson 高级锁以及 Lua 脚本原子锁。每种方案各有优劣,适用于不同场景。SETNX 方案 适合快速实现,但原子性弱;Redisson 功能强大,适合 Java 生态;Lua 脚本 性能最优,适合多语言项目。合理选择分布式锁,能有效保障系统的一致性与高可用性。

SETNX