Redis面试题 - 说说Redisson分布式锁的原理?
回答重点
Redisson是基于Redis实现的分布式锁,实际上是使用Redis的原子操作来确保多线程、多进程或多节点系统中,只有一个线程能获得锁,避免并发操作导致的数据不一致问题。
1 锁的获取:
- Redisson 使用 Lua 脚本,利用exists+hexists+hincrby命令来保证只有一个线程能成 功设置键(表示获得锁)。
- 同时,Redisson会通过pexpire命令为锁设置过期时间,防止因宕机等原因导致锁无法释放(即死锁问题)。
2 锁的续期:
- 为了防止锁在持有过程中过期导致其他线程抢占锁,Redisson实现了锁自动续期的功能。持有
锁的线程会定期续期,即更新锁的过期时间,确保任务没有完成时锁不会失效。
3 锁的释放:
- 锁释放时,Redisson也是通过Lua脚本保证释放操作的原子性。利用hexists+de1确保只有持有锁的线程才能释放锁,防止误释放锁的情况。
- Lua脚本同时利用publish命令,广播唤醒其它等待的线程。
4 可重入锁:
- Redisson支持可重入锁,持有锁的线程可以多次获取同一把锁而不会被阻塞。具体是利用 Redis中的哈希结构,哈希中的key为线程ID,如果重入则value+1,如果释放则value 1减到0说明锁被释放了,则del锁。
一、分布式锁概述
在分布式系统中,当多个进程或线程需要访问共享资源时,为了保证数据一致性,我们需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁。
分布式锁需要满足以下基本要求:
- 互斥性:同一时刻只有一个客户端能持有锁
- 避免死锁:即使持有锁的客户端崩溃,锁也能被释放
- 容错性:只要大部分Redis节点正常运行,客户端就能获取和释放锁
二、Redisson分布式锁核心原理
Redisson实现分布式锁主要依赖于Redis的以下特性:
- Lua脚本(保证原子性)
- 发布/订阅机制
- Hash数据结构
1. 加锁流程
加锁的核心Lua脚本逻辑:
if (redis.call('exists', KEYS[1]) == 0) then
redis.call('hset', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
return redis.call('pttl', KEYS[1]);
参数说明:
- KEYS[1]:锁的名称
- ARGV[1]:锁的生存时间(毫秒)
- ARGV[2]:客户端唯一标识(UUID:threadId)
2. 可重入锁实现
Redisson通过Redis的Hash结构实现可重入锁:
- Hash的field存储客户端唯一标识(UUID:threadId)
- Hash的value存储重入次数
3. 锁续期机制(WatchDog)
为了防止业务执行时间超过锁过期时间,Redisson提供了WatchDog机制自动续期:
三、Redisson分布式锁高级特性
1. 公平锁实现
Redisson公平锁通过Redis的List和ZSet结构实现排队机制:
2. 红锁(RedLock)算法
Redisson实现了Redis官方推荐的红锁算法,用于提高分布式锁的可靠性:
+----------------+ +-----------------+
| 客户端 | | Redis实例1 |
+--------+-------+ +--------+--------+
| |
| 尝试获取锁 |
+--------------------->|
| |
| +---+
| |
| v
| +-----+------+
| | 成功/失败 |
| +-----+------+
| |
| +---+
| |
v v
+--------+-------+ +--------+--------+
| 客户端 | | Redis实例2 |
+--------+-------+ +--------+--------+
| |
| 尝试获取锁 |
+--------------------->|
| |
| +---+
| |
| v
| +-----+------+
| | 成功/失败 |
| +-----+------+
| |
| +---+
| |
v v
+--------+-------+ +--------+--------+
| 客户端 | | Redis实例3 |
+--------+-------+ +--------+--------+
| |
| 尝试获取锁 |
+--------------------->|
| |
| +---+
| |
| v
| +-----+------+
| | 成功/失败 |
| +-----+------+
| |
| +---+
| |
v v
+--------+-------+ +--------+--------+
| 客户端 | | Redis实例4 |
+--------+-------+ +--------+--------+
| |
| 尝试获取锁 |
+--------------------->|
| |
| +---+
| |
| v
| +-----+------+
| | 成功/失败 |
| +-----+------+
| |
| +---+
| |
v v
+--------+-------+ +--------+--------+
| 客户端 | | Redis实例5 |
+--------+-------+ +--------+--------+
| |
| 尝试获取锁 |
+--------------------->|
| |
| +---+
| |
| v
| +-----+------+
| | 成功/失败 |
| +-----+------+
| |
| +---+
| |
v v
+----------------------+
| |
| 统计成功数量 |
| (N/2+1个成功) |
| |
v v
+--------+-------+ +--------+--------+
| 获取锁成功 | | 获取锁失败 |
+----------------+ +----------------+
红锁算法步骤:
- 获取当前时间
- 依次尝试从多个独立的Redis实例获取锁
- 计算获取锁消耗的总时间
- 当且仅当从多数节点获取锁成功,且总耗时小于锁有效时间时,锁获取成功
四、Redisson分布式锁最佳实践
-
锁命名规范:使用业务相关的有意义名称,如
order:lock:123
表示订单ID为123的锁 -
合理设置超时时间:
// 尝试获取锁,最多等待100秒,上锁以后10秒自动解锁
RLock lock = redisson.getLock("myLock");
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
-
避免嵌套锁:尽量减少锁的嵌套层级,防止死锁
-
锁粒度控制:尽量使用细粒度锁,提高并发性能
五、常见问题解答
Q: Redisson分布式锁与SETNX实现的锁有什么区别?
A: Redisson相比简单的SETNX实现有以下优势:
- 可重入支持
- 自动续期机制
- 更完善的超时处理
- 提供公平锁、联锁等高级特性
- 更完善的异常处理
Q: WatchDog机制会不会导致锁永远不释放?
A: 不会,WatchDog只会在客户端正常工作时续期,如果客户端崩溃,WatchDog线程也会终止,锁最终会因过期而自动释放。
Q: 为什么推荐使用Redisson而不是自己实现分布式锁?
A: 自己实现分布式锁需要考虑:
- 原子性保证
- 锁续期问题
- 网络分区处理
- 客户端崩溃恢复
- 可重入性
- 公平性等
Redisson经过多年生产验证,解决了上述所有问题,比自己实现更可靠。