利用Redis的单线程特性,在多个Redis客户端同时通过SETNX命令尝试获取锁,如果返回1表示获取锁成功,否则表示获取锁失败。
Redis Setnx(SET if Not eXists) 命令在指定的 key 不存在时,为 key 设置指定的值。设置成功,返回 1 。 设置失败,返回 0 。
因为Redis的单线程机制,所以可以保证只会有一个客户端成功获取到锁,而其他客户端则会失败。如果获取锁成功,则设置一个过期时间,防止该客户端挂了之后一直持有该锁。客户端释放锁的时候,需要先判断该锁是否仍然属于该客户端,如果是,则通过DEL命令释放锁。
public class RedisDistributedLock {
private final JedisPool jedisPool;
public RedisDistributedLock(JedisPool jedisPool) {
this.jedisPool = jedisPool;
}
public boolean tryLock(String lockKey, String requestId, int expireTime) {
try (Jedis jedis = jedisPool.getResource()) {
String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
return "OK".equals(result);
}
}
public boolean unlock(String lockKey, String requestId) {
try (Jedis jedis = jedisPool.getResource()) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
return Long.parseLong(result.toString()) == 1L;
}
}
}
tryLock方法接收三个参数,分别是锁的键值lockKey、加锁的请求标识requestId和锁的过期时间expireTime。该方法会尝试使用Redis的set命令加锁,如果加锁成功则返回true,否则返回false。其中NX表示只在锁的键不存在时设置锁,PX表示锁的过期时间为expireTime毫秒。
SETNX命令自身是不支持设置超时时间的,一般是结合EXPIRE一起使用,常见用法:
SETNX key value
EXPIRE key 10
或者:
SET key value EX 10 NX
unlock方法接收两个参数,分别是锁的键值lockKey和加锁的请求标识requestId。该方法会执行一个Lua脚本,判断当前锁的值是否等于请求标识requestId,如果是则删除锁并返回true,否则返回false。该方法使用eval命令执行Lua脚本,传入锁的键值和请求标识两个参数,返回值是执行结果。
优点
(1)实现简单:SETNX命令实现简单,易于理解和使用。
(2)性能较高:由于SETNX命令的执行原子性,保证了分布式锁的正确性,而且在Redis中,SETNX命令是单线程执行的,所以性能较高。
缺点
(1)锁无法续期:如果加锁方在加锁后的执行时间较长,而锁的超时时间设置的较短,可能导致锁被误释放。
(2)无法避免死锁:如果加锁方在加锁后未能及时解锁(也未设置超时时间),且该客户端崩溃,可能导致死锁。
(3)存在竞争:由于SETNX命令是对Key的操作,所以在高并发情况下,多个客户端之间仍可能存在竞争,从而影响性能。
(4)setnx不支持可重入,可以借助redission封装的能力实现可重入锁。
所以,想要一个更加完善的分布式锁,可以采用Redisson: