Java面试题kafka

分布式锁的实现方案

2019-05-22  本文已影响488人  鸿雁长飞鱼龙潜跃

一,分布式锁的实现方案

1,基于数据库实现分布式锁

2,基于缓存实现数据库锁(redis)

3,基于zookeeper实现分布式锁

三种实现方案的对比:

简单性:

性能:

是否存在单点问题:

稳定性:

什么样的分布式锁才是一把好锁呢?业界也给出了一个标准。

首先,最基本的,分布式锁必须满足这个要求:保证在分布式系统中,加锁的代码,在同一时刻,只能被一台机器的一个线程执行。

然后,如果这把锁还能满足下面的要求,那就是好锁了。

可重入(避免死锁)。

加锁解锁性能高效。

加锁解锁高可用。

二,基于数据库实现分布式锁

大体思路是这样的:创建一张数据库表,我们称之为锁表。线程A获取锁的时候,就向锁表中插入一条记录。线程A释放锁的时候,就把这条记录删掉。

这种方式简单方便,且很容易被大家接受,所以部分项目采用的就是这种方式。

这里有一个细节很关键,就是在高并发的场景下,如果有100个线程同时插入数据,如何保证只有一个执行成功。思路是使用数据库的唯一主键特性。

基于数据库实现的分布式锁,虽然简单方便,但是也存在一些固有的缺陷。

缺陷一:单点问题。

缺陷二:没有失效时间。解锁失败将导致其他线程无法获取锁。

缺陷三:非阻塞。获取锁的方式是数据库insert操作,一旦插入报错,所有线程都无法获得锁。

缺陷四:不支持重入。同一个线程在没有释放锁之前,无法再次获得锁。

上面的缺陷如何解决呢?

缺陷一的解决方案:数据库集群,对于分布式系统来说,数据库基本上可以避免单点的情况发生。

缺陷二的解决方案:采用定时任务的方式,N分钟清理一次超时的锁记录。

缺陷三的解决方案:循环处理,直到insert执行成功。

缺陷四的解决方案:记录线程A名称和其他有用的线程信息,获取锁的时候先查询锁表,如果能查到记录,就把锁直接交给线程A。

三,基于缓存实现分布式锁(redis)

基于redis实现分布式锁,我们这里以spring boot + jedis的方式来进行说明。

思路:使用Redis数据结构中的String类型创建一条记录,由于我们使用的jedis来操作redis,那么其实我们使用的就是StringRedisTemplate这个模板,调用expire()方法,这个方法有三个入参,分别是lockPath,expire,timeUnit。释放锁的操作通过调用delete()方法来实现。

分布式锁的实现方案

基于缓存redis实现分布式锁,优点是高性能,不存在单点问题。缺点是不够稳定,可靠性很难保证。为什么这样说呢?我们来分析一下。

比如机器宕机的情况下,无法释放锁。我们这里设置一个过期时间expire来防止这种情况的发生。但是,这里还有一个问题,如果线程A超时了还没有执行完,这时线程B获取了锁,线程A处于临界区的代码就不能保证线程安全了。

这个问题有没有好的解决方案呢?

另外,是否存在这种场景,线程B释放了线程A的锁?

还有,线程A超时了,是不是就会释放锁?

四,基于zookeeper实现分布式锁

zookeeper实现分布式锁可靠稳定,如果加锁的服务器宕机,zookeeper会自动删除此服务器创建的临时节点。这样的一个优势就是即使宕机,也能释放锁,避免了因服务器宕机导致锁无法释放的问题,这是其他2种方式不具备的。但是创建临时节点和删除临时节点耗费的资源和时间相对多一些。

大致思想即为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。

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

锁无法释放?使用Zookeeper可以有效的解决锁无法释放的问题,因为在创建锁的时候,客户端会在Zookeeper中创建一个临时节点,一旦客户端获取到锁之后突然挂掉(Session连接断开),那么这个临时节点就会自动删除掉。其他客户端就可以再次获得锁。

