本文分析,在分布式系统中,使用redis实现分布式锁,会遇到什么问题。关于分布式锁概念和redis分布式锁的具体实现,可参考前面的2篇文章。本文重点在于,对分布式锁技术选型的分析。
常规的,使用redis做分布式锁,主要实现如下:
/**
* 加锁
*
* @param lockName 锁名,对应被争用的共享资源
* @param randomValue 随机值,需要保持全局唯一,便于释放时校验锁的持有者
* @param expireTime 过期时间,到期后自动释放,防止出现问题时死锁,资源无法释放
* @return
*/
public static boolean acquireLock(String lockName,String randomValue,int expireTime){
Jedis jedis = jedisPool.getResource();
try {
while (true){
String result = jedis
.set(lockName, randomValue, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if(LOCK_SUCCESS.equals(result)){
logger.info("【Redis lock】success to acquire lock for [ "+lockName+" ],expire time:"+expireTime+"ms");
return true;
}
}
}catch (Exception ex){
ex.printStackTrace();
}finally {
if(null != jedis){
jedis.close();
}
}
logger.info("【Redis lock】failed to acquire lock for [ "+lockName+" ]");
return false;
}
/**
* redis释放锁
* watch和muti命令保证释放时的对等性,防止误解锁
*
* @param lockName 锁名,对应被争用的共享资源
* @param randomValue 随机值,需要保持全局唯一,以检验锁的持有者
* @return 是否释放成功
*/
public static boolean releaseLock(String lockName,String randomValue){
Jedis jedis = jedisPool.getResource();
try{
jedis.watch(lockName);//watch监控
if(randomValue.equals(jedis.get(lockName))){
Transaction multi = jedis.multi();//开启事务
multi.del(lockName);//添加操作到事务
List<Object> exec = multi.exec();//执行事务
if(RELEASE_SUCCESS.equals(exec.size())){
logger.info("【Redis lock】success to release lock for [ "+lockName+" ]");
return true;
}
}
}catch (Exception ex){
logger.info("【Redis lock】failed to release lock for [ "+lockName+" ]");
ex.printStackTrace();
}finally {
if(null != jedis){
jedis.unwatch();
jedis.close();
}
}
return false;
}
基于单点redis的锁实现,上述这种实现,基本达到了单节点的安全限度,解决了如下几个问题:
设置过期时间后,即使客户端挂了,加锁后未解锁,这个锁也是会到期释放的,不存在死锁的可能。
典型的死锁场景:
一个客户端获取锁成功,但是在释放锁之前崩溃了,此时该客户端实际上已经失去了对公共资源的操作权,但却没有办法请求解锁(删除 key-value 键值对),那么,它就会一直持有这个锁,而其它客户端永远无法获得锁。
加锁的时候,我们给锁设置了个随机值,保证了即使在如下情况,解锁也是只会释放自己加的锁,而不会误删。
典型的误删场景:
假如获取锁时SET的不是一个随机字符串,而是一个固定值,那么可能会发生下面的执行序列:
此时,如果客户端2访问共享资源,就没有锁来提供资源保护了。
一个时刻只能有一个客户端可以得到锁,这个由redis自身命令setnx即可得到保证。
释放锁,包含3个操作,“get”、“判断”、“del”,我们必须保证释放锁时,这三个操作是原子性的。有2种方式来保证这一批操作的原子性。
如果三个操作的原子性得不到保证,下面的场景,就会出问题:
单节点,存在挂机的风险,为了达到高可用,我们可以做redis集群。一个redis节点作为master,master挂一个slave,当master挂掉后,自动切到slave节点。
看上去,前面5个问题都得到了解决。
但是,在集群模式下,考虑一个场景:
由于redis的主从复制是异步的,这可能导致,在主从切换的间隙,资源丧失了锁带来的安全性。
在前文的算法中,我们给锁设置了有效期,这个值,究竟多少合适呢?
如果太短,锁可能在客户端还未完成对资源的操作之前就过期,从而失去了保护;
如果太长,一个客户端如果主动释放锁失败了,那么,需要等到过期时间才会被动释放,那么,在漫长的有效期内,其他客户端,都无法获得这个资源的锁。