高阶程序员之路-轻松学习分布式锁

前言

随着互联网技术的不断发展,数据量的不断增长,业务逻辑日趋复杂,在这种背景下,传统的集中式系统已经没法知足咱们的业务需求,分布式系统被应用在更多的场景,而在分布式系统中访问共享资源就须要一种互斥机制,来防止彼此之间的互相干扰,以保证一致性,在这种状况下,咱们就须要用到分布式锁。redis

分布式一致性问题

首先咱们先来看一个小例子:算法

假设某商城有一个商品库存剩10个,用户A想要买6个,用户B想要买5个,在理想状态下,用户A先买走了6了,库存减小6个还剩4个,此时用户B应该没法购买5个,给出数量不足的提示;而在真实状况下,用户A和B同时获取到商品剩10个,A买走6个,在A更新库存以前,B又买走了5个,此时B更新库存,商品还剩5个,这就是典型的电商“秒杀”活动。数据库

从上述例子不难看出,在高并发状况下,若是不作处理将会出现各类不可预知的后果。那么在这种高并发多线程的状况下,解决问题最有效最广泛的方法就是给共享资源或对共享资源的操做加一把锁,来保证对资源的访问互斥。在Java JDK已经为咱们提供了这样的锁,利用ReentrantLcok或者synchronized,便可达到资源互斥访问的目的。可是在分布式系统中,因为分布式系统的分布性,即多线程和多进程而且分布在不一样机器中,这两种锁将失去原有锁的效果,须要咱们本身实现分布式锁——分布式锁。缓存

分布式锁须要具有哪些条件

  1. 获取锁和释放锁的性能要好
  2. 判断是否得到锁必须是原子性的,不然可能致使多个请求都获取到锁
  3. 网络中断或宕机没法释放锁时,锁必须被清楚,否则会发生死锁
  4. 可重入一个线程中能够屡次获取同一把锁,好比一个线程在执行一个带锁的方法,该方法中又调用了另外一个须要相同锁的方法,则该线程能够直接执行调用的方法,而无需从新得到锁;

5.阻塞锁和非阻塞锁,阻塞锁即没有获取到锁,则继续等待获取锁;非阻塞锁即没有获取到锁后,不继续等待,直接返回锁失败。网络

分布式锁实现方式

1、数据库锁

  1. 基于MySQL锁表

该实现方式彻底依靠数据库惟一索引来实现,当想要得到锁时,即向数据库中插入一条记录,释放锁时就删除这条记录。这种方式存在如下几个问题:多线程

(1) 锁没有失效时间,解锁失败会致使死锁,其余线程没法再获取到锁,由于惟一索引insert都会返回失败。并发

(2) 只能是非阻塞锁,insert失败直接就报错了,没法进入队列进行重试分布式

(3) 不可重入,同一线程在没有释放锁以前没法再获取到锁高并发

  1. 采用乐观锁增长版本号

根据版本号来判断更新以前有没有其余线程更新过,若是被更新过,则获取锁失败。性能

2、缓存锁

这里咱们主要介绍几种基于redis实现的分布式锁:

1. 基于setnx、expire两个命令来实现

基于setnx(set if not exist)的特色,当缓存里key不存在时,才会去set,不然直接返回false。若是返回true则获取到锁,不然获取锁失败,为了防止死锁,咱们再用expire命令对这个key设置一个超时时间来避免。可是这里看似完美,实则有缺陷,当咱们setnx成功后,线程发生异常中断,expire还没来的及设置,那么就会产生死锁。

解决上述问题有两种方案

第一种是采用redis2.6.12版本之后的set,它提供了一系列选项

EX seconds – 设置键key的过时时间,单位时秒

PX milliseconds – 设置键key的过时时间,单位时毫秒

NX – 只有键key不存在的时候才会设置key的值

XX – 只有键key存在的时候才会设置key的值

第二种采用setnx(),get(),getset()实现,大致的实现过程以下:

(1) 线程Asetnx,值为超时的时间戳(t1),若是返回true,得到锁。

(2) 线程B用get 命令获取t1,与当前时间戳比较,判断是否超时,没超时false,若是已超时执行步骤3

(3) 计算新的超时时间t2,使用getset命令返回t3(这个值可能其余线程已经修改过),若是t1==t3,得到锁,若是t1!=t3说明锁被其余线程获取了

