Redis 分布式锁如何保证同一时间只有一个客户端持有锁

一、核心原理

Redis 通过 原子性操作唯一标识 来保证同一时间只有一个客户端持有锁。

关键点:

  1. 原子性:加锁操作必须是不可分割的,一次执行完成,不能被其他命令插入。
  2. 唯一性:锁的 value 必须能唯一标识持有者(通常是 UUID + 线程 ID)。
  3. 互斥性:只有当锁不存在时才能加锁,防止多个客户端同时持有。

二、加锁流程(保证互斥的关键步骤)

1. 使用 SET NX EX

bash 复制代码
SET lock_key unique_value NX EX expire_time
  • NX (Not eXists):只有当 lock_key 不存在时才设置成功
  • EX expire_time:设置过期时间,防止死锁
  • unique_value:唯一标识锁持有者

原子性保证

  • Redis 是单线程执行命令的,SET NX EX 是一个单条命令,在执行过程中不会被其他命令打断
  • 这意味着多个客户端同时执行加锁命令时,Redis 会按顺序处理,只有第一个执行的客户端能成功

2. 成功与失败的判断

  • 成功 → 返回 "OK",表示当前客户端持有锁
  • 失败 → 返回 null,表示锁已被其他客户端持有

3. 释放锁时的安全性

释放锁必须保证只能删除自己加的锁

Lua 复制代码
if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end
  • 先判断锁的 value 是否等于当前客户端的唯一标识
  • 如果相等 → 删除锁
  • 如果不相等 → 不删除(防止误删其他客户端的锁)

三、底层机制保证互斥性

  1. Redis 单线程模型

    • Redis 处理命令是单线程的,命令之间不会并发执行
    • 多个客户端同时请求加锁时,Redis 会按顺序处理,只有第一个满足条件的请求能成功
  2. SET NX EX 的原子性

    • SET 命令带 NXEX 参数时,是一个原子操作
    • 不会出现"先判断再设置"这种可能被其他命令插入的情况
  3. 唯一标识防误删

    • 即使锁过期被其他客户端抢到,旧客户端也无法删除新客户端的锁,因为 value 不匹配

四、边界情况与优化

1. 锁过期导致并发

  • 如果业务执行时间超过锁的过期时间,锁会提前释放,其他客户端可能加锁成功,导致多个客户端同时执行临界区代码
  • 优化:使用自动续期(Watchdog)机制,在锁快过期时延长过期时间

2. Redis 主从延迟

  • 如果使用 Redis 主从架构,主节点加锁成功,但从节点延迟同步,可能导致其他客户端在从节点加锁成功
  • 优化:使用 RedLock 算法,在多个独立 Redis 节点加锁,必须多数节点成功才算加锁成功

五、面试回答示例

Redis 分布式锁通过 SET key value NX EX expire_time 保证同一时间只有一个客户端持有锁。

其中 NX 保证只有当锁不存在时才能加锁,EX 设置过期时间防止死锁,value 用唯一标识防止误删。

Redis 是单线程执行命令的,SET NX EX 是原子操作,多个客户端同时加锁时,只有第一个执行的客户端能成功。

释放锁时用 Lua 脚本判断 value 是否匹配,保证只能删除自己加的锁。

如果业务执行时间可能超过锁的过期时间,可以用自动续期机制避免锁提前释放。

Java 代码示例:Redis 分布式锁应用

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

public class RedisDistributedLockExample {

    // Redis 连接信息
    private static final String REDIS_HOST = "127.0.0.1";
    private static final int REDIS_PORT = 6379;

    // 锁的 key
    private static final String LOCK_KEY = "my_distributed_lock";

    // 锁的过期时间(秒)
    private static final int EXPIRE_TIME = 10;

    // Jedis 客户端
    private Jedis jedis;

    public RedisDistributedLockExample() {
        this.jedis = new Jedis(REDIS_HOST, REDIS_PORT);
    }

    /**
     * 尝试获取分布式锁
     * @param lockValue 锁的唯一标识(UUID)
     * @return 是否加锁成功
     */
    public boolean tryLock(String lockValue) {
        String result = jedis.set(LOCK_KEY, lockValue, "NX", "EX", EXPIRE_TIME);
        return "OK".equals(result);
    }

    /**
     * 释放分布式锁(Lua 脚本保证原子性)
     * @param lockValue 锁的唯一标识
     * @return 是否释放成功
     */
    public boolean unlock(String lockValue) {
        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,
                Collections.singletonList(LOCK_KEY),
                Collections.singletonList(lockValue));
        return Long.valueOf(1).equals(result);
    }

    /**
     * 模拟业务逻辑
     */
    public void doBusiness() {
        System.out.println(Thread.currentThread().getName() + " 正在执行业务逻辑...");
        try {
            Thread.sleep(5000); // 模拟耗时操作
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        System.out.println(Thread.currentThread().getName() + " 业务逻辑执行完成");
    }

    public static void main(String[] args) {
        RedisDistributedLockExample lockExample = new RedisDistributedLockExample();

        // 每个线程使用不同的锁标识
        String lockValue = UUID.randomUUID().toString();

        if (lockExample.tryLock(lockValue)) {
            try {
                System.out.println(Thread.currentThread().getName() + " 获取锁成功");
                lockExample.doBusiness();
            } finally {
                if (lockExample.unlock(lockValue)) {
                    System.out.println(Thread.currentThread().getName() + " 释放锁成功");
                } else {
                    System.out.println(Thread.currentThread().getName() + " 释放锁失败(可能锁已过期或被其他线程持有)");
                }
            }
        } else {
            System.out.println(Thread.currentThread().getName() + " 获取锁失败,稍后重试");
        }
    }
}

代码说明

  1. 加锁

    • 使用 SET key value NX EX expire_time 保证原子性
    • NX:只有当 key 不存在时才设置成功
    • EX:设置过期时间,防止死锁
    • value:唯一标识锁持有者(UUID)
  2. 释放锁

    • 使用 Lua 脚本保证判断 + 删除的原子性
    • 只有锁的持有者才能删除锁,防止误删
  3. 业务逻辑

    • 在持有锁的情况下执行,确保同一时间只有一个线程执行临界区代码
相关推荐
呆呆小金人3 小时前
SQL优化实战:从慢查询到高效查询
大数据·数据库·数据仓库·sql·数据库开发·etl·etl工程师
无聊的小坏坏3 小时前
Redis 从基础到实战
数据库·redis·缓存
爱隐身的官人4 小时前
SQL注入过滤绕过fuzz字典
数据库·sql
小马哥编程4 小时前
【软考架构】案例分析-系统设计与建模:数据流图DFD与数据字典
java·数据库·架构·统一建模语言
sibylyue4 小时前
Spring编程式事务和声明式事务
java·数据库·mysql
人类二号4 小时前
MySQL安装及启用(社区版)
数据库·mysql
飞鱼&5 小时前
Kafka(文件)数据存储、清理机制、高性能设计
分布式·kafka
StarRocks_labs5 小时前
告别 Hadoop,拥抱 StarRocks!政采云数据平台升级之路
大数据·数据库·starrocks·hadoop·存算分离
重整旗鼓~5 小时前
32.图片上传功能
java·redis