分布式锁原则
1、相互排斥,即任一时刻,只能有一个客户端持有锁;
2、无死锁,持有锁的客户端宕机或网络延迟下仍可获取锁;
3、有始有终,一个客户端加了锁,只能自己释放锁,当然也不能被其他客户端解锁;
4、容错性,只要大部分redis节点还存活,那么客户端就应该可以正常加锁和释放锁;
一、加锁操作
<!--jedis 2.9.0-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
private static final String LOCKED_SUCCESS = "OK";
private static final String NX = "NX";
private static final String EXPIRE_TIME = "PX";
/**
* 获取锁
*
* @param jedis redis客户端
* @param lockKey 锁的key
* @param uniqueId 请求标识
* @param expireTime 过期时间
* @return 是否获取锁
*/
public static boolean tryDistributedLock(Jedis jedis, String lockKey, String uniqueId, long expireTime) {
String result = jedis.set(lockKey, uniqueId, NX, EXPIRE_TIME, expireTime);
return LOCKED_SUCCESS.equals(result);
}
在低版本的redis中是没有这个set方法的,至于为什么这个简单的set方法能够保证前面提到的分布式锁原则呢?看下这个set的源码参数
/**
* Set the string value as value of the key. The string can't be longer than 1073741824 bytes (1
* GB).
* @param key 唯一标识key
* @param value 存储的值value
* @param nxxx 可选项:NX、XX 其中NX表示当key不存在时才set值,XX表示当key存在时才set值
* @param expx 过期时间单位,可选项:EX|PX 其中EX为seconds,PX为milliseconds
* @param time 过期时间,单位取上一个参数
* @return Status code reply
*/
public String set(final String key, final String value, final String nxxx, final String expx,
final long time) {
checkIsInMultiOrPipeline();
client.set(key, value, nxxx, expx, time);
return client.getStatusCodeReply();
}
如上的tryDistributedLock就可以实现简单的redis分布式锁了(此set方法的原子性)
1、set方法中nxx参数为NX,表示当key不存在时才会set值,保证了互斥性;
2、set值的同时设置过期时间(过期后del此key),客户端宕机或网络延迟时不会一直持有锁,避免了死锁发生;
3、set方法中的value,比如UUID之类的,用来表示当前请求客户端的唯一性标识;
4、因为是redis单例,暂时没有考虑容错性;
常见的错误分布式加锁实现
public static void getLock(Jedis jedis, String lockKey, String uniqueId, int expireTime) {
//setnx方法表示当key不存在时才set值,存在不做任何操作
Long result = jedis.setnx(lockKey, uniqueId);
//设置过期时间,但是如果此时服务器宕机,将无法释放锁(这两个setnx和expire并不具备原子性)
if (result == 1) jedis.expire(lockKey, expireTime);
}
二、解锁操作
private static final Long RELEASE_SUCCESS = 1L;
/**
* 释放锁
*
* @param jedis redis客户端
* @param lockKey 锁的key
* @param uniqueId 请求标识
* @return 是否释放
*/
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String uniqueId) {
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(luaScript, Collections.singletonList(lockKey), Collections.singletonList(uniqueId));
return RELEASE_SUCCESS.equals(result);
}
不难看出使用Lua脚本告诉redis,如果这个key存在,且其存储的值和指定的value一致才可以删除这个key,从而释放锁,这样也保证了分布式锁的几个原则. 常见的错误释放锁会直接del这个key,没有考虑当前锁的拥有者,不符合分布式锁原则的有始有终原则;
如果不想用上面的Lua脚本,也可以用如下代码:
public static void releaseLock(Jedis jedis,String lockKey,String uniqueId){
if(uniqueId.equals(jedis.get(lockKey))){
jedis.del(lockKey);
}
}
如上代码存在这样的场景: Client A去加锁lockKey,然后释放锁,在执行del(lockKey)之前,这时lockKey锁expire到期失效了,此时Client B尝试加锁lockKey成功,Client A接着执行释放锁操作(del),便释放了Client B的锁.
补充:
之前存在一点误解,就是认为当redis也高可用时,这个锁还能不能用,答案是可以的,因为一般而言,你的应用服务和redis服务是解耦的,如果你的redis采用了高可用,比如一主多从甚至多主多从,应用服务只会和一个主redis服务交互,这样基于Redis缓存层实现分布式锁到就比较完美了!