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. 业务逻辑

    • 在持有锁的情况下执行,确保同一时间只有一个线程执行临界区代码
相关推荐
憧憬成为web高手8 小时前
皮卡丘靶场速通--sql 2
数据库·sql·mybatis
段一凡-华北理工大学9 小时前
向量数据库实战:选型、调优与落地~系列文章12:文本分块策略实战:chunk_size 怎么选?重叠多少?
开发语言·数据库·后端·oracle·rust·工业智能体·高炉智能化
二宝哥9 小时前
CentOS 7.9 系统下 Redis 5.0.4 单机安装与源码编译安装指南
redis·centos
wWYy.9 小时前
分布式:数据复制
分布式
oradh11 小时前
Oracle XTTS实现跨版本迁移和升级(Oracle 11g单库升级至19C RAC集群)
数据库·oracle·11g升级19c·xtts跨版本迁移和升级
z1234567898612 小时前
2026最新两款AI编程工具深度对比实测
java·数据库·ai编程
程序猿DD12 小时前
一个 API Key,统一调用大模型、生图和联网搜索
数据库·网关
java_logo12 小时前
Docker Compose 部署 ClickHouse:快速搭建高性能列式数据库
数据库·clickhouse·docker·列式数据库·轩辕镜像·高性能数据库·clickhouse部署教程
TlSfoward13 小时前
抓包代理链路下的 TLS 指纹变化分析 TLSFOWARD抓包工具
数据库·爬虫·网络协议·搜索引擎·php
数智化管理手记13 小时前
账龄分析手工统计易遗漏?自动账龄分析工具怎么搭建
大数据·网络·数据库·人工智能·数据挖掘