Redis SETNX两种方法实现分布式锁

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命令可以实现简单的分布式锁,步骤如下:

  1. 客户端使用SET命令设置一个键,并指定一个长时间未过期的值。如果设置成功,表示获取锁成功。
  2. 如果SET设置失败,表示锁已被其他客户端获取,此时客户端应重试或等待锁被释放。
  3. 锁定客户端在结束对共享资源的访问后,使用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命令实现的分布式锁依然存在几个问题:

  1. 只能用于锁的获取,无法判断是否释放的是正确的锁,可能导致误删其他客户端的锁。
  2. 无法解决锁的重入问题,如果一个客户端已经获取锁,再次请求会导致死锁。
  3. 无法解决锁的遗忘问题,如果锁定客户端异常终止导致锁未被正确释放。

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一样,但是它原子操作设置了键的过期时间。