随着业务的升级和用户量的飙升,单一节点的系统几乎很难维持这么庞大的数据请求交互。为此,足智多谋的程序猿们发明了分布式架构。使得请求通过负载均衡或其他策略降低单台服务器的访问压力,但随着系统的分解,很多分布式的问题也随之浮出水面,比如分布式事务、分布式锁等等。本文,将为大家介绍一些主流的分布式锁。
(PS:需要有Redis和zk的API基础,本文不做案例展示,只分析原理)
一、分布式锁概念
锁的概念相信大家耳濡目染(我的前几篇博文中有相关介绍),它能确保单一JVM在高并发情况下的线程安全性。比如:sync和lock。但分布式领域中多个JVM是怎么去保证线程安全呢?仅仅依靠JVM就有些乏力了。而且锁需要保证如下特性:
独占:即当前锁对象只能被一个线程同时持有,其它未获取锁的线程只能等待锁的释放。
避免死锁:当锁的持有线程执行完业务代码后,需要手动释放锁,如果释放锁之前,线程异常终止,那么锁需要指定过期时间,否则这把锁永远不能再被使用。
可重入:对于同一把锁而言,支持锁的重入,即该锁持有线程再次访问申请锁时,不会在此基础上再加锁,而是重入。
解锁唯一:解锁的线程必须和锁持有的线程是同一个。
那么分布式锁也需要满足这些特性才行,这就涉及一个问题,锁存放在什么地方?
其实只要是多个系统共享的资源,都可以存放锁的信息,但锁要保证唯一性,即不能出现两把一模一样的锁,这不由让我们想到了Redis和Zookeeper。
众所周知,Redis的key是不允许重复的,zk的文件路径也不允许重复,所以基于这两点,我们使用Redis或Zookeeper做分布式锁。
二、Redis分布式锁
Redis的setNX可以支持返回值,当设置成功时返回1,失败返回0。基于这一点,可以设想,多个线程去设置key,谁设置成功,证明谁拿到这把锁。
在Java代码中setNX用于设置锁,setPX设置锁的超时时间,那么往往为了保证原子性,我们需要使用lua脚本把这两条命令压成一条通过Redis客户端发送至服务器执行,由于Redis单线程,所以只要这条命令执行成功,对于锁的获取行为就提交成功,等待返回结果即可。
Redis分布式锁有个很大的缺陷,就是过期时间的设置。如果业务执行时间超过了锁的过期时长,那么锁就会自动失效,其它线程进入会获取新的锁,造成一些业务上的脏数据。
已知的解决方式是加大锁过期时长,或者解锁时,将锁的过期时间和业务的执行时间锁进行比较,业务时间超过过期时间,则进行业务回滚。
但这两者都不理想,累死累活做完业务回滚数据不划算;时间太长的话,若线程中断,还得等锁的自动过期才行。于是引入了Redisson。
三、Redission分布式锁
Redission属于Redis在Java中应用的框架,基于Netty通讯。为什么说Redission可以解决Redis分布式锁过期时间问题呢?因为Redission内部维护了看门狗机制,当JVM启动后,根据spring体系的原理,会创建一个Redission的客户端,与此同时,看门狗也在这个客户端类中,看门狗中本质上是一个Map,存放分布式锁持有线程的ID以及分布式锁重入的次数,看门狗通过Timer定时去为Map中的这些分布式锁续期(默认在还剩1/3有效时长时,进行续期)。
Redission加锁时通过执行lua返回锁的有效期时间戳,如果为null,则表明当前线程获取了这把锁;否则Redission会发布一个事件通知这些未获取锁的线程通过策略减少自旋重试的次数,在信号量Semaphore获取许可(默认许可为0,所以线程会阻塞),降低CPU压力。在lua的脚本中也会判断分布式锁是否需要重入,重入则value+1。
解锁时也是通过执行lua返回释放结果,同时也会判断锁的重入,然后发布事件使用信号量Semaphore去唤醒阻塞线程。
Redission的分布式锁很大程度上和Lock相似,其背部也是运用了AQS的一些方法和原理,通过Future模式获取lua脚本的执行结果,然后运用信号量Semaphore去对未获取锁的线程进行阻塞或唤醒。所以Redission的源码分析起来还是比较简单的。
四、Zookeeper分布式锁
Zk作为文件存储系统,支持很多回调方法。由于Zk的文件地址不允许重复,所以类似于Redis,多个线程去Zk创建相同文件路径的文件节点,谁能创建成功,谁就获取了锁,未获取锁的线程可以监听此文件的状态,当锁释放时,本质上就是文件的删除。删除后,通过回调通知监听的线程,再次抢夺资源即可。
五、分布式锁问题
分布式锁还是存在很多问题,比如如果Redis或Zk集群了,那么锁的同步就会存在延时,延时过程中出现了主节点宕机,就会造成锁的重复创建。
欢迎大家和帝都的雁积极互动,头脑交流会比个人埋头苦学更有效!共勉!
CSDN:https://blog.csdn.net/yxh13521338301