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

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

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


什么是分布式锁?

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

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

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

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

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

  • 分布式唯一性保证:如生成全局唯一 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 是单线程的,SETNX 和 EXPIRE 操作是原子的,基本可以保证锁的正确性。

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

缺点:

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

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

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


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 脚本能力,开发复杂度较高。

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

相关推荐
IvorySQL4 小时前
PostgreSQL 日报|修复截断 zstd 备份检测(10 月 9 日)
数据库·postgresql
刘胡子大叔5 小时前
SQL 脚本的导入顺序
数据库·sql
代码什么用5 小时前
Spring对IoC的实现
数据库·spring
禾小西6 小时前
07丨Redis 哨兵机制:主库故障后,如何恢复服务?
java·开发语言·redis
hz567897 小时前
涉密视频会议设备配置指南:终端、音视频采集与配套设施选型
服务器·网络·数据库·安全·实时音视频·信息与通信·智能硬件
广州浮点FLOATLIC7 小时前
许可证服务器迁移后软件打不开:研发 IT 怎样定位连接问题
linux·服务器·数据库
程序员Sunday7 小时前
MySQL 为什么使用 B+ 树索引?把范围查询、回表和覆盖索引连起来
数据库·mysql
半杯咖啡半行码7 小时前
Qt开发实战:数据库、MV 模式、QProcess与串口通信全攻略
数据库·qt
小米里的大麦8 小时前
16 MySQL 事务
数据库·mysql
西柚小萌新8 小时前
【LLM&&AI应用开发 八股文】--4.3.Agent智能体(下)
java·开发语言·数据库