前面的文章我们把Redis的基本知识都已经讲解完毕了,下面我们带入具体的场景中来逐步深入了解一下分布式锁问题。
什么是分布式锁?
为了防止分布式系统中的多个进程之间相互干扰,我们需要一种分布式协调技术来对这些进程进行调度,而这个分布式协调技术的核心就是来实现这个分布式锁。
假设成员变量X同时存在JVM1、JVM2和JVM3这三个JVM内存中,成员变量X同时会在JVM分配一块内存,三个请求发送过来同时对这个变量操作,显然结果是不对的。不是同时发过来,三个请求分别操作三个不同JVM内存中的数据,变量X之间不存在共享,也不具有可见性,处理的结果也是不对的。(变量A是一种有状态的对象)。
如果我们在业务中确实存在这个场景的话,我们就需要一种方法来解决这个问题,这就是分布式锁要解决的核心问题。
分布式锁应该具备哪些条件?
(1)在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;
(2)高可用、高性能的获取锁和释放锁;
(3)具备可重入性;
(4)具备锁失效机制,防止死锁问题出现;
(5)具备非阻塞锁的特性,即没有获取到锁将直接返回获取锁失败。
分布式锁的实现有哪些?
(1)Redis:利用Redis的setnx命令。此命令是原子性操作,只有在Key不存在的情况下,才能set成功。
(2)数据库:依赖于关系型数据库的表和自带锁来实现。
(3)Zookeeper:利用Zookeeper的顺序临时节点,来实现分布式锁和等待队列。Zookeeper设计的初衷,就是为了实现分布式锁服务的。
后买的文章中我们会讲解如何通过这三种方法实现分布式锁。
分布式锁基本概念
我们以Redis实现分布式锁为例来了解分布式锁的基本概念。
加锁
最简单的方法是使用setnx命令。Key是锁的唯一标识,按业务的类型来决定命名。value应该设置成什么呢?我们先设置为1,那么加锁的伪代码就是:
setnx(key,1)
当一个线程执行setnx返回1,说明key并不存在,该线程成功获得了锁;当一个线程执行setnx返回0,说明key已经存在,该线程抢锁失败。
解锁
当得到锁的线程执行完任务后,需要释放锁,以便其他线程可以进入。释放锁的最简单方式是执行del指令,伪代码如下:
del(key);
释放锁之后,其他线程就可以继续执行setnx命令来获取锁。
锁超时
如果一个得到锁的线程在执行任务的过程中挂掉,来不及显式的释放锁,这块资源将会永远被锁住(死锁),别的线程再也别想进来。所以,setnx的key必须设置一个超时时间,以保证即使没有被显示释放,这把锁也要在一定时间后自动释放。setnx不支持超时参数,所以需要额外的指令,伪代码如下:
expire(key,time);
将上述的三部分伪代码综合起来,就是:
if(setnv(key,1) == 1){expire(key,time);try{//业务逻辑 } finally{del(key);}
}
存在的问题
上面的伪代码实现中有三个致命的问题:
问题一:setnx指令和expire指令并不是原子性的。
这就意味着可能setnx指令刚执行完成时,线程就挂掉了,expire指令并没有执行,那么这个setnx生成的key就没有设置过期时间,也就变成了死锁;
解决方法:
问题产生是由于两条指令分开执行,可以使用set指令代替setnx指令,set指令增加了可选参数,可以在set时直接设置过期时间。
set(key,value,time,NX);
问题二:del指令的误删除。
如果某线程获得到了锁,并且这个锁的过期时间是40s。由于某些原因,拿到这把锁的线程任务执行的很慢,以至于40s过后该线程还没有执行完任务,这个时候锁自动过期了,另一个线程拿到了这把锁。在另一个线程获得这把锁执行任务时,上一个线程终于执行完了任务,执行了del操作,可是这时删除的是另一个线程上的锁,而另一个线程还没有执行完任务,这就造成了del指令误删;
解决方法:
可以在del之前判断一下当前的锁是不是自己加的,具体的实现上可以把当前线程的ID当做value,在删除前可以通过key对应的value值判断是不是自己线程的ID。
//加锁
String threadId = Thread.currentThread().getId();
set(key,threadId,time,NX);
//解锁
if(threadId.equals(redisClient.get(key))){del(key);
}
问题三:问题二的解决方法可能出现并发。
问题二的解决方法中,判断和解锁的过程是分开的,不是原子性的,可能会存在并发安全问题。
解决方法:
可以让获得锁的线程开启一个守护线程,用来给快要过期的锁“续航”。当拿到锁的线程任务还没有执行完而锁要超时过期时,守护线程会判断并给锁延长过期时间。当线程执行完任务时,会显式的关闭守护线程,这个时候锁就会自然过期;如果当前线程意外挂掉,由于守护线程和当前线程在一个进程内,所以守护线程也会挂掉,锁也会自然超时过期而不会变成无限延长超时时间的死锁。
这篇文章为分布式锁开了一个头,讲解了什么是分布式锁,并通过Redis实现分布式锁的例子来梳理了一下分布式锁的基本概念。大家有什么问题或者勘误可以在评论区留言,笔者看到都会回复的。