分布式锁的几种实现方式

2019-03-20  本文已影响0人  杜子龙

首先为什么要使用分布式锁,和单机环境类似,都是为了避免并发问题,只是在单机环境中,各种语言都有提供并发处理的方案,保证多个线程在同一时间只有唯一线程执行某段代码;但是在分布式环境中,这些方案就无能为力了,毕竟多个线程分布在多个节点的服务上,所以就需要三方组件来处理这种分布式并发问题,即分布式锁。分布式锁的几大要素:

  1. 保证在分布式部署的应用集群中,同一个方法在同一时间只能被一个节点的一个线程执行;
  2. 可重入锁(避免死锁);
  3. 最好是阻塞的(根据业务需求考虑这个);
  4. 高可用的获取锁和释放锁功能;
  5. 高性能的获取锁和释放锁功能。

目前有三种比较常见的分布式锁实现方案:

1. 基于数据库

使用数据库实现分布式锁主要是依赖数据库的一张数据表,有两种方式,一种是通过表中记录的存在情况确定当前是否有锁存在,另外一种是通过数据库的排他锁来实现分布式锁。

数据库实现分布式锁的优点:

直接借助数据库,容易理解。

数据库实现分布式锁的缺点:
  1. 会有各种各样的问题,在解决问题的过程中会使整个方案变得越来越复杂;
  2. 操作数据库需要一定的开销,性能问题需要考虑;
  3. 使用数据库的行级锁并不一定靠谱,尤其是当数据表并不大的时候,很容易出现表锁。

2. 基于缓存

可以使用缓存来代替数据库来实现分布式锁,这个可以提供更好的性能,同时,很多缓存服务都是集群部署的,可以避免单点问题。并且很多缓存服务都提供了可以用来实现分布式锁的方法,比如Tair的put方法,redis的setnx方法等。并且,这些缓存服务也都提供了对数据的过期自动删除的支持,可以直接设置超时时间来控制锁的释放。

但是,失效时间我设置多长时间为好?如何设置的失效时间太短,方法没等执行完,锁就自动释放了,那么就会产生并发问题。如果设置的时间太长,其他获取锁的线程就可能要平白的多等一段时间。这个问题使用数据库实现分布式锁同样存在

使用缓存实现分布式锁的优点:

性能好,实现起来较为方便。

使用缓存实现分布式锁的缺点:

通过超时时间来控制锁的失效时间并不是十分的靠谱。

3. 基于ZK(或ETCD)

大致思想:客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的临时有序节点。 判断是否获取锁的方式很简单,只需要判断是否为有序节点中序号最小的一个。 当释放锁的时候,只需将这个临时节点删除即可。同时,临时节点的自动删除可以避免服务宕机导致的锁无法释放,而产生的死锁问题。

来看下Zookeeper能不能解决前面提到的问题。

使用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

上一篇下一篇

猜你喜欢

热点阅读