java并发编程的艺术新版读书笔记(分布式锁)
=======================
在分布式环境下,分布式锁可以保证在同一时刻只有一个实例(或节点)进行工作。
由于访问资源状态的最小单位是线程,因此分布式锁控制的粒度同单机锁一样,任意时刻只有一个实例中的线程能够获取到分布式锁。
使用分布式锁的原因
1、保证一致性
2、提高效率:避免中所有节点重复工作
分布式锁的引入并不是系统高性能的保证,分布式不一定比单机更高效,使用分布式锁的目的是在分布式环境中为水平伸缩的应用服务提供并发控制能力,保证逻辑执行的正确性。
分布式锁的分类
分类的区别在于等待获取锁的实例被唤醒的方式。
拉模式:实例自己轮询获取存储服务中的资源状态。(Redis)
推模式:实例依靠存储服务的通知来触发它去获取锁。(ZooKeeper)
如果按照这样的分类,juc单机锁属于哪种模式呢?当拥有锁的线程释放锁时,会触发AQS的release方法,该方法会唤醒同步队列中处于等待状态的头节点,使之能够尝试去获取锁,这与事件通知节点相似,所以JUC中的单机锁可以被认为是推模式。
针对拉模式的分布式锁,需要等待获取锁的实例以自旋的方式去查看资源状态是否发生变化,并根据变化来决定是否可以去获取锁。针对推模式的分布式锁,资源状态的变化会以事件的形式通知到等待锁的实例,当实例收到通知后,就可以去获取锁,而事件通知就相当于(JUC锁的)唤醒动作。
性能差异
不同分布式锁(比如redis和ZooKeeper)的性能差异,主要体现在传输协议大小、I/O链路是否非阻塞和功能实现上。
分布式锁正确性问题
出于可用性考虑,一般会使用主从结构的部署方式。考虑一种情况:如果主节点宕机,使得另一个客户端在访问新晋升的主节点时,有可能无法看到“已有”的某些资源状态,进而成功获取到锁,导致正确性再次被违反。存储服务的主从切换可能对分布式锁的正确性造成影响,后续介绍。
纵使有专有的存储服务,也无法完全保证分布式锁的正确性。绝对的正确性是无法做到的,不同的分布式锁实现由于存储服务的不同,正确性保障也有强弱之分。
数据库分布式锁的正确性保障是比较高的,依托于关系型数据库,即使主从复制可能会带来问题,也能得到较好的解决。比如MySQL可以通过开启半同步复制,牺牲一些同步效率来确保主从数据的一致性,进而提升分布式锁的正确性保障。
拉模式
每一个分布式锁都对应存储服务上的一个键,可以将存储服务认为是一个巨大(且线程安全的)map。如果可以addIfAbsent成功地新增了一个键值,则代表成功获取到了锁。
当释放锁时,实例通过传入锁的资源名称和值来删除资源状态。由于compareAndDelete操作具备先比较再删除的特性,使得只有资源状态中的值与锁资源名相同才能删除,这就保证了只有获取到锁的实例才能删除资源状态并释放锁,同时多次执行删除操作也是无副作用的。
Redis分布式锁的实现
设置自旋获取锁超时时间。如果调用存储服务setnx 失败,则会进行睡眠,睡眠时长设置随机,避免对存储服务产生无谓的瞬时压力。
Redis分布式锁的释放
释放锁可以通过锁资源名称(RN)和锁资源值(RV)删除对应的资源状态,但过程必须是原子化的。如果先根据RN查出资源状态,再比对RV与资源状态中的值是否一样,最后使用del命令删除对应键值,那么这样的两步走逻辑会导致锁有被误释放的可能。
客户端A占有分布式锁,在锁的有效期快结束时候释放锁。如果采用两步走,在删除键值之前,锁由于达到超时时间而自动释放,客户端B成功获取到了锁,并开始执行同步逻辑。客户端A由于(旧)值比对通过,接着删除了键,这时,运行在客户端B上的同步逻辑就不受锁的保护。因为其它实例又可以获取锁了。
使用lua脚本,来将CAD的过程原子化。
主从切换带来的问题
实例A给redis主节点设置了分布式锁key value,还没释放。主节点挂了。新晋主节点还没来得及将该key value同步过来。实例B这时获取分布式锁,可以获取成功。
看似完美的Redlock
Redis单节点是CP型存储服务,使用它可以满足分布式锁对于正确性的诉求,但存在可用性问题。使用Redis主从集群技术后,redis又会变成AP型存储服务,虽然提升了分布式锁的可用性,但正确性又会存在风险。面对可用性和正确性两难的局面,Redis的作者设计了不基于Redis主从技术的RedLock算法,该算法使用多个Redis节点,采用基于法定人数过半的策略,期望做到兼顾正确性与可用性。
RedLock需要使用多个Redis节点来实现分布式锁,节点数量一般是奇数,并且至少要5个节点才能使其具备良好的可用性。该算法是一个客户端算法,也就是说,它在每个客户端上的运行方式是一致的,且客户端之间不会相互通信。
本地热点锁
引入本地锁,实例中的多线程会先尝试竞争本地锁,只有成功获取到本地锁的线程才有资格参与实例间的分布式锁竞争。可以通过在分布式锁前端增加一个本地锁实现,但事实并没有那么容易,因为实例中的多线程需要使用同一把本地锁才有意义,所以需要有一个Map结构来保存锁资源名称到本地锁的映射。如果对该结构管理不当,对任意分布式锁的访问都会创建并包有本地锁,那就会使实例有内存溢出的风险。一个比较现实的做法就是针对某些热点锁进行优化,只创建热点锁对应的本地锁来有效减少对存储服务产生的压力。(即本地维护一个map,里面只存储热点key。)获取锁时,会先从本地map中查找本地锁,如果没有找到,则可以直接调用redis客户端接口获取锁,反之,尝试获取本地锁。需要注意的是,成功获取到本地锁后,如果接下来没有获取到分布式锁,就需要释放当前的本地锁,避免阻塞其他线程获取分布式锁的行为。
推模式 todo
比选择推与拉更重要的是什么
分布式环境存在网络分区且不可靠,所以分布式锁无法将一致性和可用性同时推向极致,因为他们之间存在矛盾。
解锁胜于用锁
引入分布式锁,除了降低一定的系统性能,还需要系统的维护者在未来不仅要关注同步逻辑的执行情况,还要对依赖的锁服务状况进行持续的监控,在任何业务活动来临时,都要提前给足容量。
场景一:
场景二:
原文链接: https://juejin.cn/post/7370184763678425097
文章收集整理于网络,请勿商用,仅供个人学习使用,如有侵权,请联系作者删除,如若转载,请注明出处:http://www.cxyroad.com/17851.html