分布式锁的实现原理及多种实现方式,在工作中的实际使用,面试高频。

不仅面试中常问:说说分布式锁的实现方式,你们是怎么使用分布式锁的?分布式锁在分布式项目中也是必会的一项基本技能。

文章目录

  • 1.分布式锁的由来及为什么使用分布式锁,分布式锁的应用场景?
  • 2.分布式锁的实现方式有哪些,存在哪些问题,有没有完美的解决方案?
  • 分布式锁比较主流的解决方案有以下三种:
    • 1.基于数据库实现分布式锁。
    • 2.基于Redis自己实现或者Redisson框架,及RedLock算法解决什么问题。
    • 3.基于Zookeeper实现分布式锁。
  • 3、工作中实际使用?

1.分布式锁的由来及为什么使用分布式锁,分布式锁的应用场景?

  • 随着业务的发展需要,我们的系统可能就需要单机部署发展成了分布式集群系统,由于分布式系统多线程、多进程并且分布在不同机器上,这就导致原来单机部署情况下的并发控制锁像Synchronized、Juc包下的ReentrantLock失效,而单纯的Java Api并没有给我们提供分布式锁的解决方案,为了解决这个问题就需要一种能够跨JVM的互斥机制来控制共享资源的访问,于是就演变出来了分布式锁。
  • 分布式锁的应用场景有哪些:像日常开发中,下单秒杀、抢购商品防止库存超卖,及高并发下的缓存击穿等。

2.分布式锁的实现方式有哪些,存在哪些问题,有没有完美的解决方案?

分布式锁比较主流的解决方案有以下三种:

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

使用mysql中的悲观机制来实现,如下三个步骤

  • 创建一张用来保存锁记录的表,并且创建lock_flag字段:0表示未锁定 1表示锁定,status字段:用来判断锁执行成功没有,以及锁的开始结束时间。
  • 需要将锁定的资源作为主键或者唯一索引插入,这样就可以保证,同一时刻,只有一个线程能够执行成功,然后通过select … for update来判断能不能插入,可以的话执行insert语句插入到锁记录表中,这时就获得到了锁,如果没有上锁并且锁已经超时,则获得锁成功,若是超时未正常执行结束获取锁失败,其他情况获取锁失败,整个过程需要加上事务。
  • 方法执行完毕后,更新该条记录的lockFlag为解锁,状态为成功,进行锁的释放。

使用mysql乐观锁机制实现:

  • 乐观锁就是十分乐观,以为每次都能更新成功,当更新失败时开始重试,基于CAS思想实现。
  • 实现原理:给资源记录增加个Version字段,每次更新修改版本都会加一,然后更新商品时,把查出来的那个版本号带上条件去更新,如果和上次的版本号一致就更新,不一致则有人更新过了,更新就失败了,然后一直重试,超过了设置的重试次数则放弃重试。

2.基于Redis自己实现或者Redisson框架,及RedLock算法解决什么问题。

可以借助redis的命令中的setnx(key,value),key不存在就新增,存在就什么都不做,同时有多个客户端发送setnx命令,只有一个客户端可以成功,然后执行业务,其他请求重试,执行完删除锁。但是如果setnx刚好获得到锁,业务逻辑出现异常,就会导致锁无法释放,这时候就需要设置过期时间自动释放锁。
首先想到通过expire设置过期时间,必须保证原子性,不然如果在setnx和expire之间出现了异常,锁就无法释放了,所以使用set ex nx 命令天然具有原子性,但是又会有锁的误删情况,可以在setnx获得锁时设置一个指定的值UUID,释放前过去这个值,判断是自己的则可以删除。为了保证判断和删除时的原子性,删除锁时需要使用lua脚本,不然你删除的时候刚判断是自己的锁成功了,而此时自己的锁过期了,把人家的给删掉了。
这样看似完美了其实还有很多问题,比如业务没有执行完锁过期了,这时候就需要开启一个线程给锁续期(通过rua脚本判断只要该所锁存在没有被删就一直循环续期),同时通过redis hash结构结合lua脚本实现锁的重入。
redis集群状态(一主二从三哨兵)下存在的问题:客户端A从master获得锁,在master将锁同步到slave之前,master宕机了,slave被晋升为mater,客户端B又可以获得到锁了。这时候redis官方提供了红锁算法,需要我们部署多个完全独立的Redis master节点,同时保证在多个master实例上,与单个master实例上使用相同的加锁和释放锁方式,我们假设有5个Redis master节点(3、5、7奇数台都行),实现步骤:按照顺序向5个master节点请求加锁,根据设置的加锁超时间判断是不是需要跳过改master节点,给下一个节点去加锁,如果加锁的节点大于一半即3台,并且锁的过期时间要大于锁的使用时间,则加锁成功,如果获取锁失败则解锁,业务执行完解锁。
RedLock也存在问题,当A线程前三台刚好加锁成功,突然第三台挂了,这时候运维人员重启了,刚好B线程又拿到了3、4、5台的锁,这时候就又出现了同时获得锁的问题,避免这种情况就应该在部署情况下写明延时重启,因为延时能够保证A的业务执行完了或者A的锁失效了。
你以为这样就完了,还会有问题,当A线程拿到了锁去去执行业务,业务还没执行完,发生了STW,这时候锁的续期线程也停了,没有了续期Key过期了,这是B线程也是能加锁成功的,当STW结束,A往下执行业务,B也拿到了锁执行业务,就出现了超卖问题。
Redission就是不用我们自己去实现了分布式锁了了,同时也能配合RedLock使用。
所以比较流行的方案就是使用setnx + expire + lua或者Redission框架,根本没有完美的方案,使用鸵鸟算法吧,爱咋地咋地,于是我们使用了一台Redis结合Redission实现的。

3.基于Zookeeper实现分布式锁。

zookeeper分布式锁实现应用了临时顺序节点,当一个请求A过来时,zookeeper客户端会创建一个默认的持久节点,如果A想要获得锁,需要再改节点下创建一个临时顺序节点,接着会判断默认节点下的所有临时顺序节点,判断自己是不是排序最小的那个,如果是则获得锁成功。如果此时再来个请求B会作相同的判断,发现自己不是最小的,这时会向它排序靠前的节点注册Watcher事件,用来监听A的临时顺序节点是否存在,也就是B请求进入等待状态。如果再来一个请求C,则会再创建临时顺序节点,这是会监听B的临时节点是否存在,当zookeeper客户端完成业务或者发生故障,都会删除临时节点,释放锁,如果是任务完成,则A会显式的删除A临时顺序节点的指令。
比如当发生STW时,这是A临时节点没了,B请求也加锁成功了,就会出问题,这时候就需要结合Mysql的乐观锁机制,去增强业务了。
工作中使用不多,性能不如redis实现的分布式锁,但是可靠性较高,有封装较好的框架比如Curator。

3、工作中实际使用?

我们自己公司实际中就一台Redis做分布式锁,可以使用set ex nx + lua脚本自己实现锁续期、重入,也可以使用Redission框架,因为做过调研就算是红锁算法什么的,都不能百分百保证不出问题,那干脆使用一台得了遵循鸵鸟算法,既来之则安之。

你可能感兴趣的:(面试,我要进大厂,Java,分布式锁,分布式,微服务,Redis)