1,使用场景

在以前,我们的业务通常十分简单且通常是单机服务器,这时要想给某个业务加锁直接使用 sync 或者 lock 锁住共享资源即可,但是随着业务的扩大,单机服务器已承受不住,这是就需要搞集群服务器,部署多台实例,那么问题就来了,因为使用 sync 或 lock 加的锁只是锁住了本机的资源,是无法在集群之间共享锁状态的,所以就需要分布式锁

2,分布式锁的本源

锁的本源就是对一个所有线程共享的资源进行标记,从而让所有线程可以感知到是否有别人在使用资源,分布式锁也是如此,我们也需要创建一个共享资源来让所有的线程访问到,只需要保证这份资源在所有实例间是共享且单一的,它可以是任何东西

3,分布式锁应该具备的条件

  • 真正的单一共享,即集群环境下只允许一个实例获取执行权
  • 高可用的获取锁与释放锁
  • 高性能的获取锁与释放锁
  • 具备可重入特性
  • 具备锁失效机制防止死锁
  • 具备非阻塞锁特性,即没有获得锁的时候可以快速失败

    4,常见实现分布式锁的三种方式

    1,数据库

    通过创建一个,然后在表中记录共享资源的状态,所有的实例都来操作这个状态来实现分布式锁

    1,具体实现

    1,悲观锁

    使用 **SELECT ... WHERE ... FOR UPDATE** 的排它锁
    注意:因为判断是否被锁的条件是** WHERE name = lock**,所以 name 字段要创建索引,否则将会锁表

    2,乐观锁

    通过 CAS 版本号机制来实现,在表中创建 version 字段,每次更新时加上 version 的判断即可

    2,缺点

  • 锁依赖数据库的可用性,数据库宕机,业务崩溃

    • 搞一个多实例数据库,且有选举机制
  • 锁没有失效时间,只要客户端宕机无法解锁,记录就一直存在,别的实例无法获取锁
    • 开启一个定时任务来清理数据库中超时的数据记录
  • 锁是非阻塞的,别的实例获取不到就会快速失败,没有排队队列
    • 实例的获取锁的代码搞成 while 循环
  • 锁是非重入的,因为锁记录存在,获取锁的实例无法再次获取锁

    • 表中加一个字段记录获取锁的实例名和线程名,获取锁时先查询是否有记录且是自己的实例和线程

      2,Redis

      3,ZooKeeper

      ZooKeeper 分布式锁的原理是使用了临时顺序节点,顺便说一下 ZK 的节点类型:
  • 持久节点(PERSISTENT):创建后即便客户端失去连接仍然存在

  • 持久顺序节点(PERSISTENT_SEQUENTIAL):根据创建的时间顺序给该节点编号
  • 临时节点(EPHEMERAL):创建后当客户端失去连接就会被 ZK 删除
  • 临时顺序节点(EPHEMERAL_SEQUENTIAL):根据创建的时间顺序给该节点编号

    1,具体实现

  1. 在 ZK 中创建一个持久节点 ParentLock 充当锁资源
  2. 当第一个客户端获取锁时,在 ParentLock 下创建一个临时顺序节点 Lock1
  3. 第一个客户端查询其同级的所有临时顺序节点排序,并判断自己是否是最靠前的,如果是,则获得锁
  4. 第二个客户端同样创建一个 Lock2,并且发现自己不是最靠前的,所以会对前一个即 Lock1 注册 Watcher,监听 Lock1 是否存在,也就是说客户端2此时进入了等待状态
  5. 同样第三个客户端也会监听客户端2的 Lock2,此时就形成了一个监听链
  6. 接下来第一个客户端释放锁,分两种情况
    1. 正常释放锁,此时客户端1会发送删除 Lock1 指令
    2. 第一个客户端宕机,ZK 的特性就是当客户端宕机失去连接,其所属的临时节点,即 Lock1 会被删除
  7. 当 Lock1 被删除,Lock2 会收到通知,此时客户端2排序后发现自己是最小的,即获得锁

    2,缺点

  • 性能比 Redis 低一点,因为创建销毁及监听节点比较耗时,而且 ZK 中所有的写操作都需要 Leader 来执行,然后再将数据同步到 Follower
  • 并发问题,当网络波动时,客户端没有宕机,但是 ZK 连接不到就会删除临时节点,导致别的实例也获取到锁(实际上 ZK 再失去连接后不会立刻删除节点,而是进行重试,多次重试还是不行才会删除)