面试被问Redis和zk两种分布式锁的对比

 

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

1. 悲观锁mysql

利用select … where … for update 排他锁redis

注意: 其余附加功能与实现一基本一致,这里须要注意的是“where name=lock ”,name字段必需要走索引,不然会锁表。有些状况下,好比表不大,mysql优化器会不走这个索引,致使锁表问题。算法

2. 乐观锁sql

所谓乐观锁与前边最大区别在于基于CAS思想,是不具备互斥性,不会产生锁等待而消耗资源,操做过程当中认为不存在并发冲突,只有update version失败后才能觉察到。数据库

咱们的抢购、秒杀就是用了这种实现以防止超卖。缓存

经过增长递增的版本号字段实现乐观锁mysql优化

2、基于缓存(Redis等)实现分布式锁

一、官方叫作 RedLock 算法,是 redis 官方支持的分布式锁算法。并发

这个分布式锁有 3 个重要的考量点:异步

  • 1.互斥(只能有一个客户端获取锁)
  • 2.不能死锁
  • 3.容错(只要大部分 redis 节点建立了这把锁就能够)

二、下面是redis分布式锁的各类实现方式和缺点,按照时间的发展排序分布式

一、直接setnx

直接利用setnx,执行完业务逻辑后调用del释放锁,简单粗暴

缺点:若是setnx成功,还没来得及释放,服务挂了,那么这个key永远都不会被获取到

二、setnx设置一个过时时间

为了改正第一个方法的缺陷,咱们用setnx获取锁,而后用expire对其设置一个过时时间,若是服务挂了,过时时间一到自动释放

缺点:setnx和expire是两个方法,不能保证原子性,若是在setnx以后,还没来得及expire,服务挂了,仍是会出现锁不释放的问题

三、set nx px

redis官方为了解决第二种方式存在的缺点,在2.8版本为set指令添加了扩展参数nx和ex,保证了setnx+expire的原子性,使用方法:set key value ex 5 nx

缺点:

  • 若是在过时时间内,事务尚未执行完,锁提早被自动释放,其余的线程仍是能够拿到锁
  • 上面所说的那个缺点还会致使当前的线程释放其余线程占有的锁

四、加一个事务id

上面所说的第一个缺点,没有特别好的解决方法,只能把过时时间尽可能设置的长一点,而且最好不要执行耗时任务

第二个缺点,能够理解为当前线程有可能会释放其余线程的锁,那么问题就转换为保证线程只能释放当前线程持有的锁。

即setnx的时候将value设为任务的惟一id,释放的时候先get key比较一下value是否与当前的id相同,是则释放,不然抛异常回滚,其实也是变相地解决了第一个问题

缺点:get key和将value与id比较是两个步骤,不能保证原子性

五、set nx px + 事务id + lua

咱们能够用lua来写一个getkey并比较的脚本,jedis/luttce/redisson对lua脚本都有很好的支持

缺点:集群环境下,对master节点申请了分布式锁,因为redis的主从同步是异步进行的,master在内存中写入了nx以后直接返回,客户端获取锁成功。

此时master节点挂了,而且数据还没来得及同步,另外一个节点被升级为master,这样其余的线程依然能够获取锁。

六、redlock

为了解决上面提到的redis集群中的分布式锁问题,redis的做者antirez的提出了red lock的概念,假设集群中全部的n个master节点彻底独立,而且没有主从同步。

此时对全部的节点都去setnx,而且设置一个请求过时时间re和锁的过时时间le,同时re必须小于le(能够理解,否则请求3秒才拿到锁,而锁的过时时间只有1秒也太蠢了)。

此时若是有n / 2 + 1个节点成功拿到锁,这次分布式锁就算申请成功。

缺点:可靠性尚未被普遍验证,而且严重依赖时间,好的分布式系统应该是异步的,并不能以时间为担保,程序暂停、系统延迟等均可能会致使时间错误。

