今天我们一起来了解下分布式锁。分布式锁,顾名思义,是在分布式环境下的一种“锁”机制,要学习分布式锁的内容我们首先要先了解下什么是分布式锁,它和其他的“锁”有什么区别,
分布式锁的含义
什么是分布式锁,它解决了什么样的问题
分布式锁是我们分布式环境下对于资源共享安全的一种常见处理方式。在我们学习的过程中学到了很多的“锁”,比如synchronized,lock等等。它们都是用来解决共享资源同步问题的,但是在分布式系统中,这些锁却无法保证共享资源的安全性问题,因为它们都是基于jvm层面的锁,针对于单台机子。分布式环境下会有多台服务器,这些锁就失效了。而分布式锁的出现,则解决了分布式系统中的这些问题。
那么讲了这么多,分布式锁究竟是一个什么样的东西呢?
分布式锁其实是一项技术,它并不指某一个具体的东西。我们可以将解决分布式环境下共享资源安全性的技术统称为分布式锁技术。它的实现方式不止一种,不过无论是哪种方式,都必须注意以下几点:
- 互斥性:满足分布式锁最基本的条件,保证同一时刻只有一个节点获取到锁。
- 超时释放性:如果不能满足这个条件,当实现分布式锁的服务出现意外挂掉后,锁还没有释放,那么这个资源的锁将一直处于上锁状态,所以我们要保持分布式锁的超时释放性。
- 可重入性:增强了上锁的效率,降低了死锁的概率。
- 高性能/高可用性:本身就是现在分布式系统的基本要求,要保证随时任何时间锁的可用性,不能因为某个节点奔溃而使获取锁出现问题。
- 阻塞/非阻塞:阻塞:基于redis所实现。阻塞:基于mysql实现的。
其他问题:
- 死锁问题:客户端一定能获得锁,即使上一个获取了锁的客户端在释放之前奔溃或者网络不可达。
- 脑裂问题:当所依赖的中间件集群各个节点在数据同步过程中,已持有锁的客户端还未释放,另外一个客户端访问到了没有同步到锁信息的节点上,导致这个客户端也获得了锁
设计一个分布式锁需要注意的问题都这么多,那么真正实现一个分布式锁该多么复杂啊。对的,就是这样的,不过好在常用的中间件会为我们提供帮助。我们可以基于zookeeper和redis来实现分布式锁,如何选择要看自己的系统目前的配置。比如说虽然基于redis实现的锁的性能很高,但是当目前系统组件中没有redis,只有zookeeper,那我们也不能为了实现分布式锁而专门引入redis。在现实场景之下更多的是成本和性能的综合考量。
如何利用Redis实现一个简易的分布式锁呢?
无论是本地锁还是分布式锁,核心所体现的是“互斥”。
在Redis中可以基于SETNX指令来实现一个简易的分布式锁,即SET if not exists。它的实现原理是给Set里面添加K/V。先判断存不存在这个Key,不存在就添加这个Key,存在了就什么也不会做。Key用来判断锁的存在与否,Value用来判断占用情况,这样即完成了一个简单的分布式锁。
但是即使这样还是会存在很多的问题。比如这种情况下:在获取锁以后因为某些情况,释放锁的逻辑突然挂掉了。失去了释放逻辑的锁就会永远占有资源。这肯定是不能被允许的。
最简单的处理就是给锁设置一个过期的时间。一定要保证设置指定 key 的值和过期时间是一个原子操作!!! 不然的话,依然可能会出现锁无法被释放的问题。
可是还是有问题,如果过期时间小于共享资源操作时间则会出现还没进行完操作锁却被释放了。如果过期时间过长则又会影响到性能。可以在锁快要释放的时候去检测针对获取锁的资源的操作是否完成,如果没有完成那就再续一下时间。那么如何优雅的实现锁的续期呢。Java当中是有现成的解决方案的,Redisson
Redisson 是一个开源的 Java 语言 Redis 客户端,提供了很多开箱即用的功能,不仅仅包括多种分布式锁的实现。并且,Redisson 还支持 Redis 单机、Redis Sentinel、Redis Cluster 等多种部署架构。
有位大佬曾经说过,没有什么问题是引入一个中间层解决不了的,如果有,那就再引入一个。然后,大名鼎鼎的看门狗就出现了。看门狗这个名字来源于getLockWatchdogTimeout()方法。这个方法返回的是看门狗给锁续的过期时间。
看门狗名字的由来于 getLockWatchdogTimeout()
方法,这个方法返回的是看门狗给锁续期的过期时间,默认为 30 秒。并且看门狗使用lua脚本来保证了判断是否为持锁线程,判断是否需要延期,延期或者不延期这个操作是原子性的。
不可重入的分布式锁已经可以满足大部分场景需求了,但是有的时候也需要可重入的分布式锁。实现的具体思路为在获取资源的时候判断是否是当前的线程,如果是的话就不需要重新获取锁了。我们可以给锁加上一个计数器,然后给在这个时候只需要给计数器加一。无论获取多少次,只需要在释放资源时候释放同样次数即可。
而脑裂问题,也有现成的解决方案,
Redis 之父 antirez 设计了Relock算法来解决。即在每次加锁的时候,只有一半以上的节点获取到锁了才会默认加锁成功,否则加锁失败。可是外界对该方案的褒贬不一,Redlock 实现比较复杂,性能比较差,发生时钟变迁的情况下还存在安全性隐患。如果不是非要实现一个绝对可靠的分布式锁使用单机版的redis就可以了,实现简单,性能也非常的高。如果想实现一个绝对可靠的分布式锁的话,可以使用zookeeper来实现。
参照分布式锁的几大特性,我们讲完了基于Redis实现的分布式锁,现在来聊一聊给予zookeeper的分布式锁。
zookeeper拥有简单的数据结构,是基于树形结构来进行相互协调的。所实现的分布式锁则是基于临时顺序节点和事件监听器来完成的。
获取锁:
首先我们应该先有一个持久节点/locks,客户端获取锁则是在这个持久节点下建立一个临时节点。
假设客户端1来获取分布式锁,先建在locks下建立临时节点,如/locks/lock1。当建立好临时节点。若有其他客户端来获取锁,会判断一下lock1是否是locks下的最小节点。
如果是,客户端1成功获取到锁,否则失败。
假设获取失败,客户端1并不会在这里一直等待,他会回到上一个节点,在此节点建立一个事件监听器,如locks/lock0上面,用来监视lock1。这个监听器用来监听lock1节点是否被释放并且在被释放以后第一时间通知客户端1。
释放锁:
客户端1在执行完业务流程后会删除锁创建的临时节点。
由于创建的是临时顺序节点,也不用怕客户端在此过程中出现问题。出现问题后,对应的字节点也会被删除,这样也就避免了客户端出现故障所导致的锁无法被释放问题。