非阻塞锁?使用Zookeeper可以实现阻塞的锁,客户端可以通过在Zookeeper中创建顺序节点,并且在节点上绑定监听器,一旦节点有变化,Zookeeper会通知客户端,客户端可以检查自己创建的节点是不是当前所有节点中序号最小的,如果是,那么自己就获取到锁,便可以执行业务逻辑了。

不可重入?使用Zookeeper也可以有效的解决不可重入的问题,客户端在创建节点的时候,把当前客户端的主机信息和线程信息直接写入到节点中,下次想要获取锁的时候和当前最小的节点中的数据比对一下就可以了。如果和自己的信息一样,那么自己直接获取到锁,如果不一样就再创建一个临时的顺序节点,参与排队。

单点问题?使用Zookeeper可以有效的解决单点问题,Zookeeper是集群部署的,只要集群中有半数以上的机器存活,就可以对外提供服务。

可以直接使用zookeeper第三方库Curator客户端,这个客户端中封装了一个可重入的锁服务。

Curator提供的InterProcessMutex是分布式锁的实现。acquire方法用户获取锁,release方法用于释放锁。

使用Zookeeper实现的分布式锁好像完全符合了本文开头我们对一个分布式锁的所有期望。但是,其实并不是,Zookeeper实现的分布式锁其实存在一个缺点,那就是性能不如缓存。因为每次在创建锁和释放锁的过程中,都要动态创建、销毁瞬时节点来实现锁功能。Zookeeper中创建和删除节点只能通过Leader服务器来执行,然后将数据同步到所有的Follower机器上。

其实,使用Zookeeper也有可能带来并发问题,只是并不常见而已。考虑这样的情况,由于网络抖动,客户端可Zookeeper集群的session连接断了,那么Zookeeper以为客户端挂了,就会删除临时节点,这时候其他客户端就可以获取到分布式锁了。就可能产生并发问题。这个问题不常见是因为有Zookeeper有重试机制,一旦Zookeeper集群检测不到客户端的心跳,就会重试,Curator客户端支持多种重试策略。多次重试之后还不行的话才会删除临时节点。(所以,选择一个合适的重试策略也比较重要,要在锁的粒度和并发之间找一个平衡。)

上面说了这么多,都是理论,接下来我们看看代码实现。大致分以下步骤。以spring boot为例。

1,pom引入zookeeper依赖。

2,application.properties加入zookeeper配置。

zookeeper.server=127.0.0.1:2178

zookeeper.lockPath=/zoolock/

3,创建zookeeper的配置类ZooConfig,这个类的主要功能是加载zookeeper服务器配置,生成CuratorFramework的bean。

4,创建分布式锁的抽象类AbstractLock。

提供加锁和解锁的抽象方法。

public abstract boolean tryLock(String

lockPath, int expire, TimeUnti timeUnti);

public abstract coud rereleaseLock(String

lockPath) throws Exception;

5,用Curator实现分布式锁。

继承抽象类AbstractLock,这里需要注意,Apache Curator 2.3.0版本没有重入锁InterProcessMutex,需要使用高版本的,我使用的是Apache Curator 4.2.1版本。

InterProcessMutex提供加锁解锁的具体实现,我们只需要调用即可,加锁方法acquire(),解锁方法release()。这里有一个关键点,InterProcessMutex是与path绑定的,每个InterProcessMutex实例对应一个key,然后把InterProcessMutex实例放到CurrentHashMap中。CurrentHashMap中的key就是lockPath。

分布式锁的实现方案

解锁操作

分布式锁的实现方案

6,定义全局锁注解。需要加锁的地方,我们只要加上这个注解就可以了。这个注解是作用在方法上的,Target注解的ElementType=METHOD。

7,定义锁的键生成器。

public interface KeyGenerator

接口中定义一个方法,

String generatorKey(ProccedingJoinPoint proccedingJoinPoint);

然后定义一个全局锁key生成器,也就是实现接口KeyGenerator。这个代码量较大,稍后分析。

8,定义锁的异常类。

9,创建全局锁注解的AOP拦截类。

public class GlobalLockInterceptor

接下来就可以使用了,需要用到分布式锁的地方加上注解,就可以工作了。

上一篇 下一篇

猜你喜欢

热点阅读