theme: smartblue
分布式锁是什么?为什么要有这个技术,解决了什么问题?
讲到分布式锁,我们会很容易联想到单机锁。在java多线程编程中,通常我们会使用锁来保护共享变量,来保证线程安全。这里面的锁作用范围是同一个进程中。如果是多个进程,应该怎么保护共享资源?这就是分布式锁来解决的问题。分布式解决的是在多个进程,如果确保对共享资源操作的正确性。
那我们应该如何实现分布式锁?
需要借助其他第三方服务来解决,比如mysql、zookeeper、Redis等。
不同实现方案
mysql
利用数据库的唯一约束特性来实现锁的互斥性。比如创建一张锁表,表中包含锁名称(用于区分不同的业务锁)、锁定状态等字段。当某个服务要获取锁时,向表中插入一条对应锁名称的记录,如果插入成功则表示获取到锁,若插入失败(违反唯一约束)则说明锁已被其他服务占有。释放锁时,只需删除对应的记录即可。
- 优点:
实现相对简单,理解容易,借助数据库的稳定性和持久性保证锁的可靠性,适用于并发量不是特别巨大的场景。 - 缺点:
性能相对较差,获取锁和释放锁涉及数据库操作,有一定的 I/O 开销;存在单点故障问题,如果数据库出现故障,整个分布式锁机制将受到影响;可能会出现死锁情况,比如获取锁的服务崩溃,没来得及释放锁。
基于 Zookeeper 实现分布式锁
Zookeeper 是一个分布式协调服务,它通过节点的创建和删除以及节点的顺序性等特性来实现分布式锁。通常利用临时顺序节点来实现,当一个服务要获取锁时,在 Zookeeper 指定路径下创建一个临时顺序节点,然后获取该路径下所有子节点,判断自己创建的节点是否是序号最小的节点,如果是则表示获取到锁;如果不是,则监听序号比自己小的节点的删除事件,当监听的节点被删除时,再次判断自己是否为最小节点来获取锁。释放锁就是删除对应的临时顺序节点。在 Java 中可以通过 Curator 等框架来方便地操作 Zookeeper 实现分布式锁。
- 优点:
可靠性高,基于 Zookeeper 的分布式一致性协议(如 ZAB 协议),能保证在集群环境下锁的一致性;具备高可用和容错性,Zookeeper 集群可以应对节点故障等情况;可以实现阻塞式获取锁,方便处理需要等待锁释放的业务场景。 - 缺点:
性能相比 Redis 稍差一些,因为涉及到节点的创建、查询、监听等一系列操作,有一定的网络开销和 Zookeeper 服务器的处理成本;实现相对复杂,需要对 Zookeeper 的相关概念和操作比较熟悉,代码的编写和理解成本相对较高。
Redis分布式锁方案
一款优秀分布式锁应该具有什么特点?
互斥性
含义:在任意时刻,对于同一个被保护的资源,只能有一个客户端能够获取到分布式锁,其他客户端若尝试获取,需等待当前持有锁的客户端释放锁之后才有机会获取。这确保了在分布式环境下针对共享资源的并发访问能够按照顺序依次进行,避免多个客户端同时操作资源而导致数据不一致等问题。
可重入性
一个已经获取到分布式锁的客户端,在锁未释放期间,如果再次请求获取该锁,仍然能够成功获取,并且锁的释放次数需要和获取次数严格匹配才能最终真正释放锁。这在一些具有递归调用或者嵌套调用逻辑的业务场景中非常重要,避免自己把自己 “锁死” 而无法继续后续操作。类似java多线程中的可冲入锁概念
阻塞特性与超时机制
- 阻塞特性
-
含义:当客户端尝试获取锁但当前锁已被其他客户端持有时,该客户端能够阻塞等待,直到获取到锁或者超时为止,而不是立刻返回失败结果。这样可以保证在高并发情况下,客户端有机会获取到锁去操作资源,不用频繁地发起获取锁的请求去轮询。
-
超时机制:
-
含义:为了防止某个客户端获取锁后由于异常情况(比如进程崩溃、网络故障等)一直没有释放锁,导致其他客户端无限期等待,需要给锁设置一个超时时间,一旦超过这个时间,锁会自动释放,让其他客户端有机会获取锁。
具体实现:
单机版本的分布式锁
最简单的分布式锁: setnx 这个命令代表如果key不存在,然后才设置。操作完成后del删除这个key就行。
但是有一个很大的问题,客户端1拿到锁之后,如果没有释放锁,或者进程挂了,没机会释放锁。其他客户端就永远没有机会拿到锁。
如何解决?
很自然的一个思路,给这个锁设置一个有效时间,类似锁设置一个超时时间,过了这个时间,自动释放锁,给其他客户端一个机会获取临界资源。我们可以使用redis 提供的 SET key 1 EX 10 NX 该命令是原子性的。
看起来很完美了,但是我们再细想一下。首先就是这个过期时间,现实中很难设置准确,设置太长,导致资源浪费,设置太短,又没法确保客户端执行完业务代码。我们有没有一种机制能够自动给锁延长时间呢?答案是存在的,我们开启一个守护线程(我们通常称为看门狗线程),定时检测这个锁的失效时间,如果锁快要过期了,操作共享资源还没有完成,我们就可以自动给锁进行续期,重新设置过期时间。
这个时候我们再想一下,还存在什么问题? 客户端释放锁,会无脑释放,并不会检查是不是自己的锁,我们需要在客户端加锁的时候,设置一个唯一标识,比如UUID,线程id等。释放锁的时候,先判断一下是不是自己的锁,是自己的锁,才会进行释放。这个时候,会设计到redis不同命令。redis通过lua脚本来确保原子性。
大概流程是:
加锁。 设置锁,并且设置一个唯一标识来标识这个锁, 操作共享资源,释放锁,lua脚本,先判断是不是自己的锁,然后再del锁。
分布式场景下的锁
如果有多台redis实例,如何实现呢?
1、客户端先获取当前时间戳t1。
2、客户端依次向n个redis实例发起加锁命令,并且每一个请求设置超时时间(这个时间要远小于锁的有效时间,不然你还没设置,锁就到期了,没有意义),如果某个实例加锁失败,就立即向下一个实例申请加锁。
3、如果超过一般以上的redis实例加锁成功,就再次获取当前时间戳t2。t2-t1看一下是否超过了锁的超时时间,没有代表客户端加锁成功,否则失败。
4、加锁成功就可以操作共享资源。
5、如果加锁失败,就需要向全部节点释放锁的请求。