分布式的CAP理论告诉我们:
任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。
所以,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可。
为了保证数据的最终一致性,需要很多的技术方案来支持。分布式锁是控制分布式系统之间同步访问共享资源的一种方式。其典型的使用场景为:
不同系统或者是同一系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,需要通过一定的互斥手段来防止彼此的干扰,以保证一致性。
分布式锁需要具备的条件
- 获取锁和释放锁的性能要好。
- 判断是否获得锁必须是原子性的,否则可能导致多个请求都获取到锁。
- 网络中断或宕机无法释放锁时,锁必须被清楚,不然会发生死锁。
- 可重入,一个线程中可以多次获取同一把锁,比如一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无需重新获得锁。
- 阻塞锁和非阻塞锁,阻塞锁即没有获取到锁,则继续等待获取锁;非阻塞锁即没有获取到锁后,不继续等待,直接返回锁失败。
分布式锁的实现方式
一、数据库锁
1. 基于数据表唯一性约束
该实现方式依靠数据库唯一索引来实现,当想要获得锁时,即向数据库中插入一条记录,释放锁时就删除这条记录。这种方式存在以下几个问题:
- 锁没有失效时间,解锁失败会导致死锁,其他线程无法再获取到锁,因为唯一索引 insert 都会返回失败。
- 只能是非阻塞锁,insert失败直接就报错了,无法进入队列进行重试。
- 不可重入,同一线程在没有释放锁之前无法再获取到锁。
需要附加额外逻辑处理这些问题。
2. 基于数据库自带锁
通过数据库的排他锁或乐观锁,根据版本号来判断更新之前有没有其他线程更新过,如果被更新过,则获取锁失败。
二、缓存锁
1.基于Memcached add命令
Memcached 的 add 命令只有 KEY 不存在时,才进行添加,或者不会处理。Memcached 所有命令都是原子性的,并发下 add 同一个 KEY ,只有一个会成功。
利用这个原理,可以先定义一个锁 LockKEY ,add 成功的认为是得到锁。并且设置过期时间,保证宕机后,也不会死锁。在具体操作完后,判断是否此次操作已超时。如果超时则不删除锁,如果不超时则删除锁。
2. 基于Redis setnx、expire命令
基于 Redis setnx(set if not exist)的特点,当缓存里 key 不存在时,才会去 set,否则直接返回 false。如果返回 true 则获取到锁,否则获取锁失败,为了防止死锁,我们再用 expire 命令对这个 key 设置一个超时时间来避免。
大体的实现过程如下:
- 线程Asetnx,值为超时的时间戳(t1),如果返回true,获得锁。
- 线程B用get 命令获取t1,与当前时间戳比较,判断是否超时,没超时false,如果已超时执行步骤3。
- 计算新的超时时间t2,使用getset命令返回t3(这个值可能其他线程已经修改过),如果t1==t3,获得锁,如果t1!=t3说明锁被其他线程获取了。
- 获取锁后,处理完业务逻辑,再去判断锁是否超时,如果没超时删除锁,如果已超时,不用处理(防止删除其他线程的锁)。
3. RedLock算法
RedLock 算法是 Redis 作者推荐的一种分布式锁实现方式,算法的内容如下:
- 获取当前时间。
- 尝试从5个相互独立Redis客户端获取锁。
- 计算获取所有锁消耗的时间,当且仅当客户端从多数节点获取锁,并且获取锁的时间小于锁的有效时间,认为获得锁。
- 重新计算有效期时间,原有效时间减去获取锁消耗的时间。
- 删除所有实例的锁。
RedLock 算法相对于单节点 Redis 锁可靠性要更高,但是实现起来条件也较为苛刻。
- 必须部署5个节点才能让RedLock的可靠性更强。
- 需要请求5个节点才能获取到锁,通过Future的方式,先并发向5个节点请求,再一起获得响应结果,能缩短响应时间,不过还是比单节点Redis锁要耗费更多时间。
三、ZooKeeper分布式锁
ZooKeeper 是一个为分布式应用提供一致性服务的软件,它内部是一个分层的文件系统目录树结构,规定统一个目录下只能有一个唯一文件名。
数据模型:
- 永久节点:节点创建后,不会因为会话失效而消失。
- 临时节点:与永久节点相反,如果客户端连接失效,则立即删除节点。
- 顺序节点:与上述两个节点特性类似,如果指定创建这类节点时,zk会自动在节点名后加一个数字后缀,并且是有序的。
监视器(watcher):
- 当创建一个节点时,可以注册一个该节点的监视器,当节点状态发生改变时,watch被触发时,ZooKeeper将会向客户端发送且仅发送一条通知,因为watch只能被触发一次。
ZooKeeper实现分布式锁:
- 创建一个锁目录lock。
- 希望获得锁的线程A就在lock目录下,创建临时顺序节点。
- 获取锁目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁。
- 线程B获取所有节点,判断自己不是最小节点,设置监听(watcher)比自己次小的节点(只关注比自己次小的节点是为了防止发生“羊群效应”)。
- 线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是最小的节点,获得锁。
四、共享存储锁
sanlock 是一个基于 SAN 的分布式锁管理器。集群中的每个节点都各自运行 sanlock 服务,锁的状态都被写到了共享存储上,使用 Disk Paxos 算法读写共享存储以实现对分布式锁的获取、释放和超时。
一般认为 SAN 中的 LUN 的可靠性要比集群中的主机高,对主机来说,应用程序进程可能会崩溃、重启,主机的 IP 网络也可能会发生故障,而 SAN 是通过专门的光网连接的,还可以配置多路径以提高可用性和吞吐量,一个 LUN 也可能被冗余映射到阵列中的多块磁盘上,因此 SAN 服务质量很高。通过利用 Disk Paxos 算法,sanlock 服务的所有数据都保存在共享存储上,即使服务器进程崩溃也不会影响可靠性。在一个集群里,通常 SAN 都是核心,可以认为如果 SAN 出现问题,整个集群的功能都会受到严重影响,因此选择以共享存储作为 sanlock 服务的基础是合理的。sanlock 的锁还可以冗余存储在多个来自不同的磁盘阵列的 LUN 上,每个 LUN 有各自的多路径,这样能够进一步提高可靠性。
小结
在分布式系统中,共享资源互斥访问问题非常普遍,而针对访问共享资源的互斥问题,常用的解决方案就是使用分布式锁,这里只介绍了几种常用的分布式锁,分布式锁的实现方式还有有很多种,根据业务选择合适的分布式锁。
数据库锁:
优点:直接使用数据库,使用简单。
缺点:分布式系统大多数瓶颈都在数据库,使用数据库锁会增加数据库负担。
缓存锁:
优点:性能高,实现起来较为方便,在允许偶发的锁失效情况,不影响系统正常使用,建议采用缓存锁。
缺点:通过锁超时机制不是十分可靠,当线程获得锁后,处理时间过长导致锁超时,就失效了锁的作用。
zookeeper锁:
优点:不依靠超时时间释放锁;可靠性高;系统要求高可靠性时,建议采用zookeeper锁。
缺点:性能比不上缓存锁,因为要频繁的创建节点删除节点。
sanlock锁:
优点:支持多种共享存储,灵活可靠。
缺点:依赖后端存储实现,相对重量级。
网友评论