实现分布式锁的必要条件:互斥性和不会发生死锁
互斥性的保证:就是同时只能有一个线程注册成功获取到锁 比如 jedis.setNX(key,value):方法含义:如果key不存在就设置
避免发生死锁:就是获得锁以后 无论这个加锁的客户端怎么样,都要最终能释放出来锁;
redis的分布式锁的实现机制就是:
- 获得锁:多线程竞争注册相同的key并存储value,因为Jedis有排他性的方法比如setNX(key,value),如果不存在对应的key,就注册key和value,所以同时只会有一个线程抢注成功(排他性)。
- 然后注册成功的线程就去执行自己要执行的业务代码。
- 释放锁:就是删除key,删除key的时候,通过key先获得value,然后线程根据value的值来判断这个key是不是自己注册的,如果是就可以删除key。所以value的值的识别性很重要。
- 释放锁以后(key被删除),其余的线程就可以开始抢注key了,就重复上演1,2,3流程了。
综上:我们可以理解为 谁能注册key 就相当于谁获得了锁,至于是谁注册的, 就要根据注册key的时候的value的值来判断。所以不同线程在竞争锁的时候key值应该一样,value值应该能识别线程身份,比如value是线程的名字。
获取锁的过程中存在的问题
避免死锁
如果某个线程,获得了锁(注册key和value)之后,突然死掉了,怎么办?这把锁就永远没办法得到主动释放,所以我们要设置锁的过期时间,
让锁被动释放,我们可以给key设置过期时间,过期了,key就会被redis删除掉,从而被动释放了锁。
if(jedis.setNX(key, value) ==1) {
jedis.setExpire(key,expireTime)
}
上面代码这样写符合我们的要求吗?抢占了,也设置了过期时间,是不是就OK了,
答案是不行的,因为万一执行了jedis.setNX(key, value) ==1之后,线程挂了,怎么办?是不是key就没有过期时间了,就死锁了,
所以我们需要将设置key和过期时间放在一个原子操作里,而上面分两步是两个原子操作。所以我们可以选择。
jedis.set(key,value,"NX", "PX",expireTime);这里就是将设置key和过期时间放在一个原子操作里了。
综上获取锁的代码可以如下:
public boolean lock1(KeyPrefix prefix, String key, String value, Long lockExpireTimeOut,
Long lockWaitTimeOut) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
String realKey = prefix.getPrefix() + key;
Long deadTimeLine = System.currentTimeMillis() + lockWaitTimeOut;
for (;;) {
String result = jedis.set(realKey, value, "NX", "PX", lockExpireTimeOut);
if ("OK".equals(result)) {
return true;
}
lockWaitTimeOut = deadTimeLine - System.currentTimeMillis();
if (lockWaitTimeOut <= 0L) {
return false;
}
}
} catch (Exception ex) {
log.info("lock error");
} finally {
returnToPool(jedis);
}
return false;
}
释放锁存在的问题
错误释放锁
仅仅 jedis.del(key) 肯定是不行的,比如此时Key是线程A注册的,线程B调用这个方法会直接删除这个key,就相当于B线程释放了A线程的锁。
那加个判断呢?判断Value是否是自己放进去的,如果不是就不能删除。
String currentValue = jedis.get(realKey);
if (!StringUtils.isEmpty(currentValue) && value.equals(currentValue)) {
jedis.del(realKey);
}
看上去上面的代码好像没什么问题,但是仔细一看,这里的判断和删除是两步操作,也就是说可能存在一种情况,A线程在释放自己的锁的时候,刚执行完
!StringUtils.isEmpty(currentValue) && value.equals(currentValue),准备删除自己注册的key的时候,这个时候刚好Key过期了,
B线程抢注了自己的key和value,此时按理说锁是B的,但是A线程继续执行jedis.del(realKey)就会删除B的key,从而释放了B的锁这样也是不行的。
所以我们就需要 将判断和删除 这两步操作合并成一个原子操作,那怎么办呢?通过Lua脚本来做如下:
public boolean unlock1(KeyPrefix prefix, String key, String value) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
String realKey = prefix.getPrefix() + key;
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(realKey),
Collections.singletonList(value));
if ("1".equals(result)) {
return true;
}
} catch (Exception ex) {
log.info("unlock error");
} finally {
returnToPool(jedis);
}
return false;
}