基于 Redis 的分布式锁对大家来说并不陌生,可是你的分布式锁有失败的时候吗?在失败的时候可曾怀疑过你在用的分布式锁真的靠谱吗?以下是结合自己的踩坑经验总结的一些经验之谈。
你真的需要分布式锁吗?
用到分布式锁说明遇到了多个进程共同访问同一个资源的问题。一般是在两个场景下会防止对同一个资源的重复访问:
-
**提高效率。**比如多个节点计算同一批任务,如果某个任务已经有节点在计算了,那其他节点就不用重复计算了,以免浪费计算资源。不过重复计算也没事,不会造成其他更大的损失。也就是允许偶尔的失败。
-
**保证正确性。**这种情况对锁的要求就很高了,如果重复计算,会对正确性造成影响。这种不允许失败。
引入分布式锁势必要引入一个第三方的基础设施,比如 MySQL,Redis,Zookeeper 等。这些实现分布式锁的基础设施出问题了,也会影响业务,所以在使用分布式锁前可以考虑下是否可以不用加锁的方式实现?不过这个不在本文的讨论范围内,本文假设加锁的需求是合理的,并且偏向于上面的第二种情况,为什么是偏向?因为不存在 100% 靠谱的分布式锁,看完下面的内容就明白了。
从一个简单的分布式锁实现说起
分布式锁的 Redis 实现很常见,自己实现和使用第三方库都很简单,至少看上去是这样的,这里就介绍一个最简单靠谱的 Redis 实现。
最简单的实现
实现很经典了,这里只提两个要点:
-
加锁和解锁的锁必须是同一个,常见的解决方案是给每个锁一个钥匙(唯一 ID),加锁时生成,解锁时判断。
-
不能让一个资源永久加锁。常见的解决方案是给一个锁的过期时间。当然了还有其他方案,后面再说。
一个可复制粘贴的实现方式如下:加锁:
public static boolean tryLock(String key, String uniqueId, int seconds) {
return "OK".equals(jedis.set(key, uniqueId, "NX", "EX", seconds));
}
这里调用了 SET key value PX milliseoncds NX,不明白这个命令的可以参考 SET key value [EX seconds|PX milliseconds] [NX|XX] [KEEPTTL]:
https://redis.io/commands/set
解锁:
public static boolean releaseLock(String key, String uniqueId) {
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(uniqueId)
).equals(1L);
}
这段实现的精髓在那个简单的 Lua 脚本上,先判断唯一 ID 是否相等再操作。
靠谱吗?
这样的实现有什么问题呢?
-
**单点问题。**上面的实现只要一个 Master 节点就能搞定,这里的单点指的是单 Master,就算是个集群,如果加锁成功后,锁从 Master 复制到 Slave 的时候挂了,也是会出现同一资源被多个 Client 加锁的。
-
**执行时间超过了锁的过期时间。**上面写到为了不出现一直上锁的情况,加了一个兜底的过期时间,时间到了锁自动释放,但是,如果在这期间任务并没有做完怎么办?由于 GC 或者网络延迟导致的任务时间变长,很难保证任务一定能在锁的过期时间内完成。
如何解决这两个问题呢?试试看更复杂的实现吧。
Redlock 算法
对于第一个单点问题,顺着 Redis 的思路,接下来想到的肯定是 Redlock 了。
Redlock 为了解决单机的问题,需要多个(大于 2)Redis 的 Master 节点,多个 Master 节点互相独立,没有数据同步。
Redlock 的实现如下:
①获取当前时间。
②依次获取 N 个节点的锁。每个节点加锁的实现方式同上。这里有个细节,就是每次获取锁的时候的过期时间都不同,需要减去之前获取锁的操作的耗时
比如传入的锁的过期时间为 500ms,获取第一个节点的锁花了 1ms,那么第一个节点的锁的过期时间就是 499ms;获取第二个节点的锁花了 2ms,那么第二个节点的锁的过期时间就是 497ms。
如果锁的过期时间小于等于 0 了,说明整个获取锁的操作超时了,整个操作失败。
③判断是否获取锁成功。如果 Client 在上述步骤中获取到了(N/2+1)个节点锁,并且每个锁的过期时间都是大于 0 的,则获取锁成功,否则失败。失败时释放锁。
④释放锁。对所有节点发送释放锁的指令,每个节点的实现逻辑和上面的简单实现一样。
为什么要对所有节点操作?因为分布式场景下从一个节点获取锁失败不代表在那个节点上加速失败,可能实际上加锁已经成功了,但是返回时因为网络抖动超时了。
以上就是大家常见的 Redlo