使用SETNX实现分布式锁
Redis 中,Setnx(SET if Not eXists)命令是指:只有当指定的 key 不存在时,才会为 key 设置指定的值,此时设置成功,返回 1;如果指定 key 存在,不会覆盖该 key 的值,此时设置失败,返回 0。
因此,利用 Redis 执行命令时是单线程的特性 + Setnx 并发操作的原子性可以实现一个简单的分布式锁。
Redis 执行命令时是单线程的特性可以保证:当多个客户端同时通过 Setnx 命令尝试获取锁时,只有一个客户端会获取成功,其他客户端则获取失败。
Setnx 并发操作的原子性可以保证:多个客户端并发操作的互斥性,即当一个客户端执行 Setnx 命令时,不会被其他客户端影响。
此外,还要考虑:如果获取锁成功,则设置一个过期时间,防止该客户端挂了之后一直持有该锁;客户端释放锁的时候,需要先判断该锁是否仍然属于该客户端,如果是,则通过 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;}}}
1、tryLock方法接收三个参数,分别是锁的键值lockKey、加锁的请求标识requestId和锁的过期时间expireTime。该方法会尝试使用Redis的set命令加锁,如果加锁成功则返回true,否则返回false。其中NX表示只在锁的键不存在时设置锁,PX表示锁的过期时间为expireTime毫秒。
2、unlock方法接收两个参数,分别是锁的键值lockKey和加锁的请求标识requestId。该方法会执行一个Lua脚本,判断当前锁的值是否等于请求标识requestId,如果是则删除锁并返回true,否则返回false。该方法使用eval命令执行Lua脚本,传入锁的键值和请求标识两个参数,返回值是执行结果。
优点:
(1)实现简单:SETNX 命令实现简单,易于理解和使用。
(2)性能较高:由于 SETNX 命令的执行原子性,保证了分布式锁的正确性,而且在 Redis 中,SETNX 命令是单线程执行的,所以性能较高。
缺点:
(1)锁无法续期:如果加锁方在加锁后的执行时间较长,而锁的超时时间设置的较短,可能导致锁被误释放。
(2)无法避免死锁:如果加锁方在加锁后未能及时解锁(也未设置超时时间),且该客户端崩溃,可能导致死锁。
(3)存在竞争:由于 SETNX 命令是对 Key 的操作,所以在高并发情况下,多个客户端之间仍可能存在竞争,从而影响性能。
(4)SETNX 不支持可重入,可以借助 Redission 封装的能力实现可重入锁。
总结:
使用 SETNX 命令是 Redis 实现分布式锁最简单的方法,虽然上述方案中并不支持可重入性,但是依然可以在当前逻辑的基础上进行调整,使其支持可重入。 尽管如此,使用 SETNX 命令仍然有其他问题,比如锁无法续期等问题。
其次,Redission 中已经实现了效果不错的分布式锁,开箱即用即可,这里我们只是思考一下基于 SETNX 实现分布式锁的思路而已。