1. 为什么需要分布式锁?

首先我们需要明白为什么需要加锁,再并发环境下为保证数据一致性,则需要加锁;比如库存管理,当多个线程操作同一条库存记录的时候,需要加锁来保证互斥;我们可以通过ReentrantLock、synchronized来是实现;

加分布式锁是因为再分布式集群部署的情况下,并发指的不是多个线程,二是多个进程或多个服务器;如果只是加线程锁只能保证一个JVM下的线程互斥,而其他的JVM还是可以进行访问的,从而造成数据不一致!

分布式锁必须要具备的特性:

  • 「互斥性」: 任意时刻,只有一个客户端能持有锁。
  • 「锁超时释放」:持有锁超时,可以释放,防止不必要的资源浪费,也可以防止死锁。
  • 「可重入性」:一个线程如果获取了锁之后,可以再次对其请求加锁。
  • 「高性能和高可用」:加锁和解锁需要开销尽可能低,同时也要保证高可用,避免分布式锁失效。
  • 「安全性」:锁只能被持有的客户端删除,不能被其他客户端删除

2. 分布式锁那些实现方案?

2.1. 实现方案及原理

2.1.1. 数据库实现分布式锁

  1. 增加锁表(主要记录那个机器那个服务那个方法,获取锁时,则进行插入,如果插入成功则加锁成功,否则失败;释放锁删除记录即可)
  2. 乐观锁(条件,比如库存>0的时候才修改;版本号,如果版本号一直则修改,修改后版本号+1)
  3. 悲观锁(select for update,正常情况下是行锁,如果没有正确使用,比如没有索引或没有记录或隔离级别是串行化则会升级成表锁!)

2.1.2. Zookeeper实现分布式锁

主要通过znode节点和watch机制实现的;

znode节点有四种:

  1. 持久节点:一旦创建,永久存在zookeeper中,除非手动删除
  2. 持久有序节点:持久节点增加序号,并且是有序的;
  3. 临时节点:节点创建后,如果服务重启或宕机,则被删除;
  4. 临时有序节点:临时节点增加序号,并且是有序的;

watch机制: 监听节点变更,比如B节点监听A节点,当A节点状态发生变更或删除,则B节点就会收到通知!

实现流程:

  1. 当首次对共享资源访问的时候,zookeeper会创建一个持久节点作为父节点
  2. 当有其他应用进程访问的访问的时候,会在父节点后面依次创建临时有序节点
  3. 如果临时有序节点是最小的,则可以获取到锁
  4. 没有获取到锁的进程会对前一个节点进行监控,一旦就会依次唤醒

所以可以理解为zookeeper是公平锁,但是可以是实现非公平锁,比如只创建一个临时有序节点,其他进程进行争抢!

2.1.3. Redis实现分布式锁

  1. setnx(如果key不存在则设置成功) + expire(设置过期时间,避免死锁) + delete (释放锁)
    1. setnx + expire可以使用一条命令来实现保证原子性;
    2. 加锁时使用requestId避免其他线程释放锁,比如A获取锁后因业务逻辑没有执行完就到了过期时间释放了锁,B获取到锁后刚开始执行,A执行结束就把锁释放了,增加requestId后,可以先查询在判断是否可以释放,因为分了两步所以需要使用lua脚本保证原子性;
    3. 过期时间设置问题,如果A没有执行结束就释放锁,如果B还是和A执行相同的业务逻辑就会造成重复执行,可以在创建锁的时候增加守护线程来通过定时任务增加过期时间,释放的时候删除守护线程;
  2. 使用三方类库Redisson
    1. 上面的requestId以及过期时间问题,Redisson早就解决了,requestId在lua脚本中有判断、过期时间是通过看门狗机制实现的
    2. 加解锁实现原理,请看图:

img

img

  1. RedLock(红锁)
    1. 解决集群部署时,如果主节点挂了,但是没有把锁信息同步给从节点而出现问题
    2. 原理:为每个节点加锁,如果加锁的节点大于一般则获取锁成功,释放锁也是同理
    3. 在业界很有争议,redison的作者Antirez和Martin(分布式大佬)battle,Martin说并没有保证并发安全,分布式环境可能会出现线程暂停,比如A在加锁后进程暂停了,超过过期时间进行释放,B获取锁执行中,A进程开始执行就会认为获取到了锁会继续执行从而出现问题;而Antirez会说会检测到从而停止执行!

2.2. 具体区别以及适用场景

  1. 数据库实现分布式锁性能比较差,有较大的代码改动;
  2. Redis中更多的采用的是Redisson方案,简单,并且性能是三种方案中最好的,可以抗住高并发的加解锁,但是在集群部署的收也会偶尔出现数据不正确的情况;
  3. Zookeeper保证强一致性,如果加解锁频繁Zookeepre服务器压力会很大

具体选择那个根据实际开发环境选择,比如我们系统已经使用了Redis做缓存,也能容忍极少数情况下数据不正确就选择了Redisson;如果你们系统已经使用了Zookeeper并且数据要求非常严格,就是用Zookpeer实现分布式锁!