分布式锁
分布式场景下,保证同一时刻对共享数据只能被一个应用的一个线程操作。用来保证共享数据的安全性和一致性。
常见实现方式:
- 基于数据库实现
- 基于
ZooKeeper
实现 - 基于
Redis
实现
分布式锁特性
- 排他性:同一时刻只能有一个应用的一个线程可以执行加锁的方法,获取到锁。
- 高性能:分布式锁可能会有很多的服务器来获取,高性能的获取和释放,不能因为某一个分布式锁获取的服务不可用,导致所有服务都拿不到或释放锁,所以要满足高可用要求。
- 锁失效机制,防止死锁:假设某个应用获取到锁之后,一直没有来释放锁,可能服务本身已经挂掉了,不能一直不释放,导致其他服务一直获取不到锁。
- 可重入性:一个应用如果成功获取到锁之后,再次获取锁也可以成功
- 非堵塞:在某个服务来获取锁时,假设该锁已经被另一个服务获取,我们要能直接返回失败,不能一直等待。
基于数据库
- 利用数据库的唯一约束 UNIQUE KEY
- 排他锁 for update
排他性:具备
高可用:多机部署,主从同步、主备切换等。
高性能:没有 nosql 数据库并发性能高。
失效机制:单独增加定时任务,按照记录的更新时间定时清除或者增加超时时间字段,sql 中进行优化
可重入性:通过增加字段,记录占有锁的应用节点信息和线程信息,再次获取锁时判断是否是当前线程获取的锁达到可重入的特性。
非阻塞特性:具备,在获取锁失败时,会直接返回失败。但是无法满足超时获取的场景,比如5秒内获取不到锁再失败等。
基于ZooKeeper
临时 znode
- 节点尝试创建临时 znode,此时创建成功了就获取了这个锁
- 其他客户端来创建锁会失败,只能注册个监听器监听这个锁
- 释放锁就是删除这个 znode,一旦释放掉就会通知客户端
- 等待着的客户端就可以再次重新加锁。
羊群效应:如果有1000个客户端发起请求并创建临时节点,都会去监听A结点的变化,然后A删除节点的时候会通知其他节点,耗费资源了
临时顺序节点
临时节点和顺序节点的结合体,每个节点创建时会指定顺序编号,并且在客户端与ZK服务端断开时,节点会被删除。
- 创建一个持久节点表示一个分布式锁节点
- 所有客户端都尝试去创建临时有序节点以获取锁
- 序号最小的临时有序节点获得锁
- 未获取到锁的客户端给自己的上一个临时有序节点添加监听
- 获得锁的客户端进行自己的操作,操作完成之后删除自己的临时有序节点
- 当监听到自己的上一个临时有序节点释放了锁,尝试自己去加锁
- 操作完成之后释放锁
- 之后剩下的客户端重复加锁和解锁的操作
JVM 调优,尽量避免长时间 GC 的情况发生:
由于ZK依靠session定期的心跳来维持客户端,如果客户端进入长时间的GC,可能会导致ZK认为客户端宕机而释放锁,让其他的客户端获取锁,但是客户端在GC回复后,会认为自己还持有锁。
基于Redis
基于 lua 脚本
采用Lua脚本,来保证SETNX+EXPIRE+随机value操作的原子性。
public boolean getLock(String key,String uniId,Long expireTime){
//加锁
return jedis.set(key, uniId, "NX", "EX", expireTime) == 1;
}
// 解锁
public boolean releaseLock(String key,String uniId){
// 因为get和del操作并不是原子的,所以使用lua脚本
String lua_script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1])
+"else return 0 end;";
Object result = jedis.eval(lua_scripts,Collections.singletonList(key),Collections.singletonList(uniId));
return result.equals(1L);
}
随机值防止锁误删,一些情况下,执行时间超过了锁过期时间,此时其他线程也获取到了锁,如果直接删除,导致更多的客户端能够获取到锁。
RedLock 算法
RedLock的核心原理:
- 在Redis集群中选出多个Master节点,保证这些Master节点不会同时宕机;
- 并且各个Master节点之间相互独立,数据不同步;
- 使用与Redis单实例相同的方法来加锁和解锁。
那么RedLock到底是如何来保证在有节点宕机的情况下,还能安全的呢?
- 假设集群中有N台Master节点,首先,获取当前时间戳;
- 客户端按照顺序使用相同的key,value依次获取锁,并且获取时间要比锁超时时间足够小;比如超时时间5s,那么获取锁时间最多1s,超过1s则放弃,继续获取下一个;
- 客户端通过获取所有能获取的锁之后减去第一步的时间戳,这个时间差要小于锁超时时间,并且要至少有N/2 + 1台节点获取成功,才表示锁获取成功,否则算获取失败;
- 如果成功获取锁,则锁的有效时间是原本超时时间减去第三不得时间差;
- 如果获取锁失败,则要解锁所有的节点,不管该节点加锁时是否成功,防止有漏网之鱼。
方案总结
- 分布式锁,数据库本身就不适合分布式锁,并发不高的情况下可以尝试
- redis 分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能。
- zk 分布式锁,获取不到锁,注册个监听器即可,不需要不断主动尝试获取锁,性能开销较小。
- Redis 获取锁的那个客户端 出现 bug 挂了,那么只能等待超时时间之后才能释放锁;而 zk 的话,因为创建的是临时 znode,只要客户端挂了,znode 就没了,此时就自动释放锁。