分布式环境下各个主机竞争统一资源时,传统的synchronize、ReentrankLock已不能满足要求。利用redis可以方便的实现分布式锁。
分布式锁要满足如下四个条件:
- 1、互斥性 – 同一时间只有一方可以获得锁
- 2、安全性 – 释放锁时不能影响其他方获得的锁,即只能释放自己获得的锁。
- 3、死锁 – 避免获得锁的一方由于宕机而一直占用锁无法释放。
- 4、容错 – 及时redis某些节点宕机,调用方依然能够获取或者释放锁。
1 获取锁
本质上是利用redis提供的setnx原子操作方法,
setnx key vlaue
如果key存在,则不设置value并返回0,否则设置key值为value并返回1。
为了防止获取锁的进程宕机而发生死锁,需要为redis指定一个超时时间
expire key 10
但此方法实现获取锁分为两步,先获取锁然后为锁指定超时时间,如果在获取锁后,调用进程宕机,则会导致锁无法释放,因此需要(setnx+expire)合二为一的原子操作方法。最新的redis版本已经提供了这个原子方法:
SET key value [expiration EX seconds|PX milliseconds] [NX|XX]
SET name "hello" EX 10086 NX
EX表示过期时间为秒,NX表示使用setnx 方法设置key。
java实现:
public String getRedisLock(Jedis jedis, String lockKey, Long timeOut) {
try {
// 定义锁的唯一标识,用于释放琐时判断是否是当前锁,用于保证安全性
String uuid = UUID.randomUUID().toString();
if ("OK".equals(jedis.set(lockKey, lockKey, "NX", "PX", timeOut))) {
return uuid;
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
2 释放锁
释放锁分为两步:
- 判断获取锁时的uuid是否与redis中存的value值是否相等,相等则进行下一步
- 删除key
两步共同保证安全性,防止由于当前进程阻塞导致锁过期,另一个线程获取锁后更新了redis中的value值,当前进程醒来后不会删除掉最新的key。
但是前提是两步必须依然是原子操作,否则若在进行到第一步结束第二步未开始时,当前进程阻塞导致锁过期,其他进程获取锁,此时当前进程执行第二步依然会删除其他进程的锁。要想实现原子操作,需要借助lua脚本,如下:
public void unRedisLock(Jedis jedis, String lockKey, String value) {
try {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Integer result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(value));
//0释放锁失败。1释放成功
if (1 == result) {
//如果你想返回删除成功还是失败,可以在这里返回
System.out.println(result + "释放锁成功");
}
if (0 == result) {
System.out.println(result + "释放锁失败");
}
} catch (Exception e) {
e.printStackTrace();
}
}
上述脚本会以原子形式被执行,lockKey和value会替换掉KEYS[1]) 和ARGV[1],最终返回的结果为脚本执行结果。
以上就是redis实现分布式锁的过程,至于容错性的保证,可以借助redis-cluster实现,这就是另外一个话题了。