redis分布式锁实现
一、分布式锁的概念
分布式锁:项目在集群部署时,用分布式锁控制不同系统中的多个进程对同一资源的并发访问。
二、redis分布式锁的3个基本概念
加锁 :setnx命令。key是锁的唯一标识,按业务来决定命名。(setnx 命令,在指定的 key 不存在时,为 key 设置指定的值,并返回1,表示成功,当指定的key存在时,设置失败,返回0);
锁超时 :expire(key, timeout)命令,为指定的key设置过期时间;
释放锁 :del(key)命令,删除指定的key值,释放锁之后,其他线程或者进程就可以继续执行setnx命令来获得锁。
三、redis分布式锁的第一版
//伪代码实现
/**
* 加锁
*/
public static boolean setnx(String key, String value, String timeout){
Boolean isSuccess = false;
jedis = RedisPool.getJedis();
try {
if(jedis.setnx(key, value) == 1) { //Ⓐ 此处加锁,突然挂掉了
jedis.expire(key, timeout);//Ⓑ 此处设置过期时间
isSuccess = true;
}
} catch (Exception e){
e.printStackTrace();
} finally {
jedis.close();
}
return isSuccess ;
}
/**
* 释放锁
*/
public static Long del(String key){
jedis = RedisPool.getJedis();
Long result = null;
try {
result = jedis.del(key);
} catch (Exception e){
e.printStackTrace();
} finally {
jedis.close();
}
return result;
}
第一版的问题:设想进程A在Ⓐ处加锁成功,即setnx刚执行成功,但是由于某种原因,随后挂掉了,这样一来,Ⓑ就没有执行,这个锁就没有设置过期时间,变的长生不老,而其他的进程就再也无法获取这把锁了。其主要原因是setnx和expire命令的分步操作是非原子性的。
四、redis分布式锁的第二版
//伪代码实现,Redis 2.6.12以上版本为set指令增加了设置超时时间的可选参数,伪代码如下:set(key,1,30,NX),这样就可以取代setnx指令
/**
* 加锁
*/
public static boolean setnx(String key, String value, String timeout){
return redisTemplate.opsForValue().setIfAbsent(key, value, timeout, TimeUnit.SECONDS);
}
/**
* 释放锁
*/
public static boolean del(String key){
return redisTemplate.del(lock);
}
第二版的问题:设想一个进程A成功得到了锁,并且设置的超时时间是20秒。但是由于某些原因导致进程A过了20秒都没执行完,这时候锁过期了,便会自动释放,进程B得到了锁。随后,当进程A执行完了任务时,进程A接着执行del指令来释放锁。但这时候进程B还没执行完,进程A实际上删除的是进程B加的锁。
五、redis分布式锁的第三版
//伪代码实现
/**
* 加锁
*/
/** 加锁成功标识 */
private static final String LOCK_SUCCESS = "OK";
/** NX|XX, NX -- Only set the key if it does not already exist. XX -- Only set the key if it already exist. */
private static final String SET_IF_NOT_EXIST = "NX";
/** 过期时间单位标识, EX|PX, expire time units: EX = seconds; PX = milliseconds*/
private static final String SET_WITH_EXPIRE_TIME = "EX";
public String setLock(String lockName, long timeout) {
//每个线程均实现自己的value值,用于标注,该锁是由哪个线程加的,便于后期的对应释放锁,而避免所有的线程均可释放锁
String value = UUID.randomUUID().toString();
RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
Jedis jedis = (Jedis) factory.getConnection().getNativeConnection();
String result = jedis.set(lockName, value, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, timeout);
if (LOCK_SUCCESS.equals(result)) {
return value;
}
return null;
}
/**
* 释放锁
*/
//解锁成功的标志
private static final Long RELEASE_SUCCESS = 1L;
public boolean unlock(String lockName, String value) {
RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
Jedis jedis = (Jedis) factory.getConnection().getNativeConnection();
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(lockName), Collections.singletonList(value));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
第三版优点:解决了二版本误删锁的问题,因为在删除之前,会进行一次锁的判定,确保要删除的锁是属于当前进程自己本身的。为了实现验证和删除过程的原子性,使用Lua脚本来实现。
第三版问题:试想一个进程A成功得到了锁,并且设置的超时时间是20秒。但是由于某些原因导致进程A过了20秒都没执行完,这时候锁过期了,便会自动释放,进程B得到了锁。这样分布式锁就会失去了意义,因为多个进程可以得到这把锁并执行。
解决思路:有一个动作,专门去检查获得锁的进行是否在锁设定的过期时间内完成了当前任务,如果没有,则需要进行相应的锁续期,执行完成后,在释放锁。
六、Redission分布式锁原理部分(Java代码的实现,请自行百度)
对于这一部分推荐一个好一点的博文供大家参考:redission原理概述
其中最主要的是这么一下几点:
1. redission加锁的基本原理:使用hset命令保存请求进程号以及加锁次数(可重入锁)最主要的还是那一段lua脚本;
2. 锁的续期:watchDog自动续期(默认加锁30秒,10秒一检查锁当前进程对锁的占有情况);
注:部分文字及参考链接来自互联网,如有侵权,请及时联系本人进行删除