Redis的setnx命令
setnx key value
将key设置为value,当键不存在时,才能成功,若键存在,什么也不做,成功返回1,失败返回0 。 setnx实际上就是SET IF NOT Exists的缩写。
expire命令
expire key timeout
将key的超时时间设置为timeout。成功返回1,失败返回0。
代码如下。
public boolean tryLock(String key,String requset,int timeout) {
Long result = jedis.setnx(key, requset);
// 当result=1时,设置成功,否则设置失败
if (result == 1L) {
return jedis.expire(key, timeout) == 1L;
} else {
return false;
}
}
但是这种方案是由问题的,因为setnx和expire命令是两个操作,不具有原子性。当上锁之后设置超时之前服务器宕机,那么锁将无法过期。
既然setnx和expire命令不是原子性的,那么久利用lua脚本来保证其原子性。代码如下
public boolean tryLock_with_lua(String key, String UniqueId, int seconds) {
String lua_scripts = "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then" +
"redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end";
List<String> keys = new ArrayList<>();
List<String> values = new ArrayList<>();
keys.add(key);
values.add(UniqueId);
values.add(String.valueOf(seconds));
Object result = jedis.eval(lua_scripts, keys, values);
//判断是否成功
return result.equals(1L);
}
set key value [EX seconds][PX milliseconds][NX|XX]
EX seconds:过期时间,单位为秒
PX milliseconds:过期时间,单位为毫秒
NX:仅当key不存在时设置值
XX:仅当key存在时设置值
加锁代码如下
public boolean tryLock_with_set(String key, String UniqueId, int seconds) {
return "OK".equals(jedis.set(key, UniqueId, "NX", "EX", seconds));
}
释放锁代码如下,使用lua脚本的方式,尽量保证原子性。
public boolean releaseLock_with_lua(String key,String value) {
String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1] then " +
"return redis.call('del',KEYS[1]) else return 0 end";
return jedis.eval(luaScript, Collections.singletonList(key), \
Collections.singletonList(value)).equals(1L);
}
需要注意的地方是,1)value必须要具有唯一性,2)释放锁时要验证value值,避免误解锁。
下面解释下为什么。
如果返回值是一个固定值,那么
所以释放锁的时候需要对vlaue进行验证,判断是不是自己的锁。value可以使用uuid来做。
但是这种方式也会存在问题。它最大的缺点是加锁时只作用在一个Redis节点上,即使Redis使用Sentinel保证高可用,如果master节点由于某些原因宕机,导致主从切换,就会发生锁丢失的情况。具体如下
针对上面所说的这种情况,Redis作者antirez提出了一种丰高级的分布式锁实现方式:Redlock。
加锁算法大致如下
在Redis分布式环境中,假设有N个Redis master,这些master完全互相独立,不存在主从复制或其他集群协调机制。(可以是N个Redis单master实例,也可以是N个Cluster集群,但不能是一个Cluster有5个master节点。)
解锁算法很简单,客户端向所有Redis节点发起释放锁操作,不论这些节点在获取锁时是否成功(和获取锁失败的处理方式一致)。
这么做确实提高了可用性,但是也会存在一些问题。
假设一个5个Redis节点,A,B,C,D,E。
在默认情况下,Redis的AOF持久化方式是每秒写一次磁盘(即执行fsync),因此最坏情况下可能丢失1秒的数据。为了尽可能不丢数据,Redis允许设置成每次修改数据都进行fsync,但这会降低性能。当然,即使执行了fsync也仍然有可能丢失数据(这取决于系统而不是Redis的实现)。所以,上面分析的由于节点重启引发的锁失效问题,总是有可能出现的。为了应对这一问题,antirez又提出了延迟重启(delayed restarts)的概念。也就是说,一个节点崩溃后,先不立即重启它,而是等待一段时间再重启,这段时间应该大于锁的有效时间(lock validity time)。这样的话,这个节点在重启前所参与的锁都会过期,它在重启后就不会对现有的锁造成影响。
最后一个细节,为什么解锁的时候需要向所有Redis发送解锁请求?
因为可能出现这么一种情况,客户端请求的锁在Redis上成功执行,但是返回的响应包缺丢失了。导致客户端认为Redis没加锁,但是Redis加了锁的情况。