相信很多小伙伴面试时都被问过“Redis怎么实现分布式锁?”,今天我们就来说一下Redis实现分布式锁。
分布式锁
在计算机科学中,锁是在执行多线程时用于强行限制资源访问的同步机制,即用于在并发控制中保证对互斥要求的满足。
分布式锁需要满足三个要求:
- 互斥性,在同一时刻,只能有一个客户端获得锁
- 无死锁,即使客户端崩溃等其他因素,锁最终都会被释放
- 容错能力,只要大部分节点都可用,客户端就能正常获得和释放锁
Redis分布式锁
获得锁
当我们需要对某个资源加锁时,只需要在Redis设置一个变量,将该资源的名称作为key,一个唯一值作为value即可,
getLock(resourceName, myRandomValue) {
val = redis.get(resourceName)
if (!val) {
redis.set(resourceName, myRandomValue)
redis.expire(resourceName, 1s)
return true
}
return false
}
复制代码
大致的逻辑是这样的。
SETNX(错误做法)
又因为上述逻辑中:
val = redis.get(resourceName)
if (!val) {
redis.set(resourceName, myRandomValue)
...
}
复制代码
这段逻辑可以使用Redis的setnx
命令代替,
SETNX key value
Set key to hold string value if key does not exist. In that case, it is equal to SET. When key already holds a value, no operation is performed. SETNX is short for "SET if Not eXists".
所以网上有不是文章直接这样写:
getLock(resourceName, myRandomValue) {
result = redis.setnx(resourceName, myRandomValue)
if (result) {
redis.expire(resourceName, 1s)
return true
}
return false
}
复制代码
这种做法是错误的。因为这个操作不是原子性的,如果客户端还没来得及执行expire
命令,就宕机了,这时,这个锁就变成了死锁,违背了上面说的第二条规则。
Lua脚本(不推荐)
使用Lua脚本执行setnx
和expire
命令,使其变成原子性操作
if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then
redis.call('expire',KEYS[1],ARGV[2])
return 1
else
return 0
end
复制代码
这样虽然能解决问题,但是不推荐,因为将问题复杂化了。
SET key value [EX seconds|PX milliseconds] [NX|XX] 命令(推荐做法)
- EX seconds -- 设置过期时间,单位秒.
- PX milliseconds -- 设置过期时间, 单位毫秒.
- NX -- 当key不存在时,设置值.
- XX -- 当key存在时,设置值.
所以获得锁时,我们只需要:
getLock(resourceName, myRandomValue) {
return "OK" == redis.command(SET resourceName myRandomValue NX PX 1000)
}
复制代码
锁释放
释放锁时不能直接del
,直接这样的话,可能会把别人的锁删除了,所以需要进行身份识别,怎么识别这个锁是否属于你的呢?这就需要通过我们之前设置的myRandomValue
来识别了。
releaseLock(resourceName, myRandomValue) {
val = redis.get(resourceName)
if (val == myRandomValue) {
redis.del(resourceName)
}
}
复制代码
还是那个问题,整个操作不是原子操作
releaseLock(resourceName, myRandomValue) {
val = redis.get(resourceName)
if (val == myRandomValue) {
// 如果这时resourceName刚好过期
// 可能把别人的锁删除了
redis.del(resourceName)
}
}
复制代码
这时,我们就需要借助Lua脚本了
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
复制代码
分布式锁
上面说的一堆,都是基于单个Redis实例,分布式锁的实现也是基于上面逻辑实现。获取分布式锁时,需要下列几步:
1、获取当前时间,使用毫秒级时间
2、使用相同的键名和随机值,尝试从所有N个实例中顺序获取锁。在每个实例中设置锁时,客户端使用的超时时间需小于总锁自动释放时间,例如,如果自动释放时间为10秒,则超时时间可能在5到50毫秒之间。 这样可以防止客户端长时间与处于故障状态的Redis节点进行通信:如果某个实例不可用,我们应该尽快尝试与下一个实例进行通信。
3、再次获取当前毫秒级时间,减去在步骤1中获得的时间戳,来计算获取锁所花费的时间。当且仅当客户端能够在大多数实例(至少N/2+1)中获取锁,并且获取锁所花费的总时间小于锁有效时间时,则认为已获取锁。
4、如果获取了锁,则将其有效时间视为初始有效时间减去步骤3中计算的花费时间,即有效时间=初始有效时间-花费时间。
5、如果客户端由于某种原因(如无法锁定N/2+1个实例)而未能获得该锁,必须解锁所有实例。
当客户端无法获取锁时,应在随机延迟后重试,防止其他客户端也在尝试获取锁。 同样,客户端在大多数Redis实例中尝试获取锁的速度越快,失败的情况就越低,因此,客户端应尽快将SET命令发送到所有实例。
这就是Redlock
算法,是不是觉得很麻烦?!不用担心,伟大的开源社区已经有很多实现方案了,实际工作中直接拿来用就行了。
- Redlock-rb (Ruby implementation). There is also a fork of Redlock-rb that adds a gem for easy distribution and perhaps more.
- Redlock-py (Python implementation).
- Aioredlock (Asyncio Python implementation).
- Redlock-php (PHP implementation).
- PHPRedisMutex (further PHP implementation)
- cheprasov/php-redis-lock (PHP library for locks)
- Redsync (Go implementation).
- Redisson (Java implementation).
- Redis::DistLock (Perl implementation).
- Redlock-cpp (C++ implementation).
- Redlock-cs (C#/.NET implementation).
- RedLock.net (C#/.NET implementation). Includes async and lock extension support.
- ScarletLock (C# .NET implementation with configurable datastore)
- Redlock4Net (C# .NET implementation)
- node-redlock (NodeJS implementation). Includes support for lock extension.
后记
使用Redlock算法时,需要注意,当义务逻辑耗时较长时,超出了Redis设置的过期时间,导致锁自动释放掉了。所以这时需要我们自己延长锁的时间,需要延长多久呢?这些你只需要知道就行了,很多开源的实现方案中都已经考虑到了,所以在选择开源包时,需要注意一下是否实现了这些。
关于Redlock算法,Martin Kleppmann大神提出了好几点异议,Martin Kleppmann何许人也?
他的这本书相信很多人应该听过吧?!!
然后Redis的作者针对Martin Kleppmann的几点异议,做出了反驳,双方都有理有据,堪称神仙打架。有兴趣的同学可以看一下。