Redis 实现分布式锁的三种方式

Redis 实现分布式锁的三种方式

在现代分布式系统中,多个节点同时操作共享资源是常见的情况。这种并发访问如果不加以控制,可能会导致数据不一致、业务异常等问题。因此,分布式锁成为了确保分布式系统中各节点协调一致、避免资源冲突的一个重要工具。本文将介绍三种常用的分布式锁实现方式:基于 Redis 的 SETNX 锁实现基于 Redisson 实现的分布式锁 以及 使用 Redis Lua 脚本的分布式锁实现


什么是分布式锁?

分布式锁是一种在分布式系统中,确保同一时刻只有一个节点能够访问共享资源的机制。与传统的单机锁(如 synchronized)不同,分布式锁跨越多个机器、节点,通过一个外部协调者来管理锁。常见的分布式锁实现工具包括 RedisZooKeeperConsul 等。

分布式锁广泛应用于以下场景:

  • 限流控制:防止多个请求同时修改同一资源。

  • 全局任务调度:在多个节点中确保只有一个节点执行特定的任务。

  • 防止重复处理:确保同一操作不会被多个节点重复执行。

  • 分布式唯一性保证:如生成全局唯一 ID。


1. 基于 Redis SETNX 实现分布式锁

Redis 是最常见的分布式锁实现工具之一。我们可以通过 Redis 的 SETNX 命令来实现一个基本的分布式锁。SETNX 命令的作用是 只有在键不存在时设置该键的值,因此可以用来确保在同一时刻只有一个节点能够获取锁。

实现步骤:

  1. 使用 SETNX 命令尝试获取锁(设置某个键值对)。

  2. 如果获取成功,表示该节点获得了锁,可以进行后续操作。

  3. 如果获取失败,表示锁已被其他节点占用,当前节点需要等待或重试。

  4. 设置锁的过期时间,防止死锁的发生。

基于 SETNX 实现代码(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 确保只有一个节点能够获取到锁。

  • 锁设置了过期时间,避免由于异常导致的死锁。

  • del 只有在确认当前持有锁的客户端才会释放锁。


2. 使用 Redisson 实现分布式锁

Redisson 是基于 Redis 提供的高层次 Java 客户端,它简化了分布式锁的实现。Redisson 提供了 RLock 接口来管理分布式锁,使得开发者无需手动处理底层的细节。

Redisson 的优势在于其高效性和易用性,能够自动处理锁的过期、重试、续期等功能。

Redisson 分布式锁代码示例

复制代码
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.");
        }
    }
}
关键点:
  • RLock 是 Redisson 提供的分布式锁对象。

  • tryLock() 方法可以用来尝试获取锁,获取成功返回 true,否则返回 false

  • Redisson 自动处理了锁的超时和重试等逻辑,避免手动管理锁状态。


3. 使用 Lua 脚本实现分布式锁

在 Redis 中,Lua 脚本能够确保多条 Redis 命令的原子性执行。通过 Lua 脚本,我们可以把获取锁、设置过期时间和释放锁等操作合并成一个原子操作,避免了竞态条件问题。

复制代码
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();
    }
}

解释 Lua 脚本:

  1. 获取锁SETNX 命令保证只有一个客户端能成功设置锁,同时使用 EXPIRE 设置锁的过期时间。

  2. 释放锁:通过 Lua 脚本确认当前客户端持有锁,防止其他客户端误释放锁。


对比三种 Redis 分布式锁实现方式

在分布式系统中,我们可以通过 Redis 实现分布式锁,保证多个节点在访问共享资源时的互斥性。本文介绍了三种常见的分布式锁实现方式:基于 Redis SETNX 命令的锁基于 Redisson 实现的锁 、以及 基于 Redis Lua 脚本的锁。每种实现方式有其优缺点,根据系统的不同需求,开发者可以选择合适的方式。接下来,我们将对这三种方式进行对比,帮助大家做出更明智的选择。


1. 基于 Redis SETNX 命令实现分布式锁

实现原理:

使用 Redis 的 SETNX(SET if Not eXists)命令来设置一个唯一的锁键,如果该键不存在,表示成功获取锁;如果该键已经存在,表示锁已被占用。然后,使用 EXPIRE 命令为锁设置过期时间,防止死锁。

优点:

  • 简单易懂:实现思路简单,代码量少。

  • 高效 :由于 Redis 是单线程的,SETNXEXPIRE 操作是原子的,基本可以保证锁的正确性。

  • 无依赖:只需要一个基础的 Redis 客户端(如 Jedis 或 Lettuce)即可,不需要引入额外的库。

