什么是分布式锁?能干什么?
分布式锁是为了解决在分布式系统下面实现高并发的一种锁,可能会有人说了,我们明明有Java的synchronized等等一系列锁你不用,你要用这个?
我们来假设一个场景
例如,我们公司有一个电商系统(单体架构),这个时候,同时有两个请求打进服务器,但是现在商品只有一个了,即使我们先做了验证(先查询这个商品库存是否为1),这两个请求同时查到该库存为1,同时进行减一的操作,这个时候就不好啦,发生了超卖问题!
我们怎么处理呢?
很容易,我们在这个下单接口业务上加上一个JUC中的锁就可以了
可是这仅仅只是在单体架构下面完成的,我们都知道分布式项目的方方面面都是远远超越单体项目的,这个时候我们JUC的锁就会失效了
上图可以很明显的看出我们的系统中的锁已经失效了,仍然会出现超卖问题。所以,分布式锁就应运而生了
分布式锁其实就是一种多服务共享锁,他是在分布式部署环境下通过锁机制来让客户端互斥的对资源进行访问,而我们前面看到的JUC的锁(synchronized、Lock等)都是线程锁.只能针对于某个进程之间
分布式锁的必要条件
分布式锁必须要具备的几个条件
在分布式环境下,一个方法在同一时间只能被某一台机器的某一个线程执行
高性能的获取与释放锁
、高可用的获取与释放锁
具备可重入特性
、具备锁失效机制
(防止死锁)、具备非阻塞锁特性
(假如没有获取到锁能够直接返回获取锁失败)
分布式锁的实现方式
基于数据库实现分布式锁
基于数据库实现分布式锁是基于数据库的唯一性特性的,我们可以创建一个表来放锁,插入一条记录成功就代表获取锁成功,删除一条记录就代表释放锁
大概流程就是上图这样子,而获取失败之后我们可以进行循环等待等等一系列操作,使该锁具有种种特性。
基于Redis实现分布式锁
在叙述实现方式之前,我们先说一下setnx命令,我们都知道set命令,无论redis中是否存在该k-v
键值对,该命令都直接去新增/修改他们。而setnx命令不是这样,该命令本质也是设置k-v
键值对,但是不同的是,在设置之前他会进行判断,判断key是否已经存在,如果存在则不进行修改返回0,如果不存在则设置k-v
键值对返回1
说完这些该说一下该怎么实现分布式锁了,其实基于Redis最简单分布式锁的实现就是根据setnx
命令的
这个时候会有一些问题让我们思考,也就是如果服务A-1在后续业务没有执行完,宕机了,这个时候他并没有释放该锁,这个时候,其他服务如果想要获取该锁那么就会一直获取不到,导致始终成为一个阻塞态。
解决这个问题其实也很简单,我们给这个锁加上一个过期时间就可以啦。我们先估计一下执行后续业务需要多久,算出这个服务需要该锁多久,然后我们使用给这个锁设置一个过期时间,到了这个时间不管有没有使用完毕那么我必须释放。
这个时候又会引出一系列问题,也就是如果服务A-1后续业务执行的比较慢,然后这个时候这个锁已经过期了被强制删除了,这个时候,服务A-2尝试获取锁,成功获得该锁,此时服务A-1执行后续业务执行完毕,会尝试释放锁,但是这个时候锁其实已经释放了,这次他会把服务A-2的锁给释放掉。
上述问题我们可以有多种解法,最最简单的一种就是将锁的过期时长设置的长一点,但是如果这样子做的话我们好像又回到了之前的问题,也就是上面我叙述的服务器宕机问题,所以不推荐使用这种解法,我们一般的操作是使用唯一值value,每个线程之间设置的value都不同,这样做,我们就可以解决这个问题了,我们在释放锁的时候,先获取一下这个value,然后判断这个value是否为该线程对应的value,如果是那么释放,否则返回
这样之后,你是不是觉得这样做已经完事大吉了,其实不然,在我们判断是否是当前线程的锁的时候,以及删除锁的时候,这很明显是两个步骤,也就是说,他们并不是原子的,也许在我们判断时候这个未删除的锁是当前线程的锁,要删除的时候,系统卡了,然后,在这个阶段,这个锁过期了,并且服务A-2尝试获取锁,而且获取成功,此时服务A-1恢复并且需要释放锁,这个时候他会把服务A-2的锁释放掉,真正的生产环境中,这种情况非常少
这种情况很多小伙伴估计都不知道该怎么办了,我们不要把Lua脚本忘掉呀,这个脚本可以让Redis的操作变成原子性的,然后我们将判断并删除的这段操作变成原子性的就可以啦!!并且我们实现这个分布式锁的时候,忘掉了分布式锁的一个特性,那就是可重入性,那该怎么实现呢?同样的我们使用Lua脚本来实现。既然我们释放锁的时候都可以使用Lua脚本来实现,那么同样的,我们加锁的时候也可以使用Lua脚本来实现。
其实,实现原理并不是很难,简单来说,在加锁的时候,我们判断该锁是否存在,如果存在那么会判断一下持有该锁的线程是否为当前需要加锁线程,如果是那么将获取成功,并将可重入次数+1,如果不是那么加锁失败,同样的释放锁的时候也是这样,我们将可重入次数-1,直到可重入次数变为0的时候结束。
在日常开发中,我们并不使用这种方式来进行分布式锁的实现,市面上有已经有比较成熟的第三方工具
Redisson
,俗话说得好不要随便造轮子
这里就不在讲解Redisson了以后应该会专门发一篇文章
基于zookeeper实现分布式锁
在了解zookeeper实现分布式锁之前,我们先看看zookeeper存储结构类似与Windows的文件系统,区别在于Windows他的目录是不能存储数据的,只有文件才能存储数据
但是zookeeper的所有的层级目录,都成为ZNode,他们都是一样,没有目录和文件这种说法。只有父节点和子节点的概念
zookeeper的节点类型有四种:临时节点、临时有序节点、持久节点、持久有序节点
- 临时节点 客户端与zookeeper断开连接后,该节点自动删除
- 临时有序节点 客户端与zookeeper断开连接后,该节点会自动删除,但是这些节点都是有序排列
- 持久节点 客户端与zookeeper断开连接后,该节点依然存在
- 持久有序节点 客户端与zookeeper断开连接后,该节点依然存在,但是这些节点都是有序排列的
zookeeper的出现就是为了解决分布式服务,而使用zookeeper实现分布式锁相较于redis并不是那么难,下面两张图即可解释
比较三种方式实现分布式锁
这里我分为多个角度来分别比较三种方式
开发者理解角度:数据库 > Redis > zookeeper
开发者实现难度:zookeeper > Redis > 数据库
性能角度:Redis > zookeeper > 数据库
可靠角度: zookeeper > Redis > 数据库