分布式锁是在分布式场景中,实现共享资源互斥访问的一种方式。Java中synchronized或ReentrantLock只能保证在一个jvm中的最多只有一个线程可以获取资源的锁,但是如果是分布式场景,会有多个jvm中各自的线程都会竞争共享资源。这时synchronized或ReentrantLock就无能为力,就需要使用分布式锁。
分布式锁的特点
- 互斥性:同一时间只能有一个线程获取锁
- 可重入性:一个线程获取锁之后,可以再次获取锁
- 锁超时:支持锁超时,防止死锁
分布式锁的实现方式
- redis
- zookeeper
- 数据库
本文使用第一种redis实现方式。
用Redis实现分布式锁
网上流传的一种用redis实现分布式锁的方法,直接使用setnx和expire两条命令,其实是错误,因为这两条命令的执行不是原子的,所以无法保证setnx加锁后,expire设置锁超时时间一定成功,这种情况就会出现死锁。
一、使用set命令实现
从redis 2.6.12版本起,set命令增加了一种支持多个参数的格式
set key value [EX seconds] [PX miliseconds] [NX|XX]
EX: 以秒为单位的过期时间
PX:以毫秒为单位的过期时间
NX:if not exists的缩写,表示当key不存在时,才设置值
XX:if exists的缩写,表示当key存在时,才设置值
public class DistributedLock{
private static final String SET_IF_NOT_EXISTS = "NX";
private static final String EXPIRE = "EX";
private static final String LOCK_SUCCESS = "OK";
//返回true表示获取锁成功
public boolean lock(String key, String sessionId, int expiredSeconds){
return LOCK_SUCCESS.equals(jedis.set(key, sessionId, SET_IF_NOT_EXISTS, EXPIRE, expiredSeconds);
}
//返回true表示释放成功
public boolean releaseLock(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));
}
}
这里的key一般是资源的名称,sessionId要求在分布式场景中是唯一的,比如会话id,可以使用uuid实现。因为在释放锁时会比较value的值,保证不会错误地释放其他锁。释放锁使用lua脚本,保证原子性。
这种方式适用於单节点redis的情况。如果是redis集群,比如说A客户端在Redis的master节点上拿到了锁,但是这个加锁的key还没有同步到slave节点,master故障,发生故障转移,一个slave节点升级为master节点,B客户端也可以获取同个key的锁,但客户端A也已经拿到锁了,这就导致多个客户端都拿到锁。这种情况需要其他方式处理。
二、使用lua脚本实现
使用lua脚本加锁可以保证原子性,释放锁的方法与上面相同
public boolean getLockWithLua(String key, String uniqueId, int seconds) {
String luaScript = "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);
}
本文参考以下博文,感谢原作者