SETNX是我们最常用的基于Redis实现分布式锁的一种解决方案,在Redis的较新的版本中set命令也支持实现分布式锁,并且还能同时指定过期时间,也就是设置key-value和过期时间是原子操作。我们接下来分别看一下这两种实现。
SETNX实现分布式锁
SETNX是Redis中的一个原子操作命令,SETNX命令是”SET if Not eXists”的缩写。它的语法如下:
SETNX key value
其作用是:只在键key不存在的情况下,将键的值设为value。 如果键key已经存在,则SETNX不做任何操作。
SETNX返回值:
- 1:表示键key设置成功,原来不存在。
- 0:表示键key设置失败,原来已经存在。
使用SETNX可以实现分布式锁的原因是:多个客户端同时使用SETNX设置一个键,只有一个客户端可以设置成功,其它客户端设置会失败。 SETNX设置成功的客户端获得锁,其它客户端需要等待锁释放后重试。
使用SETNX来实现分布式锁,步骤如下:
1.客户端在获取锁之前,使用SETNX尝试设置一个键,并指定一个长时间未过期的值。
2.如果SETNX设置成功(返回1),表示获取锁成功。此时客户端可以访问共享资源。
3.如果SETNX设置失败(返回0),表示锁已被其他客户端获取,此时客户端应该不断重试或等待其他客户端释放锁。
4.锁定客户端在结束对共享资源的访问后,使用DEL命令释放锁。
命令方式设置
SETNX key value
示例
# 客户端1
SETNX lock "1"
# 返回1,表示获得锁
# 客户端2
SETNX lock "2"
# 返回 0,表示未获得锁
# 客户端1释放锁
DEL lock
# 客户端2再次尝试
SETNX lock "2"
# 返回1,表示获得锁
Jedis方式设置
使用Jedis(Java Redis客户端)来演示Redis SETNX实现分布式锁。代码如下:
public class DistributedLock {
private Jedis jedis = new Jedis("localhost");
private final String LOCK_KEY = "lock";
public boolean acquireLock() {
String uuid = UUID.randomUUID().toString();
long timeout = 30000; // 锁超时时间 ms
long end = System.currentTimeMillis() + timeout;
do {
String result = jedis.setnx(LOCK_KEY, uuid);
if ("1".equals(result)) {
return true;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
} while (System.currentTimeMillis() < end);
return false;
}
public void releaseLock() {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
jedis.eval(script, Collections.singletonList(LOCK_KEY), Collections.singletonList(uuid));
}
}
- 使用SETNX尝试获取锁,如果返回1表示获得锁,否则一直循环重试直到超时。
- 使用EVAL命令释放锁,该命令可以根据锁的值判断是否释放正确的锁,避免误删其他线程的锁。
- 使用Uuid作为锁的值,可以避免锁被其他线程不小心获取到。
示例使用:
DistributedLock lock = new DistributedLock();
if (lock.acquireLock()) {
// 获得锁,访问共享资源
} else {
// 未获得锁
}
lock.releaseLock(); // 释放锁
另外,如果我们想要使用SETNX命令同时实现过期时间,一个命令是不能实现的,可以转变思路,我们把SETNX设置的key对应的value,设置成过期时间的时间点。我们SETNX设置缓存时,如果返回1,说明设置成功,如果返回0说明设置失败,此时get这个key,获取到里面的值,这个值就是key的过期时间,和当前时间对比,如果发现过期了,就可以用getset方法重新设置SETNX的值。同时要比较get的值和getset返回的值是否相等,不相等说明已经被其他值修改了。
set实现分布式锁
命令方式设置
Redis SET命令用于设置键的值。语法如下:
SET key value [NX | XX] [GET] [EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL]
命令参数说明:
- EX seconds:设置键key的过期时间为seconds秒。
- PX milliseconds:设置键key的过期时间为milliseconds毫秒。
- NX:只在键key不存在时设置值,用于分布式锁实现。
- XX:只在键key存在时设置值。
使用Redis SET命令可以实现简单的分布式锁,步骤如下:
- 客户端使用SET命令设置一个键,并指定一个长时间未过期的值。如果设置成功,表示获取锁成功。
- 如果SET设置失败,表示锁已被其他客户端获取,此时客户端应重试或等待锁被释放。
- 锁定客户端在结束对共享资源的访问后,使用DEL命令释放锁。
示例:
# 客户端1
SET lock "1" NX EX 30000
# 设置成功,获得锁
# 客户端2
SET lock "2" NX EX 30000
# 返回错误,未获得锁
# 30000秒后锁自动释放
# 客户端2再次尝试
SET lock "2" NX EX 30000
# 设置成功,获得锁
相比SETNX,SET命令可以同时设置值和过期时间,使得锁可以在一定时间后自动释放,避免死锁。
但是,SET命令实现的分布式锁依然存在几个问题:
- 只能用于锁的获取,无法判断是否释放的是正确的锁,可能导致误删其他客户端的锁。
- 无法解决锁的重入问题,如果一个客户端已经获取锁,再次请求会导致死锁。
- 无法解决锁的遗忘问题,如果锁定客户端异常终止导致锁未被正确释放。
Jedis方式设置
使用Jedis和SET命令实现带超时时间的分布式锁,代码如下:
public class DistributedLock {
private Jedis jedis = new Jedis("localhost");
private final String LOCK_KEY = "lock";
public boolean acquireLockWithTimeout(int timeout) {
String uuid = UUID.randomUUID().toString();
long end = System.currentTimeMillis() + timeout;
do {
String result = jedis.set(LOCK_KEY, uuid, "NX", "EX", timeout);
if ("OK".equals(result)) {
return true;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
} while (System.currentTimeMillis() < end);
return false;
}
public void releaseLock() {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
jedis.eval(script, Collections.singletonList(LOCK_KEY), Collections.singletonList(uuid));
}
}
不同之处在于使用SET命令取代SETNX来设置锁:
SET key value NX EX timeout
- NX:只在键不存在时设置值
- EX:设置键的过期时间为timeout秒
所以这个SET命令的效果跟SETNX一样,但是它原子操作设置了键的过期时间。