Redis 分布式锁如何保证同一时间只有一个客户端持有锁
一、核心原理
Redis 通过 原子性操作 和 唯一标识 来保证同一时间只有一个客户端持有锁。
关键点:
- 原子性:加锁操作必须是不可分割的,一次执行完成,不能被其他命令插入。
- 唯一性:锁的 value 必须能唯一标识持有者(通常是 UUID + 线程 ID)。
- 互斥性:只有当锁不存在时才能加锁,防止多个客户端同时持有。
二、加锁流程(保证互斥的关键步骤)
1. 使用 SET NX EX
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. 释放锁时的安全性
释放锁必须保证只能删除自己加的锁:
if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end- 先判断锁的 value 是否等于当前客户端的唯一标识
- 如果相等 → 删除锁
- 如果不相等 → 不删除(防止误删其他客户端的锁)
三、底层机制保证互斥性
Redis 单线程模型
- Redis 处理命令是单线程的,命令之间不会并发执行
- 多个客户端同时请求加锁时,Redis 会按顺序处理,只有第一个满足条件的请求能成功
SET NX EX 的原子性
SET命令带NX和EX参数时,是一个原子操作- 不会出现“先判断再设置”这种可能被其他命令插入的情况
唯一标识防误删
- 即使锁过期被其他客户端抢到,旧客户端也无法删除新客户端的锁,因为 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 分布式锁应用
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;// 锁的 keyprivate 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() + " 获取锁失败,稍后重试");}}
}
代码说明
加锁
- 使用
SET key value NX EX expire_time保证原子性 NX:只有当 key 不存在时才设置成功EX:设置过期时间,防止死锁value:唯一标识锁持有者(UUID)
- 使用
释放锁
- 使用 Lua 脚本保证判断 + 删除的原子性
- 只有锁的持有者才能删除锁,防止误删
业务逻辑
- 在持有锁的情况下执行,确保同一时间只有一个线程执行临界区代码
