首先为什么要使用分布式锁,和单机环境类似,都是为了避免并发问题,只是在单机环境中,各种语言都有提供并发处理的方案,保证多个线程在同一时间只有唯一线程执行某段代码;但是在分布式环境中,这些方案就无能为力了,毕竟多个线程分布在多个节点的服务上,所以就需要三方组件来处理这种分布式并发问题,即分布式锁。分布式锁的几大要素:
- 保证在分布式部署的应用集群中,同一个方法在同一时间只能被一个节点的一个线程执行;
- 可重入锁(避免死锁);
- 最好是阻塞的(根据业务需求考虑这个);
- 高可用的获取锁和释放锁功能;
- 高性能的获取锁和释放锁功能。
目前有三种比较常见的分布式锁实现方案:
1. 基于数据库
使用数据库实现分布式锁主要是依赖数据库的一张数据表,有两种方式,一种是通过表中记录的存在情况确定当前是否有锁存在,另外一种是通过数据库的排他锁来实现分布式锁。
数据库实现分布式锁的优点:
直接借助数据库,容易理解。
数据库实现分布式锁的缺点:
- 会有各种各样的问题,在解决问题的过程中会使整个方案变得越来越复杂;
- 操作数据库需要一定的开销,性能问题需要考虑;
- 使用数据库的行级锁并不一定靠谱,尤其是当数据表并不大的时候,很容易出现表锁。
2. 基于缓存
可以使用缓存来代替数据库来实现分布式锁,这个可以提供更好的性能,同时,很多缓存服务都是集群部署的,可以避免单点问题。并且很多缓存服务都提供了可以用来实现分布式锁的方法,比如Tair的put方法,redis的setnx方法等。并且,这些缓存服务也都提供了对数据的过期自动删除的支持,可以直接设置超时时间来控制锁的释放。
- 没有失效时间?tair的put方法支持传入失效时间,到达时间之后数据会自动删除。
- 非阻塞?while重复执行。
- 非可重入?在一个线程获取到锁之后,把当前主机信息和线程信息保存起来,下次再获取之前先检查自己是不是当前锁的拥有者。
但是,失效时间我设置多长时间为好?如何设置的失效时间太短,方法没等执行完,锁就自动释放了,那么就会产生并发问题。如果设置的时间太长,其他获取锁的线程就可能要平白的多等一段时间。这个问题使用数据库实现分布式锁同样存在
使用缓存实现分布式锁的优点:
性能好,实现起来较为方便。
使用缓存实现分布式锁的缺点:
通过超时时间来控制锁的失效时间并不是十分的靠谱。
3. 基于ZK(或ETCD)
大致思想:客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的临时有序节点。 判断是否获取锁的方式很简单,只需要判断是否为有序节点中序号最小的一个。 当释放锁的时候,只需将这个临时节点删除即可。同时,临时节点的自动删除可以避免服务宕机导致的锁无法释放,而产生的死锁问题。
来看下Zookeeper能不能解决前面提到的问题。
- 锁无法释放?使用Zookeeper可以有效的解决锁无法释放的问题,因为在创建锁的时候,客户端会在ZK中创建一个临时节点,一旦客户端获取到锁之后突然挂掉(Session连接断开),那么这个临时节点就会自动删除。其他客户端就可以再次获得锁。
- 非阻塞锁?使用Zookeeper可以实现阻塞的锁,客户端可以通过在ZK中创建顺序节点,并且在节点上绑定监听器,一旦节点有变化,Zookeeper会通知客户端,客户端可以检查自己创建的节点是不是当前所有节点中序号最小的,如果是,那么自己就获取到锁,便可以执行业务逻辑了。
- 不可重入?使用Zookeeper也可以有效的解决不可重入的问题,客户端在创建节点的时候,把当前客户端的主机信息和线程信息直接写入到节点中,下次想要获取锁的时候和当前最小的节点中的数据比对一下就可以了。如果和自己的信息一样,那么自己直接获取到锁,如果不一样就再创建一个临时的顺序节点,参与排队。
- 单点问题?使用Zookeeper可以有效的解决单点问题,ZK是集群部署的,只要集群中有半数以上的机器存活,就可以对外提供服务。
使用ZK实现的分布式锁好像完全符合了本文开头我们对一个分布式锁的所有期望。但是,其实并不是,Zookeeper实现的分布式锁其实存在一个缺点,那就是性能上可能并没有缓存服务那么高。因为每次在创建锁和释放锁的过程中,都要动态创建、销毁瞬时节点来实现锁功能。ZK中创建和删除节点只能通过Leader服务器来执行,然后将数据同步到所有的Follower机器上。
其实,使用Zookeeper也有可能带来并发问题,只是并不常见而已。考虑这样的情况,由于网络抖动,客户端与ZK集群的session连接断了,那么zk以为客户端挂了,就会删除临时节点,这时候其他客户端就可以获取到分布式锁了。就可能产生并发问题。这个问题不常见是因为zk有重试机制,一旦zk集群检测不到客户端的心跳,就会重试,Curator客户端支持多种重试策略。多次重试之后还不行的话才会删除临时节点。(所以,选择一个合适的重试策略也比较重要,要在锁的粒度和并发之间找一个平衡。)
使用Zookeeper实现分布式锁的优点:
有效的解决单点问题,不可重入问题,非阻塞问题以及锁无法释放的问题。实现起来较为简单。
使用Zookeeper实现分布式锁的缺点:
性能上不如使用缓存实现分布式锁。 需要对ZK的原理有所了解。
三种方案的比较
上面几种方式,哪种方式都无法做到完美。就像CAP一样,在复杂性、可靠性、性能等方面无法同时满足,所以,根据不同的应用场景选择最适合自己的才是王道。
从理解的难易程度角度(从低到高)
数据库 > 缓存 > Zookeeper
从实现的复杂性角度(从低到高)
Zookeeper >= 缓存 > 数据库
从性能角度(从高到低)
缓存 > Zookeeper >= 数据库
从可靠性角度(从高到低)
Zookeeper > 缓存 > 数据库
参考:
https://www.cnblogs.com/austinspark-jessylu/p/8043726.html
网友评论