
目录
[SETNX + EXPIRE](#SETNX + EXPIRE)
[SETNX + value值是(系统时间+过期时间)](#SETNX + value值是(系统时间+过期时间))
[SET的扩展命令(SET EX PX NX)](#SET的扩展命令(SET EX PX NX))
分布式锁
概念
分布式锁是一种应用级的锁,在分布式系统下,一个应用通常会有多个节点部署。在高并发的情况下,就可能出现同一时间多个节点多个线程同时处理同一条数据,导致数据不一致。分布式锁的出现就是为了解决此问题,它能够保证一个代码块在同一时间只能被同一线程或同一个节点执行。
原理
Redis分布式锁的实现原理主要是利用Redis的原子性操作,如SETNX(SET IF NOT EXISTS)命令。当进程请求执行操作前,会尝试使用SETNX命令来设置锁。如果锁不存在(即key不存在),则SETNX命令会成功设置锁并返回1,表示加锁成功。如果锁已经存在(即key已存在),则SETNX命令会返回0,表示加锁失败。同时,为了避免锁被长时间占用而导致死锁,Redis分布式锁通常会设置一个过期时间,当锁过期时,它会自动释放。
实现方式
引入校验ID
对于 Redis 中写⼊的加锁键值对, 其他的节点也是可以删除的。
为了解决上述问题, 我们可以引⼊⼀个校验 id.
⽐如可以把设置的键值对的值, 不再是简单的设为⼀个 1, ⽽是设成服务器的编号. 形如 "001": "服务器1".
这样就可以在删除 key (解锁)的时候, 先校验当前删除 key 的服务器是否是当初加锁的服务器, 如果是,才能真正删除; 不是, 则不能删除.
伪代码如下:
java
String key = [要加锁的资源 id];
String serverId = [服务器的编号];
// 加锁, 设置过期时间为 10s
redis.set(key, serverId, "NX", "EX", "10s");
// 执⾏各种业务逻辑, ⽐如修改数据库数据.
doSomeThing();
// 解锁, 删除 key. 但是删除前要检验下 serverId 是否匹配.
if (redis.get(key) == serverId) {
redis.del(key);
}
SETNX + EXPIRE
这是最常见的Redis分布式锁实现方式。首先,使用SETNX命令尝试设置锁,如果成功,则使用EXPIRE命令为锁设置一个过期时间。这种方式简单易懂,但需要注意的是,SETNX和EXPIRE是两个独立的命令,它们不是原子操作。因此,在极端情况下(如进程崩溃或网络中断),可能会出现锁被设置但没有设置过期时间的情况,导致死锁。为了避免这种情况,可以使用Lua脚本来保证SETNX和EXPIRE的原子性。
SETNX + value值是(系统时间+过期时间)
这种方式将过期时间直接存储在锁的value值中。当加锁失败时,进程会检查当前时间是否大于锁的过期时间。如果大于,则认为锁已经过期,可以使用GETSET命令尝试获取锁。这种方式避免了SETNX和EXPIRE非原子性操作的问题,但要求分布式环境下每个客户端的时间必须同步。此外,如果多个客户端同时请求过期的锁,可能会出现竞争条件,导致只有一个客户端能够成功获取锁。
使用Lua脚本
Lua脚本可以确保SETNX和EXPIRE两个命令的原子性。通过将SETNX和EXPIRE两个命令封装在Lua脚本中,可以确保它们在同一时间被Redis执行。这种方式避免了SETNX和EXPIRE非原子性操作的问题,提高了分布式锁的可靠性。
伪代码如下:
Lua
if redis.call('get',KEYS[1]) == ARGV[1] then
return redis.call('del',KEYS[1])
else
return 0
end;
SET的扩展命令(SET EX PX NX)
Redis的SET命令提供了扩展参数,可以同时设置key的值、过期时间和条件。其中,NX参数表示只有在key不存在时才设置key的值。通过使用SET EX PX NX命令,可以一次性完成设置锁和设置过期时间的操作,从而避免了SETNX和EXPIRE非原子性操作的问题。
Redisson
Redisson是一个Java实现的Redis客户端,它提供了丰富的分布式锁功能。Redisson的分布式锁基于Redis的发布/订阅、脚本、事务等特性实现,具有可重入性、可中断性、公平性等特性。使用Redisson可以大大简化分布式锁的实现和管理。
引入Redlock算法
我们引⼊⼀组 Redis 节点. 其中每⼀组 Redis 节点都包含⼀个主节点和若⼲从节点. 并且组和组之间存储的数据都是⼀致的, 相互之间是 "备份" 关系(⽽并⾮是数据集合的⼀部分, 这点有别于 Redis cluster).
加锁的时候, 按照⼀定的顺序, 写多个 master 节点. 在写锁的时候需要设定操作的 "超时时间". ⽐如50ms. 即如果 setnx 操作超过了 50ms 还没有成功, 就视为加锁失败.

如果给某个节点加锁失败, 就⽴即再尝试下⼀个节点.
当加锁成功的节点数超过总节点数的⼀半, 才视为加锁成功.
这样的话, 即使有某些节点挂了, 也不影响锁的正确性.
同理, 释放锁的时候, 也需要把所有节点都进⾏解锁操作. (即使是之前超时的节点, 也要尝试解锁, 尽量保证逻辑严密).
简⽽⾔之, Redlock 算法的核⼼就是, 加锁操作不能只写给⼀个 Redis 节点, ⽽要写个多个!! 分布式系统中任何⼀个节点都是不可靠的. 最终的加锁成功结论是 "少数服从多数的".
应用场景
1.并发控制:在分布式系统中,如果多个客户端同时对同一个资源进行读写操作,很可能会引起数据冲突。通过使用Redis分布式锁,可以确保同一时间只有一个客户端能够访问该资源,从而避免数据冲突。
2.任务调度:在分布式系统中,需要进行任务的统一调度和管理。通过使用Redis分布式锁,可以确保同一时间只有一个客户端能够执行任务,从而避免任务重复执行或任务冲突。
3.限流控制:在高并发的场景下,为了避免系统资源被耗尽或过载,需要对请求进行限流控制。通过使用Redis分布式锁,可以限制同一时间处理的请求数量,从而保护系统的稳定性和可用性。
注意事项
1.锁的过期时间:锁的过期时间必须根据业务执行时间进行合理设置。如果过期时间太短,可能会导致业务还未执行完毕锁就被释放;如果过期时间太长,则可能会导致资源被长时间占用而浪费。
2.锁的释放:在使用Redis分布式锁时,必须确保在业务执行完毕后及时释放锁。如果忘记释放锁或因为异常导致锁无法释放,则可能会导致死锁问题。因此,在使用Redis分布式锁时,通常会在finally块中释放锁以确保锁的及时释放。
3.可重入性:在某些业务场景中,一个线程在持有锁的情况下可能会再次请求加锁。因此,Redis分布式锁需要支持可重入性特性。即同一个线程在持有锁的情况下可以再次获取锁而不会导致死锁问题。
4.安全性:Redis分布式锁需要确保锁只能被持有的客户端删除而不能被其他客户端删除。这可以通过在锁的value值中存储唯一标识来实现。当释放锁时,只有持有该唯一标识的客户端才能成功释放锁。