Redisson 如何实现分布式锁?(核心原理与思考)
Redisson 是一个功能强大的 Redis 客户端,它提供了许多分布式对象和服务,其中就包括分布式锁。Redisson 的分布式锁是基于 Redis 的 Lua 脚本实现的,这保证了操作的原子性。
我们来一步步拆解 Redisson 锁的实现原理
4.1 最基础的 Redis 锁(存在的问题)
如果只用 SETNX
和 DEL
:
- 加锁:
SETNX mylock_key my_client_id
my_client_id
是一个唯一标识,比如 UUID,用来标识是哪个客户端加的锁。
- 解锁:
DEL mylock_key
问题:
- 死锁: 如果客户端加锁后,还没来得及
DEL
就宕机了,那么mylock_key
永远不会被删除,其他客户端就永远拿不到锁了。 - 误删: 如果客户端A加锁,但因为网络延迟等原因,锁过期了(被Redis自动删除了),然后客户端B加锁成功。此时客户端A恢复,执行
DEL mylock_key
,它删除了客户端B的锁!
4.2 Redisson 的改进:过期时间 + 唯一ID + Lua 脚本
Redisson 解决了上述问题,它的核心思想是:
- 加锁时设置过期时间: 避免死锁。
- 加锁时设置唯一ID: 避免误删。
- 使用 Lua 脚本: 保证加锁和解锁操作的原子性。
Redisson 加锁的 Lua 脚本(简化版):
lua
-- KEYS[1]: 锁的名称 (e.g., "myLock")
-- ARGV[1]: 锁的过期时间 (e.g., 30000 毫秒)
-- ARGV[2]: 客户端的唯一ID (e.g., UUID + 线程ID)
-- 如果锁不存在,或者锁是当前客户端加的
if redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[2]) == 1 then
-- 设置锁,并设置过期时间
-- HINCRBY: 将哈希表中字段的值增加指定增量。这里是记录重入次数
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]); -- 设置过期时间 (毫秒)
return nil; -- 表示加锁成功
end;
return redis.call('pttl', KEYS[1]); -- 返回锁的剩余过期时间,表示加锁失败
Redisson 解锁的 Lua 脚本(简化版):
lua
-- KEYS[1]: 锁的名称
-- ARGV[1]: 客户端的唯一ID
-- 如果锁不存在,或者不是当前客户端加的锁
if redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 0 then
return nil; -- 表示解锁失败或锁不存在
end;
-- 减少重入次数
local counter = redis.call('hincrby', KEYS[1], ARGV[1], -1);
-- 如果重入次数归零,说明完全释放了锁
if counter > 0 then
-- 重新设置过期时间
redis.call('pexpire', KEYS[1], ARGV[2]); -- ARGV[2] 是新的过期时间
return 0; -- 表示锁还在,只是重入次数减少
else
-- 重入次数归零,删除锁
redis.call('del', KEYS[1]);
return 1; -- 表示锁已完全释放
end;
return nil;
思考:为什么用 Hash 类型?
HINCRBY
:这实现了可重入锁。同一个客户端(同一个线程)可以多次获取同一个锁,每次获取都会增加哈希表中对应字段的值(重入次数)。释放锁时,每次减少重入次数,直到为0才真正释放锁。- 哈希表的字段是客户端的唯一ID,值是重入次数。
- 锁的类型:
- 可重入锁 (Reentrant Lock): 最常用,上面已解释。
- 公平锁 (Fair Lock): 保证获取锁的顺序。
- 联锁 (MultiLock): 同时获取多个 Redis 实例上的锁,只要有一个实例加锁失败,所有已加的锁都会被释放。用于解决 Redis 单点故障问题。
- 红锁 (RedLock): Redisson 实现了 Redis 官方推荐的 RedLock 算法。它需要多个独立的 Redis Master 节点(至少3个,通常5个)。客户端尝试在大多数(N/2 + 1)Redis 实例上获取锁,才能算成功。这提供了更高的可用性和容错性,但实现更复杂,性能开销也更大。
- 读写锁 (ReadWriteLock): 允许多个读操作同时进行,但写操作是独占的。
- 信号量 (Semaphore): 控制并发访问资源的数量。
- 闭锁 (CountDownLatch): 类似 Java 的 CountDownLatch。