分布式锁应该具备哪些条件:
在分布式环境下,一个方法在同一时间只能被一个机器的一个线程执行;
高可用的获取锁和释放锁;
高性能的获取锁和释放锁;
具备可重入特性;
具备锁失效机制,防止死锁;
具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败;
分布式锁的应用场景:
分布式系统就可能会有一些场景用到分布式锁;zookeeper 主要应用场景:
分布式协调
分布式锁
元数据/配置信息管理
HA 高可用性
分布式协调:
zookeeper 经典用法,用zookeeper 就可以实现分布式系统之间的协调工作,一个系统发送请求之后就可以在zookeeper 上对某个节点的值注册一个监听器,一旦接收请求的系统处理完了修改zookeeper 那个节点的值,系统就能马上收到处理完成的通知。
分布式锁:
如果对某个数据连续发出两个修改操作,有两台机器同时收到了请求,但是只能一台机器执行完了另一台机器才能执行,此时就可以用分布式锁,一个机器接收到请求后先获取zookeeper 上的一把分布式锁,可以去创建一个znode 再执行;然后另一个机器也去尝试创建znode,因为已经创建了,就只能等着第一个机器执行完了才能执行。
元数据/配置信息管理:
zookeeper 可以用作很多系统的配置信息的管理,比如kafka、strom、dubbo 的注册中心等都支持。
HA 高可用性:
就是一个进程一般会做主备两个,主进程挂了可以通过zookeeper 感知到然后去切换到备用进程。
基于redis 实现分布式锁:
redis 分布式锁官方叫做RedLock 算法,是redis 官方支持的分布式锁算法,它有三个重要的考量点:
互斥(只能有一个客户端获取锁)
不能死锁
容错(只需要大部分redis 节点创建了这把锁,客户端就可以获取和释放锁)
Redis 普通的分布式锁:
最普通的实现方式就是再redis 里面使用setnx 命令创建一个key,这样就算加锁了。
命令中NX: 表示只用key 不存在的时候才会设置成功,存在就会设置失败返回nil。
PX 1000:表示经过多少毫秒后会自动释放,如果已经有了就不能加锁了。
释放就是删除key,一般可以用lua 脚本删除,删除锁的时候,找到key 对应的value,跟自己传过去的value 做比较,如果是一样的才会进行删除。
如果某个用户获取到了锁,但是阻塞了很长时间才执行完成,执行完之后可能已经释放锁了,可能别的用户端已经获取这个锁在等待了,要是这个时候删除可以的话会出现问题,所以得用随机值加lua 脚本来释放锁。但是如果是普遍使用redis 单实例,那就是单点故障。或者redis 主从时,redis 主从异步复制,如果主节点挂了,key 就没有了,key 还没有同步到从节点就从从节点转换为主节点,别人就可以设置key 从而拿到锁。
RedLock 算法:
获取一把锁的步骤:
- 获取当前的时间戳,单位是毫秒。
- 跟上面类似,轮流尝试在每个主节点上创建锁,过期时间较短,一般就几十毫秒。
- 尝试在大多数节点上建立一个锁。
- 客户端计算建立好锁的时间,如果建立锁的时间小于超过时间,就算建立成功了。
- 要是建立锁失败了就依次把之前建立过的锁删除。
- 只要别人建立了一把分布式锁,就得不断轮询去尝试获取锁。
虽然进程之间咩有同步时钟,但是每个进程中的本地时间仍然以大致相同的速率流动,与锁的自然释放时间相比误差很小。在这点上需要更好地指定我们的互斥规则:只要持有锁的客户端会在锁的有效期减去一些时间(仅几毫秒)内终止工作,就可以保证以补偿进程之间的时钟漂移。
失败重试:
当一个客户端无法获取锁时,应该在随机延迟后重试,以尝试去同步多个尝试获取同意资源锁的客户端。大多数Redis 实例中获取锁的速度越快,出现脑裂的情况就越小。因此在理想状况下,客户端应尝试将set 命令发送到N 个实例同时使用多路复用。
释放锁:
释放锁很简单,只涉及在所有实例中释放锁,无论客户端是否相信它能够成功锁定给定的实例。
zk(zookeeper)分布式锁:
zk 分布式锁简单一点就是某个节点尝试创建临时的znode,此时创建成功就获取了这个锁,这个时候别的客户端来创建锁就会失败,只能注册一个监听器来监听这个锁。释放锁就是删除这个znode,一旦释放掉就会通知客户端,然后等待着的客户端就可以再次重新加锁。
zk 的另一种方式就是创建临时顺序节点。一把锁被多个客户端竞争时就会排队,第一个拿到锁就会执行,执行我那之后释放,后面的每个客户端都会去监听排在自己前面客户端创建的节点上,一旦前面的释放了锁,后一个就会被zookeeper 通知,收到通知后自己就可以获取到锁,就可以执行代码了。
redis 分布式锁和zzookeeper 分布式锁对比:
redis 分布式锁需要自己不断去尝试获取锁,比较消耗性能;如果某个获取锁的客户端挂了,只能等待时间结束后才能释放锁。redis 分布式锁需要遍历上锁。计算时间等,操作比较麻烦。
zk 分布式锁获取不到锁就注册个监听器就好,不需要不断主动尝试获取锁,性能开销较小;zk 因为创建的时临时znode,只要客户端挂了znode 就没了,此时就自动释放锁了。
分布式锁高并发(优化)
分段式加锁:
java 里的ConcurrentHashMap 的底层原理的核心思路就是分段加锁,就是把数据分成很多个段,每个段都是单独的锁,所以多个线程过来并发修改数据的时候就可以并发的修改不同段的数据,不至于同一时间只能有一个线程占用修改。CAS 类操作在高并发场景下使用乐观锁思路会导致大量线程长时间重复循环,所以就很适合考虑分所加锁这种思路。
首先要对一个数据进行分段存储。
在每次处理数据库的时候自己去写随机算法随机挑选一个分段来处理。
如果某个分段的数据不足时,自动切换到下一个分数数据去处理。
这个过程是要手动写代码实现的,工作量大、不方便、实现复杂。