1.分布式锁的应用场景
当多个机器(多个进程)会对同一条数据进行修改时,并且要求这个修改是原子性的。这里有两个限定:
1. 多个进程之间的竞争,意味着JDK自带的锁失效;
2. 原子性修改,意味着数据是有状态的,修改前后有依赖。
2.分布式锁的实现条件
1. 高性能(加、解锁时高性能)
2.可以使用阻塞锁与非阻塞锁。
3.不能出现死锁。
4.可用性(不能出现节点 down 掉后加锁失败)。
3.Redis实现分布式锁
Redis实现分布式锁的过程中,参考了Redis的官方文档实现RedLock。
该文指出大部分的Redis分布式锁并没有考虑到Redis单点故障的问题并且文章指出即使搭建了Redis的集群,但基于节点之间异步模式来实现数据同步的过程中,也会导致两个进程获取同一个锁的问题。
主流的Redis锁实现方法
1.利用 Redis set key 时的一个 NX 参数可以保证在这个 key 不存在的情况下写入成功。并且再加上 EX 参数可以让该 key 在超时之后自动删除(防止释放锁失败)。
2.使用lua脚本
(530条消息) Redisson 实现分布式锁原理_redisson分布式锁实现原理_闻道☞的博客-优快云博客
在SET
命令中,有很多选项可用来修改命令的行为。 以下是SET命令可用选项的基本语法。
redis 127.0.0.1:6379> SET KEY VALUE [EX seconds] [PX milliseconds] [NX|XX]
Shell
EX seconds
− 设置指定的到期时间(以秒为单位)。PX milliseconds
- 设置指定的到期时间(以毫秒为单位)。NX
- 仅在键不存在时设置键。XX
- 只有在键已存在时才设置。
示例
redis 127.0.0.1:6379> SET mykey "redis" EX 60 NX
OK
多个尝试获取锁的客户端使用同一个key做为目标数据的唯一键,value为当前线程的唯一标识(随机的字符串taken);首先进行一次setnx命令,尝试获取锁,如果获取成功,则设置锁的最终超时时间(以防在当前进程获取锁后奔溃导致锁无法释放)。
这里利用 Redis set key 时的一个 NX 参数可以保证在这个 key 不存在的情况下写入成功。并且再加上 EX 参数可以让该 key 在超时之后自动删除。
注意:此处使用Jedis的如下方法,该命令可以保证 NX EX 的原子性。
一定不要把两个命令(NX EX)分开执行,如果在 NX 之后程序出现问题就有可能产生死锁。(以前redis的版本中只有SETNX或者SETEX,两个命令没有合并)
String set(String key, String value, String nxxx, String expx, long time);
非阻塞锁
//非阻塞锁
public static boolean tryLock(String key, long timeout, String requestId) {
String result = RedisUtil.getJedis().set(key, requestId, "NX", "EX", timeout);
if (LOCK_MSG.equals(result)) {
return true;
} else {
return false;
}
}
阻塞锁
同时也可以实现一个阻塞锁:
//实现一个阻塞锁
public static void lock(String key, long timeout, String requestId) throws Exception {
for (; ; ) {
String result = RedisUtil.getJedis().set(key, requestId, "NX", "EX", timeout);
if (LOCK_MSG.equals(result)) {
break;
}
//防止一直消耗 CPU
Thread.sleep(DEFAULT_SLEEP_TIME);
}
}
//自定义阻塞时间获取锁
public static boolean lock(String key, String requestId, int blockTime, long timeout) throws InterruptedException {
while (blockTime >= 0) {
String result = RedisUtil.getJedis().set(key, requestId, "NX", "EX", timeout);
if (LOCK_MSG.equals(result)) {
return true;
}
blockTime -= DEFAULT_SLEEP_TIME;
Thread.sleep(DEFAULT_SLEEP_TIME);
}
return false;
}
解锁
解锁就是把这个key删掉但同时要保证是set key的那个线程删掉的,所以将该操作封装为一个lua脚本,这样即可保证其原子性,eval()方法是将Lua代码交给Redis服务端执行,eval()方法可以确保原子性。
加锁时需要传递一个参数,将该参数作为这个 key 的 value,这样每次解锁时判断 value 是否相等即可。
为了更好的健壮性,将该操作
/**
* 解锁
*
* @param key
* @param requestId
* @return
*/
public static boolean unlock(String key, String requestId) {
//lua script
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = null;
result = RedisUtil.getJedis().eval(script, Collections.singletonList(key), Collections.singletonList(requestId));
if (!LOCK_MSG.equals(result)) {
System.out.println(requestId+"--释放锁成功");
return true;
} else {
System.out.println(requestId+"--释放锁失败");
return false;
}
}
看完上述的代码,再来回顾下本文开头所说的问题,单点故障问题很好理解。那么即使搭建了Redis的集群,当进程1对master节点写入了锁,此时master节点宕机。slave节点提升为master而刚刚写入master的锁还未同步,此时进程2也将能够获取锁成功。此时必然会导致数据不同步问题。还有另一个问题即: key 超时之后业务并没有执行完毕但却自动释放锁了,这样就会导致并发问题。
Redis官方给出来一种解决方案RedLock,大致实现思路如下:
存在N个Redis服务(奇数个),之间完全独立没有构成集群。
当某个进程获取锁时,如果在N/2+1个Redis服务上成功写入了锁。则获取锁成功。如果获取锁失败,一定要再写入成功了的Redis服务上del
当释放锁时,再N个Redis服务上依次del
当一个客户端获取锁失败时,这个客户端应该在一个随机延时后进行重试,之所以采用随机延时是为了避免不同客户端同时重试导致谁都无法拿到锁的情况出现。
该实现可靠性确实提升了,但算法效率特别低。不适合生产环境。
延伸的讨论:GC 可能引发的安全问题
Martin Kleppmann 曾与 Redis 之父 Antirez 就 Redis 实现分布式锁的安全性问题进行过深入的讨论,其中有一个问题就涉及到 GC。
熟悉 Java 的同学肯定对 GC 不陌生,在 GC 的时候会发生 STW(Stop-The-World),这本身是为了保障垃圾回收器的正常执行,但可能会引发如下的问题:
服务 A 获取了锁并设置了超时时间,但是服务 A 出现了 STW 且时间较长,导致了分布式锁进行了超时释放,在这个期间服务 B 获取到了锁,待服务 A STW 结束之后又恢复了锁,这就导致了 服务 A 和服务 B 同时获取到了锁,这个时候分布式锁就不安全了。
不仅仅局限于 Redis,Zookeeper 和 MySQL 有同样的问题。
想吃更多瓜的童鞋,可以访问下列网站看看 Redis 之父 Antirez 怎么说:http://antirez.com/news/101
2)单点/多点问题
如果 Redis 采用单机部署模式,那就意味着当 Redis 故障了,就会导致整个服务不可用。
而如果采用主从模式部署,我们想象一个这样的场景:服务 A 申请到一把锁之后,如果作为主机的 Redis 宕机了,那么 服务 B 在申请锁的时候就会从从机那里获取到这把锁,为了解决这个问题,Redis 作者提出了一种 RedLock 红锁 的算法 (Redission 同 Jedis):
// 三个 Redis 集群
RLock lock1 = redissionInstance1.getLock("lock1");
RLock lock2 = redissionInstance2.getLock("lock2");
RLock lock3 = redissionInstance3.getLock("lock3");
RedissionRedLock lock = new RedissionLock(lock1, lock2, lock2);
lock.lock();
// do something....
lock.unlock();