缺点:

  • 操作不原子 :尽管 SETNX 本身是原子操作,但获取锁和设置过期时间是两次独立的操作,存在被中断的风险(例如网络延迟、Redis 节点重启等)。

  • 不支持锁续期:当任务执行时间长时,不支持锁续期:当任务执行时间长时,锁过期可能导致其他节点误获取锁,进而引发资源竞争、数据不一致等问题。没有机制来自动续期锁。

  • 可能的竞态条件 :由于 SETNXEXPIRE 是分开的操作,若在获取锁后未及时设置过期时间,可能导致锁在任务执行完之前过期,进而导致其他客户端误获取锁,造成资源竞争或数据不一致。


2. 使用 Redisson 实现分布式锁

实现原理:

Redisson 是基于 Redis 提供的高层次 Java 客户端,它通过 RLock 接口来管理分布式锁。Redisson 提供了更高层次的 API,自动处理锁的超时、重试、续期等问题。

优点:

  • 易用性高:Redisson 提供了丰富的分布式锁接口,开发者只需要关心锁的获取和释放,而无需关心底层细节。

  • 自动续期:Redisson 支持自动续期功能,如果任务执行时间超过锁的过期时间,Redisson 会自动延长锁的生存时间,避免锁过期导致的死锁。

  • 高效与可靠性:Redisson 通过 Redis 实现高效的分布式锁,且内置了重试机制,适应高并发场景。

  • 功能丰富:除了锁,Redisson 还提供了分布式集合、分布式队列等数据结构,适用于更多场景。

缺点:

  • 依赖 Redisson:需要额外引入 Redisson 库,增加了项目的复杂度和依赖。

  • 性能损耗:虽然 Redisson 提供了很高的抽象,但在某些场景下,其封装的操作可能会引入额外的性能损耗。

  • 限制于 Java:Redisson 是为 Java 提供的客户端,不适合其他语言的开发者使用。


3. 使用 Redis Lua 脚本实现分布式锁

实现原理:

Lua 脚本可以在 Redis 服务器端原子地执行多个命令。通过 Lua 脚本,我们将获取锁、设置过期时间和释放锁的操作合并为一个原子操作,避免了竞争条件的发生。

优点:

  • 原子性:Lua 脚本在 Redis 服务器端执行,避免了在客户端与 Redis 之间的多次往返操作,确保锁的获取和过期时间的设置是原子性的。

  • 避免竞态条件:通过 Lua 脚本,获取锁和设置过期时间操作合并为一个原子操作,避免了竞态条件问题。

  • 减少网络开销:通过 Lua 脚本将多个 Redis 操作合并为一个操作,减少了网络延迟,提升了性能。

缺点:

  • 代码复杂 :相比于 SETNX 命令和 Redisson,Lua 脚本需要编写和调试,开发者需要了解 Lua 脚本的语法和 Redis 的命令。

  • 可读性差:Lua 脚本在 Redis 上执行,不如 Redisson 这样的高级 API 直观,调试和维护相对困难。

  • 错误处理复杂:Lua 脚本会在 Redis 服务器端执行,错误处理较为复杂。如果出现脚本执行错误,排查会更麻烦。


三者对比

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

总结

根据实际需求,开发者可以选择不同的分布式锁实现方式:

  • 基于 SETNX 的实现适合于简单的场景,且对性能要求较高时,可以快速实现锁功能,但存在较低的原子性和不支持续期的缺点。

  • Redisson 实现适合需要高并发和高可用的应用,能够自动续期、处理复杂的分布式锁需求,且易于使用。但会增加外部依赖。

  • Lua 脚本实现适合于对性能要求极高、需要保证原子性和减少网络延迟的场景,且不依赖外部库,但需要一定的 Lua 脚本能力,开发复杂度较高。

选择合适的分布式锁实现方式,能够有效保证系统的一致性和高可用性。

相关推荐
菜鸟小九1 小时前
redis原理篇(基本数据结构)
数据结构·数据库·redis
没有bug.的程序员1 小时前
电商秒杀系统深度进阶:高并发流量建模、库存零超卖内核与 Redis+MQ 闭环
数据库·redis·缓存·高并发·电商秒杀·流量建模·库存零超卖
先做个垃圾出来………2 小时前
Python常见文件操作
linux·数据库·python
轩情吖2 小时前
MySQL库的操作
android·数据库·mysql·oracle·字符集·数据库操作·编码集
LaughingZhu2 小时前
Product Hunt 每日热榜 | 2026-02-25
数据库·人工智能·经验分享·神经网络·chatgpt
Flobby5292 小时前
深入理解 MySQL 锁:从全局锁到死锁检测
数据库·后端·mysql
是小崔啊2 小时前
MySQL22 - 分库分表的聚合问题
数据库
玖雨y2 小时前
【DDIA】存储和查询
数据库·后端·存储·ddia
蒸蒸yyyyzwd2 小时前
redis实战学习笔记p1-12
数据库·笔记