3、基于zookeeper实现的分布式锁

1. 实现方式

ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个惟一文件名。基于ZooKeeper实现分布式锁的步骤以下:

  1. 建立一个目录mylock;
  2. 线程A想获取锁就在mylock目录下建立临时顺序节点;
  3. 获取mylock目录下全部的子节点,而后获取比本身小的兄弟节点,若是不存在,则说明当前线程顺序号最小,得到锁;
  4. 线程B获取全部节点,判断本身不是最小节点,设置监听比本身次小的节点;
  5. 线程A处理完,删除本身的节点,线程B监听到变动事件,判断本身是否是最小的节点,若是是则得到锁。

这里推荐一个Apache的开源库Curator,它是一个ZooKeeper客户端,Curator提供的InterProcessMutex是分布式锁的实现,acquire方法用于获取锁,release方法用于释放锁。

优势:具有高可用、可重入、阻塞锁特性,可解决失效死锁问题。

缺点:由于须要频繁的建立和删除节点,性能上不如Redis方式。

2. 两种利用特性实现原理:

一、利用临时节点特性

zookeeper的临时节点有两个特性,一是节点名称不能重复,二是会随着客户端退出而销毁,所以直接将key做为节点名称,可以成功建立的客户端则获取成功,失败的客户端监听成功的节点的删除事件

缺点:全部客户端监听同一个节点,可是同时只有一个节点的事件触发是有效的,形成资源的无效调度

二、利用顺序临时节点特性

zookeeper的顺序临时节点拥有临时节点的特性,同时,在一个父节点下建立建立的子临时顺序节点,会根据节点建立的前后顺序,用一个32位的数字做为后缀。

咱们能够用key建立一个根节点,而后每次申请锁的时候在其下建立顺序节点,接着获取根节点下全部的顺序节点并排序,获取顺序最小的节点,若是该节点的名称与当前添加的名称相同。

则表示可以获取锁,不然监听根节点下面的处于当前节点以前的节点的删除事件,若是监听生效,则回到上一步从新判断顺序,直到获取锁。

总结

基于数据库分布式锁实现

优势:直接使用数据库,实现方式简单。

缺点:

  1. db操做性能较差,而且有锁表的风险
  2. 非阻塞操做失败后,须要轮询,占用cpu资源;
  3. 长时间不commit或者长时间轮询,可能会占用较多链接资源

基于redis缓存

  1. redis set px nx + 惟一id + lua脚本

优势:redis自己的读写性能很高,所以基于redis的分布式锁效率比较高

缺点:依赖中间件,分布式环境下可能会有节点数据同步问题,可靠性有必定的影响,若是发生则须要人工介入

  1. 基于redis的redlock

优势:能够解决redis集群的同步可用性问题

缺点:

  1. 依赖中间件,并无被普遍验证,维护成本高,须要多个独立的master节点;须要同时对多个节点申请锁,下降了一些效率
  2. 锁删除失败 过时时间很差控制
  3. 非阻塞,操做失败后,须要轮询,占用cpu资源;

基于zookeeper的分布式锁

优势:不存在redis的超时、数据同步(zookeeper是同步完之后才返回)、主从切换(zookeeper主从切换的过程当中服务是不可用的)的问题,可靠性很高

缺点:依赖中间件,保证了可靠性的同时牺牲了一部分效率(可是依然很高)。性能不如redis。

jdk的方式不太推荐。

  1. 从理解的难易程度角度(从低到高)数据库 > 缓存 > Zookeeper
  2. 从实现的复杂性角度(从低到高)Zookeeper >= 缓存 > 数据库
  3. 从性能角度(从高到低)缓存 > Zookeeper >= 数据库
  4. 从可靠性角度(从高到低)Zookeeper > 缓存 > 数据库

没有绝对完美的实现方式,具体要选择哪种分布式锁,须要结合每一种锁的优缺点和业务特色而定。

 

相关文章
相关标签/搜索