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 循环
锁是非重入的,因为锁记录存在,获取锁的实例无法再次获取锁
持久节点(PERSISTENT):创建后即便客户端失去连接仍然存在
- 持久顺序节点(PERSISTENT_SEQUENTIAL):根据创建的时间顺序给该节点编号
- 临时节点(EPHEMERAL):创建后当客户端失去连接就会被 ZK 删除
- 临时顺序节点(EPHEMERAL_SEQUENTIAL):根据创建的时间顺序给该节点编号
1,具体实现
- 在 ZK 中创建一个持久节点 ParentLock 充当锁资源
- 当第一个客户端获取锁时,在 ParentLock 下创建一个临时顺序节点 Lock1
- 第一个客户端查询其同级的所有临时顺序节点并排序,并判断自己是否是最靠前的,如果是,则获得锁
- 第二个客户端同样创建一个 Lock2,并且发现自己不是最靠前的,所以会对前一个即 Lock1 注册 Watcher,监听 Lock1 是否存在,也就是说客户端2此时进入了等待状态
- 同样第三个客户端也会监听客户端2的 Lock2,此时就形成了一个监听链
- 接下来第一个客户端释放锁,分两种情况
- 正常释放锁,此时客户端1会发送删除 Lock1 指令
- 第一个客户端宕机,ZK 的特性就是当客户端宕机失去连接,其所属的临时节点,即 Lock1 会被删除
- 当 Lock1 被删除,Lock2 会收到通知,此时客户端2排序后发现自己是最小的,即获得锁
2,缺点
- 性能比 Redis 低一点,因为创建销毁及监听节点比较耗时,而且 ZK 中所有的写操作都需要 Leader 来执行,然后再将数据同步到 Follower
- 并发问题,当网络波动时,客户端没有宕机,但是 ZK 连接不到就会删除临时节点,导致别的实例也获取到锁(实际上 ZK 再失去连接后不会立刻删除节点,而是进行重试,多次重试还是不行才会删除)