
在分布式系统中,当多个服务实例需要竞争同一资源(如秒杀库存、分布式任务调度)时,"分布式锁"是保证操作原子性的核心工具。
Redis凭借高性能、易部署的特点,成为实现分布式锁的主流选择。但很多开发者只知道"用SET NX加锁",却踩过"锁过期业务没执行完""主从切换锁丢失"等坑。
本文将从"基础实现→优化演进→Redisson高级版"逐步拆解,附完整Java代码和生产环境避坑指南,帮你彻底掌握Redis分布式锁的实战要点。
一、分布式锁的核心要求(面试必背)
一个可靠的分布式锁必须满足4个核心条件,少一个都可能出问题:
- 互斥性:同一时间只有一个服务能获取锁;
- 安全性:不能出现"释放别人的锁"的情况;
- 可用性:锁获取/释放过程不能阻塞,避免单点故障;
- 防死锁:即使服务宕机,锁也能自动释放(如设置过期时间)。
Redis分布式锁的实现,本质上是围绕这4个条件逐步优化的过程。
二、基础版:SET NX EX实现(最简化方案)
1. 核心原理
利用Redis的SET命令扩展参数:
bash
SET lock_key unique_value NX EX 10
NX:只有当lock_key不存在时才设置成功(保证互斥性);EX 10:设置10秒过期时间(防死锁,避免服务宕机后锁永远不释放);unique_value:每个请求的唯一标识(如UUID,用于后续释放锁时验证"自己的锁",保证安全性)。
2. Java代码实现(Spring Boot)
java
@Component
public class BasicRedisLock {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String LOCK_PREFIX = "lock:";
private static final long LOCK_EXPIRE = 10; // 锁过期时间(秒)
// 获取锁
public String tryLock(String resource) {
String lockKey = LOCK_PREFIX + resource;
String uniqueValue = UUID.randomUUID().toString(); // 生成唯一标识
// 执行SET NX EX命令
Boolean success = redisTemplate.opsForValue()
.setIfAbsent(lockKey, uniqueValue, LOCK_EXPIRE, TimeUnit.SECONDS);
return success != null && success ? uniqueValue : null; // 成功返回唯一值,失败返回null
}
// 释放锁(注意:此实现有坑,下文会优化)
public void unlock(String resource, String uniqueValue) {
String lockKey = LOCK_PREFIX + resource;
// 先判断是否是自己的锁,再删除(注意:这两步非原子操作,有风险)
String value = redisTemplate.opsForValue().get(lockKey);
if (uniqueValue.equals(value)) {
redisTemplate.delete(lockKey);
}
}
}
3. 存在的问题(面试高频坑)
- 问题1:释放锁的"判断+删除"非原子操作
假设锁的过期时间是10秒,当线程A执行到"判断是自己的锁"后,还没来得及删除,锁过期自动释放,线程B已获取新锁。此时线程A删除的是线程B的锁,导致互斥性失效。 - 问题2:锁过期了,业务还没执行完
若业务逻辑执行时间超过10秒,锁会提前释放,其他线程会获取到锁,导致"并发操作同一资源"。
三、优化版1:Lua脚本保证释放锁原子性
1. 核心优化点
用Lua脚本将"判断锁标识"和"删除锁"合并为一个原子操作,避免中间被打断。Redis会将整个Lua脚本作为单个命令执行,保证原子性。
2. Lua脚本释放锁(关键代码)
lua
-- 脚本逻辑:只有当lock_key的值等于unique_value时,才删除锁
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
3. 优化后的Java代码
java
@Component
public class LuaRedisLock {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String LOCK_PREFIX = "lock:";
private static final long LOCK_EXPIRE = 10; // 秒
// 释放锁的Lua脚本
private static final String UNLOCK_LUA = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
// 获取锁(同基础版)
public String tryLock(String resource) {
String lockKey = LOCK_PREFIX + resource;
String uniqueValue = UUID.randomUUID().toString();
Boolean success = redisTemplate.opsForValue()
.setIfAbsent(lockKey, uniqueValue, LOCK_EXPIRE, TimeUnit.SECONDS);
return success != null && success ? uniqueValue : null;
}
// 用Lua脚本释放锁(原子操作)
public void unlock(String resource, String uniqueValue) {
String lockKey = LOCK_PREFIX + resource;
// 执行Lua脚本
redisTemplate.execute(
new DefaultRedisScript<>(UNLOCK_LUA, Integer.class),
Collections.singletonList(lockKey), // KEYS[1]
uniqueValue // ARGV[1]
);
}
}
4. 仍存在的问题
解决了"误删锁"的问题,但**"锁过期业务未完成"的问题仍存在**。例如:锁过期时间10秒,而业务逻辑需要15秒,第10秒锁释放后,其他线程会获取到锁,导致并发冲突。
四、优化版2:"看门狗"自动续期(解决锁过期问题)
1. 核心思路
当业务未执行完时,启动一个"看门狗"线程,定期(如每隔5秒)检查锁是否仍持有,若持有则延长锁的过期时间(如续期至10秒),避免锁提前释放。
2. Java代码实现(自定义看门狗)
java
@Component
public class WatchDogRedisLock {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String LOCK_PREFIX = "lock:";
private static final long LOCK_EXPIRE = 10; // 初始过期时间(秒)
private static final long WATCH_DOG_INTERVAL = 5; // 看门狗续期间隔(秒)
private static final String UNLOCK_LUA = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
// 存储看门狗线程,用于释放锁时停止
private final Map<String, ScheduledFuture<?>> watchDogMap = new ConcurrentHashMap<>();
// 获取锁并启动看门狗
public String tryLock(String resource) {
String lockKey = LOCK_PREFIX + resource;
String uniqueValue = UUID.randomUUID().toString();
Boolean success = redisTemplate.opsForValue()
.setIfAbsent(lockKey, uniqueValue, LOCK_EXPIRE, TimeUnit.SECONDS);
if (success != null && success) {
// 启动看门狗线程,定期续期
ScheduledFuture<?> watchDog = Executors.newScheduledThreadPool(1)
.scheduleAtFixedRate(() -> {
// 续期:只有锁存在且是自己的,才延长过期时间
redisTemplate.execute(
new DefaultRedisScript<>(
"if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('expire', KEYS[1], ARGV[2]) else return 0 end",
Integer.class
),
Collections.singletonList(lockKey),
uniqueValue,
String.valueOf(LOCK_EXPIRE)
);
}, WATCH_DOG_INTERVAL, WATCH_DOG_INTERVAL, TimeUnit.SECONDS);
watchDogMap.put(lockKey + ":" + uniqueValue, watchDog);
return uniqueValue;
}
return null;
}
// 释放锁并停止看门狗
public void unlock(String resource, String uniqueValue) {
String lockKey = LOCK_PREFIX + resource;
String key = lockKey + ":" + uniqueValue;
// 执行Lua脚本释放锁
redisTemplate.execute(
new DefaultRedisScript<>(UNLOCK_LUA, Integer.class),
Collections.singletonList(lockKey),
uniqueValue
);
// 停止看门狗线程
ScheduledFuture<?> watchDog = watchDogMap.get(key);
if (watchDog != null) {
watchDog.cancel(true);
watchDogMap.remove(key);
}
}
}
3. 优化效果
- 解决了"锁过期业务未完成"的问题:只要业务还在执行,看门狗会自动续期;
- 业务执行完调用
unlock时,会停止看门狗,避免资源浪费。
五、高级版:Redisson分布式锁(生产环境首选)
手动实现的分布式锁存在线程池管理、异常处理等细节问题,生产环境更推荐使用成熟框架------Redisson。它封装了"自动续期""可重入锁""公平锁"等高级特性,且经过大量实践验证。
1. Redisson的核心优势
- 自动续期:内置看门狗,默认每30秒续期一次,无需手动实现;
- 可重入性:支持同一线程多次获取锁(类似ReentrantLock);
- 公平锁:避免线程饥饿,按请求顺序获取锁;
- 异常安全:自动处理锁释放、线程中断等异常场景。
2. Redisson集成与使用(Spring Boot)
(1)引入依赖
xml
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.20.0</version>
</dependency>
(2)配置Redisson
yaml
spring:
redis:
host: 127.0.0.1
port: 6379
(3)代码实现(可重入锁示例)
java
@Service
public class RedissonLockService {
@Autowired
private RedissonClient redissonClient;
public void seckill(Long goodsId) {
String lockKey = "lock:seckill:" + goodsId;
// 获取可重入锁
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试获取锁:最多等待3秒,10秒后自动释放(未手动释放时)
boolean locked = lock.tryLock(3, 10, TimeUnit.SECONDS);
if (!locked) {
throw new RuntimeException("获取锁失败,请重试");
}
// 执行业务逻辑(如扣减库存)
deductStock(goodsId);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 释放锁(只有持有锁的线程才能释放)
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
private void deductStock(Long goodsId) {
// 实际库存扣减逻辑
System.out.println("扣减商品" + goodsId + "库存成功");
}
}
3. Redisson高级特性(按需使用)
- 公平锁 :
RLock fairLock = redissonClient.getFairLock(lockKey);------避免线程饥饿,适合对顺序性要求高的场景; - 读写锁 :
RReadWriteLock rwLock = redissonClient.getReadWriteLock(lockKey);------读锁共享,写锁互斥,适合"多读少写"场景; - 红锁(Redlock) :
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);------解决集群环境下的锁丢失问题(下文详解)。
六、集群场景下的坑:主从切换导致锁丢失
1. 问题描述
Redis主从复制是异步的:当主节点获取锁后,未同步到从节点就宕机,哨兵会将从节点升级为主节点。此时新主节点中没有锁信息,其他线程可重新获取锁,导致"一把锁被两个线程持有"。
2. 解决方案:Redlock算法(极端场景使用)
Redlock算法的核心是"多节点加锁":
- 向多个独立的Redis节点(如5个)请求加锁;
- 当超过半数节点(如3个)加锁成功,且总耗时不超过锁过期时间的1/3时,认为锁获取成功;
- 释放锁时,向所有节点发送释放请求。
Redisson已实现Redlock,代码示例:
java
// 创建5个独立的Redis节点锁
RLock lock1 = redissonClient1.getLock(lockKey);
RLock lock2 = redissonClient2.getLock(lockKey);
RLock lock3 = redissonClient3.getLock(lockKey);
RLock lock4 = redissonClient4.getLock(lockKey);
RLock lock5 = redissonClient5.getLock(lockKey);
// 组合为红锁
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3, lock4, lock5);
// 尝试获取锁:最多等待3秒,10秒自动释放
boolean locked = redLock.tryLock(3, 10, TimeUnit.SECONDS);
if (locked) {
try {
// 业务逻辑
} finally {
redLock.unlock();
}
}
3. 注意事项
Redlock算法虽能解决集群锁丢失问题,但复杂度高、性能损耗大(需访问多个节点)。大部分场景下,可接受"主从切换瞬间的锁丢失风险"(概率极低),仅在金融级强一致性场景下使用Redlock。
七、生产环境避坑指南(实战经验)
- 锁的粒度要小 :避免用"大锁"(如
lock:order),应细化到资源ID(如lock:order:123),减少锁竞争; - 过期时间要合理:初始过期时间应大于业务最大耗时(如业务需5秒,设10秒),预留冗余;
- 避免长时间持有锁:锁内逻辑尽量精简(如只做库存扣减,不包含复杂计算),减少锁占用时间;
- 失败重试要有限制:获取锁失败时,重试次数不宜过多(如3次),避免线程阻塞;
- 监控锁状态 :用Redis的
KEYS lock:*或监控工具(如Prometheus)跟踪锁的持有时间,及时发现异常。
八、面试高频题&标准答案
- 问:Redis分布式锁和ZooKeeper分布式锁有什么区别?
答:① 实现方式:Redis用SET NX,ZooKeeper用临时节点;② 性能:Redis更高(内存操作);③ 可靠性:ZooKeeper更强(主从同步是同步的,无锁丢失风险);④ 适用场景:Redis适合高性能场景,ZooKeeper适合高可靠场景。 - 问:Redisson的看门狗原理是什么?
答:当获取锁后,Redisson会启动一个定时任务(默认每30秒),检查锁是否仍被当前线程持有,若是则延长锁的过期时间(默认续期至30秒),直到业务执行完释放锁。 - 问:如何解决Redis主从切换导致的锁丢失问题?
答:① 接受低概率风险(大部分场景);② 用Redlock算法(多节点加锁,超过半数成功才算获取锁);③ 结合本地锁(如先获取本地锁,再获取分布式锁,减少分布式锁竞争)。 - 问:分布式锁为什么需要uniqueValue?
答:防止"误删别人的锁"。当锁过期后,若没有uniqueValue,线程A可能删除线程B的锁,导致互斥性失效。uniqueValue确保只有锁的持有者才能释放锁。
九、总结
Redis分布式锁的演进路径是"解决问题→发现新问题→再优化"的过程:
- 基础版:
SET NX EX实现互斥和防死锁,但存在误删锁风险; - 优化版1:Lua脚本保证释放锁原子性,解决误删问题;
- 优化版2:看门狗自动续期,解决锁过期业务未完成问题;
- 高级版:Redisson封装所有特性,生产环境首选;
- 集群场景:极端情况用Redlock,多数场景可简化。
掌握这些演进逻辑和避坑点,就能在面试和实战中应对自如。
至此,Redis核心面试系列(线程模型→高并发→数据安全→缓存问题→分布式锁)已全部更新完毕。建议结合实际场景多动手实践,真正理解每个机制的"为什么",而非死记硬背。
如果觉得系列文章有用,欢迎收藏+转发,帮助更多开发者攻克Redis难点~