写在前面
在分布式系统中,多个服务实例可能同时访问共享资源,如何保证同一时刻只有一个实例能操作资源?分布式锁是解决这个问题的经典方案。今天我们来深入学习Redis分布式锁的实现原理和最佳实践。

文章目录
-
- 写在前面
- 一、为什么需要分布式锁
-
- [1.1 单机锁的局限性](#1.1 单机锁的局限性)
- [1.2 分布式锁的需求](#1.2 分布式锁的需求)
- [1.3 分布式锁的特性](#1.3 分布式锁的特性)
- 二、SETNX实现分布式锁
-
- [2.1 基本原理](#2.1 基本原理)
- [2.2 基础实现](#2.2 基础实现)
- [2.3 加锁失败重试](#2.3 加锁失败重试)
- [2.4 SETNX方案的缺陷](#2.4 SETNX方案的缺陷)
- 三、Redisson实现分布式锁
-
- [3.1 Redisson简介](#3.1 Redisson简介)
- [3.2 Redisson基本使用](#3.2 Redisson基本使用)
- [3.3 看门狗机制](#3.3 看门狗机制)
- [3.4 可重入锁](#3.4 可重入锁)
- [3.5 读写锁](#3.5 读写锁)
- [3.6 公平锁](#3.6 公平锁)
- 四、Redlock算法
-
- [4.1 Redlock原理](#4.1 Redlock原理)
- [4.2 Redisson实现Redlock](#4.2 Redisson实现Redlock)
- [4.3 Redlock的争议](#4.3 Redlock的争议)
- 五、锁的续期
-
- [5.1 为什么需要续期](#5.1 为什么需要续期)
- [5.2 手动续期](#5.2 手动续期)
- [5.3 Redisson自动续期](#5.3 Redisson自动续期)
- 六、踩坑提醒
-
- [6.1 锁超时释放](#6.1 锁超时释放)
- [6.2 误删其他线程的锁](#6.2 误删其他线程的锁)
- [6.3 主从切换丢锁](#6.3 主从切换丢锁)
- [6.4 锁的可重入问题](#6.4 锁的可重入问题)
- [6.5 常见问题汇总](#6.5 常见问题汇总)
- 七、分布式锁对比
-
- [7.1 Redis vs Zookeeper vs Database](#7.1 Redis vs Zookeeper vs Database)
- [7.2 选择建议](#7.2 选择建议)
- 八、面试高频考点
-
- [8.1 Redis分布式锁的实现方式?](#8.1 Redis分布式锁的实现方式?)
- [8.2 如何保证Redis分布式锁的可靠性?](#8.2 如何保证Redis分布式锁的可靠性?)
- [8.3 Redis分布式锁和Zookeeper分布式锁的区别?](#8.3 Redis分布式锁和Zookeeper分布式锁的区别?)
- [8.4 如何解决锁续期问题?](#8.4 如何解决锁续期问题?)
- 九、参考资料
- 十、互动话题
一、为什么需要分布式锁
1.1 单机锁的局限性
实际场景:电商系统部署了3个实例,用户购买商品时需要扣减库存,如何保证库存不会被超卖?
单机锁的问题:
java
// 单机锁只能保证单个JVM内的线程安全
public void deductStock(Long productId) {
synchronized(this) {
// 只能保证单个实例内的线程安全
// 多实例部署时,synchronized失效
int stock = getStock(productId);
if (stock > 0) {
updateStock(productId, stock - 1);
}
}
}
1.2 分布式锁的需求
| 场景 | 说明 |
|---|---|
| 库存扣减 | 防止超卖 |
| 订单创建 | 防止重复下单 |
| 定时任务 | 防止重复执行 |
| 秒杀活动 | 控制并发数量 |
| 分布式事务 | 保证原子性 |
1.3 分布式锁的特性
经验之谈:一个可靠的分布式锁需要满足以下条件。
| 特性 | 说明 |
|---|---|
| 互斥性 | 任意时刻只有一个客户端持有锁 |
| 防死锁 | 锁必须有超时机制,避免死锁 |
| 唯一性 | 只有持有锁的客户端才能释放锁 |
| 容错性 | Redis部分节点宕机,锁仍然可用 |
二、SETNX实现分布式锁
2.1 基本原理
核心命令:SET key value NX PX milliseconds
- NX:只有key不存在时才设置成功
- PX:设置过期时间(毫秒)
redis
# 加锁
SET lock:product:123 "uuid-xxx" NX PX 30000
# 解锁(需要使用Lua脚本保证原子性)
EVAL "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 lock:product:123 "uuid-xxx"
2.2 基础实现
java
public class RedisLock {
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 加锁
* @param key 锁的key
* @param value 锁的值(唯一标识,用于释放锁时校验)
* @param expireTime 过期时间(毫秒)
* @return 是否加锁成功
*/
public boolean lock(String key, String value, long expireTime) {
return Boolean.TRUE.equals(
redisTemplate.opsForValue()
.setIfAbsent(key, value, expireTime, TimeUnit.MILLISECONDS)
);
}
/**
* 解锁
* @param key 锁的key
* @param value 锁的值
* @return 是否解锁成功
*/
public boolean unlock(String key, String value) {
// 使用Lua脚本保证原子性
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
RedisScript<Long> redisScript = RedisScript.of(script, Long.class);
Long result = redisTemplate.execute(redisScript, Collections.singletonList(key), value);
return result != null && result == 1L;
}
}
2.3 加锁失败重试
实际场景:加锁失败时,需要重试等待,而不是直接返回失败。
java
public boolean lockWithRetry(String key, String value, long expireTime, long waitTime) {
long startTime = System.currentTimeMillis();
while (true) {
// 尝试加锁
if (lock(key, value, expireTime)) {
return true;
}
// 检查是否超时
if (System.currentTimeMillis() - startTime > waitTime) {
return false;
}
// 等待一段时间后重试
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
}
2.4 SETNX方案的缺陷
踩坑提醒:SETNX方案在Redis主从切换时可能出现问题。
| 问题 | 说明 |
|---|---|
| 锁超时释放 | 业务执行时间超过锁过期时间 |
| 主从切换丢锁 | 主节点加锁后宕机,从节点未同步 |
| 无法续期 | 长时间任务无法自动续期 |
| 单点故障 | 单Redis实例故障导致锁不可用 |
三、Redisson实现分布式锁
3.1 Redisson简介
经验之谈:Redisson是Redis官方推荐的Java分布式锁实现,提供了丰富的分布式对象和服务。
Redisson优势:
- 自动续期(看门狗机制)
- 可重入锁
- 公平锁
- 读写锁
- 联锁
- 红锁(RedLock)
3.2 Redisson基本使用
java
// 1. 配置Redisson
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379")
.setPassword("password");
RedissonClient redisson = Redisson.create(config);
// 2. 获取锁
RLock lock = redisson.getLock("lock:product:123");
try {
// 3. 加锁(带自动续期)
lock.lock();
// 或者指定过期时间
// lock.lock(30, TimeUnit.SECONDS);
// 尝试加锁,最多等待100秒,锁过期时间10秒
// boolean acquired = lock.tryLock(100, 10, TimeUnit.SECONDS);
// 4. 执行业务逻辑
doBusiness();
} finally {
// 5. 释放锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
3.3 看门狗机制
核心原理:Redisson的看门狗机制会自动为锁续期,防止业务执行时间超过锁过期时间。
看门狗工作流程:
加锁成功 → 启动看门狗线程
↓
每隔10秒(过期时间/3)检查
↓
如果锁还被当前线程持有 → 续期30秒
↓
业务执行完成 → 停止看门狗
看门狗源码分析:
java
// Redisson内部实现
private void scheduleExpirationRenewal(long threadId) {
// 每10秒执行一次续期
Timeout task = commandExecutor.getConnectionManager()
.newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
// 续期Lua脚本
RFuture<Boolean> future = renewExpirationAsync(threadId);
future.onComplete((res, e) -> {
if (res) {
// 续期成功,继续调度
scheduleExpirationRenewal(threadId);
}
});
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
}
3.4 可重入锁
java
public void methodA() {
RLock lock = redisson.getLock("myLock");
lock.lock();
try {
// 调用methodB,同一把锁可以重入
methodB();
} finally {
lock.unlock();
}
}
public void methodB() {
RLock lock = redisson.getLock("myLock");
lock.lock();
try {
// 执行业务逻辑
} finally {
lock.unlock();
}
}
3.5 读写锁
实际场景:读多写少的场景,使用读写锁提高并发性能。
java
// 获取读写锁
RReadWriteLock rwLock = redisson.getReadWriteLock("rwLock:product");
// 读锁(共享锁)
RLock readLock = rwLock.readLock();
readLock.lock();
try {
// 读取数据,多个线程可以同时读
Product product = getProduct(id);
} finally {
readLock.unlock();
}
// 写锁(排他锁)
RLock writeLock = rwLock.writeLock();
writeLock.lock();
try {
// 写入数据,独占资源
updateProduct(product);
} finally {
writeLock.unlock();
}
3.6 公平锁
java
// 公平锁:按照请求顺序获取锁
RLock fairLock = redisson.getFairLock("fairLock:order");
fairLock.lock();
try {
// 按照请求顺序执行
processOrder();
} finally {
fairLock.unlock();
}
四、Redlock算法
4.1 Redlock原理
面试高频考点:Redlock是Redis作者提出的分布式锁算法,用于解决单点故障问题。
Redlock核心思想:
- 获取当前时间戳
- 按顺序向N个Redis节点请求加锁
- 计算获取锁消耗的时间
- 如果在大多数节点(N/2+1)加锁成功,且消耗时间小于锁过期时间,则加锁成功
- 否则,向所有节点请求解锁
Redlock示意图:
┌─────────────┐
│ Client │
└──────┬──────┘
│
├──→ Redis1 (加锁成功)
│
├──→ Redis2 (加锁成功)
│
├──→ Redis3 (加锁失败)
│
├──→ Redis4 (加锁成功)
│
└──→ Redis5 (加锁成功)
结果:4/5成功,超过半数,加锁成功
4.2 Redisson实现Redlock
java
// 配置多个Redis节点
Config config1 = new Config();
config1.useSingleServer().setAddress("redis://redis1:6379");
Config config2 = new Config();
config2.useSingleServer().setAddress("redis://redis2:6379");
Config config3 = new Config();
config3.useSingleServer().setAddress("redis://redis3:6379");
RedissonClient client1 = Redisson.create(config1);
RedissonClient client2 = Redisson.create(config2);
RedissonClient client3 = Redisson.create(config3);
// 创建Redlock
RLock lock1 = client1.getLock("lock:product");
RLock lock2 = client2.getLock("lock:product");
RLock lock3 = client3.getLock("lock:product");
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
try {
// 加锁
redLock.lock();
// 或者尝试加锁
// boolean acquired = redLock.tryLock(100, 10, TimeUnit.SECONDS);
// 执行业务
doBusiness();
} finally {
redLock.unlock();
}
4.3 Redlock的争议
踩坑提醒:Redlock算法存在争议,需要根据实际场景选择。
| 支持观点 | 反对观点 |
|---|---|
| 解决单点故障 | 时钟跳变可能导致问题 |
| 高可用性 | 实现复杂 |
| 官方推荐 | 性能开销大 |
Martin Kleppmann的质疑:
- 网络延迟可能导致锁失效
- 时钟同步问题影响锁的正确性
- GC暂停可能导致锁过期
Redis作者的回应:
- 建议使用物理时钟而非NTP同步
- 锁过期时间要考虑网络延迟
- 实际场景中问题很少出现
五、锁的续期
5.1 为什么需要续期
实际场景:业务执行时间不确定,如果锁过期时间太短,业务还没执行完锁就释放了。
问题场景:
时间轴:
├──────┼──────┼──────┼──────┤
0s 10s 20s 30s 40s
│ │ │ │
加锁 锁过期 │ │
↓ │ │
其他线程获取锁 │
业务执行完成
↓
尝试解锁(但锁已经不是自己的了)
5.2 手动续期
java
public class RedisLockWithRenewal {
private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
public boolean lockWithRenewal(String key, String value, long expireTime) {
// 加锁
if (!lock(key, value, expireTime)) {
return false;
}
// 启动续期任务
scheduler.scheduleAtFixedRate(() -> {
// 续期Lua脚本
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('pexpire', KEYS[1], ARGV[2]) " +
"else " +
" return 0 " +
"end";
redisTemplate.execute(RedisScript.of(script, Long.class),
Collections.singletonList(key), value, String.valueOf(expireTime));
}, expireTime / 3, expireTime / 3, TimeUnit.MILLISECONDS);
return true;
}
}
5.3 Redisson自动续期
经验之谈:推荐使用Redisson的看门狗机制,自动续期更可靠。
java
RLock lock = redisson.getLock("myLock");
// 不指定过期时间,启用看门狗自动续期
lock.lock();
try {
// 业务执行时间可能很长
Thread.sleep(60000); // 60秒
} finally {
lock.unlock();
}
六、踩坑提醒
6.1 锁超时释放
踩坑提醒:业务执行时间超过锁过期时间,锁自动释放,其他线程获取锁,导致并发问题。
解决方案:
- 设置合理的过期时间
- 使用Redisson看门狗自动续期
- 监控业务执行时间
6.2 误删其他线程的锁
踩坑提醒:线程A的锁过期释放,线程B获取锁,线程A执行完释放了线程B的锁。
错误示例:
java
// 错误:直接删除锁,可能删除其他线程的锁
redisTemplate.delete("lock:product");
正确做法:
java
// 正确:使用Lua脚本校验锁的归属
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
redisTemplate.execute(RedisScript.of(script, Long.class),
Collections.singletonList(key), value);
6.3 主从切换丢锁
踩坑提醒:主节点加锁后宕机,从节点未同步锁信息,导致锁丢失。
解决方案:
- 使用Redlock算法
- 使用Redis Cluster
- 使用Zookeeper等一致性协议
6.4 锁的可重入问题
踩坑提醒:同一个线程多次获取同一把锁,需要支持可重入。
Redisson可重入锁原理:
redis
# Redis存储结构
HSET lock:myLock <threadId>:1 <count>
# value存储线程ID和重入次数
6.5 常见问题汇总
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 锁无法释放 | 客户端宕机 | 设置过期时间 |
| 误删锁 | 锁过期被其他线程获取 | 使用唯一标识校验 |
| 主从切换丢锁 | 异步复制 | 使用Redlock |
| 死锁 | 锁未释放 | 设置过期时间+监控 |
| 锁续期失败 | 网络问题 | 重试机制 |
七、分布式锁对比
7.1 Redis vs Zookeeper vs Database
| 对比项 | Redis | Zookeeper | Database |
|---|---|---|---|
| 性能 | 高 | 中 | 低 |
| 可靠性 | 中 | 高 | 高 |
| 实现复杂度 | 简单 | 中等 | 简单 |
| 一致性 | 最终一致 | 强一致 | 强一致 |
| 适用场景 | 高并发 | 高可靠 | 低并发 |
7.2 选择建议
| 场景 | 推荐方案 |
|---|---|
| 高并发、允许偶尔失败 | Redis分布式锁 |
| 高可靠、强一致性 | Zookeeper分布式锁 |
| 低并发、简单场景 | 数据库乐观锁 |
| 混合场景 | Redis + 数据库双保险 |
八、面试高频考点
8.1 Redis分布式锁的实现方式?
答案:
- SET NX EX:使用SET命令的NX和EX参数实现原子加锁
redis
SET lock:key value NX EX 30
-
SETNX + EXPIRE:先SETNX再EXPIRE(非原子,不推荐)
-
Lua脚本:使用Lua脚本保证原子性
-
Redisson:使用Redisson框架,提供完善的分布式锁实现
8.2 如何保证Redis分布式锁的可靠性?
答案:
-
设置过期时间:防止死锁
-
唯一标识:使用UUID等唯一标识,防止误删
-
Lua脚本:解锁时使用Lua脚本保证原子性
-
看门狗机制:自动续期,防止业务未完成锁过期
-
Redlock算法:多节点部署,防止单点故障
-
监控告警:监控锁的持有时间和等待时间
8.3 Redis分布式锁和Zookeeper分布式锁的区别?
答案:
| 对比项 | Redis | Zookeeper |
|---|---|---|
| 实现方式 | SET NX + 过期时间 | 临时顺序节点 |
| 一致性 | 最终一致 | 强一致 |
| 性能 | 高 | 中 |
| 锁释放 | 主动删除或过期 | Session断开自动释放 |
| 适用场景 | 高并发 | 高可靠 |
8.4 如何解决锁续期问题?
答案:
-
预估业务时间:设置足够长的过期时间
-
看门狗机制:Redisson自动续期
-
手动续期:定时任务续期
-
监控告警:监控业务执行时间
九、参考资料
十、互动话题
- 你的项目中使用哪种分布式锁方案?遇到过什么问题?
- Redis分布式锁和Zookeeper分布式锁你会如何选择?
- 对于秒杀场景,分布式锁是否是最佳方案?还有什么替代方案?
欢迎在评论区分享你的经验和看法!
下期预告:Day14我们将学习Redis性能优化,深入理解慢查询分析、内存优化、网络优化等技巧。