(4) 获取锁后,处理完业务逻辑,再去判断锁是否超时,若是没超时删除锁,若是已超时,不用处理(防止删除其余线程的锁)

2. RedLock算法

redlock算法是redis做者推荐的一种分布式锁实现方式,算法的内容以下:

(1) 获取当前时间;

(2) 尝试从5个相互独立redis客户端获取锁;

(3) 计算获取全部锁消耗的时间,当且仅当客户端从多数节点获取锁,而且获取锁的时间小于锁的有效时间,认为得到锁;

(4) 从新计算有效期时间,原有效时间减去获取锁消耗的时间;

(5) 删除全部实例的锁

redlock算法相对于单节点redis锁可靠性要更高,可是实现起来条件也较为苛刻。

(1) 必须部署5个节点才能让Redlock的可靠性更强。

(2) 须要请求5个节点才能获取到锁,经过Future的方式,先并发向5个节点请求,再一块儿得到响应结果,能缩短响应时间,不过仍是比单节点redis锁要耗费更多时间。

而后因为必须获取到5个节点中的3个以上,因此可能出现获取锁冲突,即你们都得到了1-2把锁,结果谁也不能获取到锁,这个问题,redis做者借鉴了raft算法的精髓,经过冲突后在随机时间开始,能够大大下降冲突时间,可是这问题并不能很好的避免,特别是在第一次获取锁的时候,因此获取锁的时间成本增长了。

若是5个节点有2个宕机,此时锁的可用性会极大下降,首先必须等待这两个宕机节点的结果超时才能返回,另外只有3个节点,客户端必须获取到这所有3个节点的锁才能拥有锁,难度也加大了。

若是出现网络分区,那么可能出现客户端永远也没法获取锁的状况,介于这种状况,下面咱们来看一种更可靠的分布式锁zookeeper锁。

zookeeper分布式锁

首先咱们来了解一下zookeeper的特性,看看它为何适合作分布式锁,

zookeeper是一个为分布式应用提供一致性服务的软件,它内部是一个分层的文件系统目录树结构,规定统一个目录下只能有一个惟一文件名。

数据模型:

永久节点:节点建立后,不会由于会话失效而消失

临时节点:与永久节点相反,若是客户端链接失效,则当即删除节点

顺序节点:与上述两个节点特性相似,若是指定建立这类节点时,zk会自动在节点名后加一个数字后缀,而且是有序的。

监视器(watcher):

当建立一个节点时,能够注册一个该节点的监视器,当节点状态发生改变时,watch被触发时,ZooKeeper将会向客户端发送且仅发送一条通知,由于watch只能被触发一次。

根据zookeeper的这些特性,咱们来看看如何利用这些特性来实现分布式锁:

  1. 建立一个锁目录lock
  2. 但愿得到锁的线程A就在lock目录下,建立临时顺序节点
  3. 获取锁目录下全部的子节点,而后获取比本身小的兄弟节点,若是不存在,则说明当前线程顺序号最小,得到锁
  4. 线程B获取全部节点,判断本身不是最小节点,设置监听(watcher)比本身次小的节点(只关注比本身次小的节点是为了防止发生“羊群效应”)
  5. 线程A处理完,删除本身的节点,线程B监听到变动事件,判断本身是最小的节点,得到锁。

小结

在分布式系统中,共享资源互斥访问问题很是广泛,而针对访问共享资源的互斥问题,经常使用的解决方案就是使用分布式锁,这里只介绍了几种经常使用的分布式锁,分布式锁的实现方式还有有不少种,根据业务选择合适的分布式锁,下面对上述几种锁进行一下比较:

数据库锁:

优势:直接使用数据库,使用简单。

缺点:分布式系统大多数瓶颈都在数据库,使用数据库锁会增长数据库负担。

缓存锁:

优势:性能高,实现起来较为方便,在容许偶发的锁失效状况,不影响系统正常使用,建议采用缓存锁。

缺点:经过锁超时机制不是十分可靠,当线程得到锁后,处理时间过长致使锁超时,就失效了锁的做用。

zookeeper锁:

优势:不依靠超时时间释放锁;可靠性高;系统要求高可靠性时,建议采用zookeeper锁。

缺点:性能比不上缓存锁,由于要频繁的建立节点删除节点。

相关文章
相关标签/搜索