分布式锁

1. zookeeper 实现

Zookeeper是通过创建临时顺序节点的方式来实现。

zookeeper分布式锁

  1. 当需要对资源进行加锁时,实际上就是在父节点之下创建一个临时顺序节点。
  2. 客户端A来对资源加锁,首先判断当前创建的节点是否为最小节点,如果是,那么加锁成功,后续加锁线程阻塞等待
  3. 此时,客户端B也来尝试加锁,由于客户端A已经加锁成功,所以客户端B发现自己的节点并不是最小节点,就会去取到上一个节点,并且对上一节点注册监听
  4. 当客户端A操作完成,释放锁的操作就是删除这个节点,这样就可以触发监听事件,客户端B就会得到通知,同样,客户端B判断自己是否为最小节点,如果是,那么则加锁成功。

2. redis 方案

redis setNX EX :

NX 代表如果要设置的key已存在,则取消设置

EX 代表过期时间为秒,PX则为毫秒

2.1 锁超时问题和锁误删

客户A加锁成功3秒,业务流程执行超过3秒,锁释放,客户B申请锁成功,客户A执行完释放锁,导致释放B申请的锁。

锁超时

  • 评估业务设置合适的超时时间
  • 自动续期,通过其他线程为将要过期的线程续期

锁误删:

  • set 时 生成唯一id,只解锁自己申请的锁,通过 lua脚本解锁

2.2 RedLock算法

因为在Redis的主从架构下,主从同步是异步的,如果在Master节点加锁成功后,指令还没有同步到Slave节点,此时Master挂掉,Slave被提升为Master,新的Master上并没有锁的数据,其他的客户端仍然可以加锁成功

RedLock的理念下需要至少2个Master节点,多个Master节点之间完全互相独立,彼此之间不存在主从同步和数据复制

  1. 获取当前Unix时间
  2. 按照顺序依次尝试从多个节点锁,如果获取锁的时间小于超时时间,并且超过半数的节点获取成功,那么加锁成功。这样做的目的就是为了避免某些节点已经宕机的情况下,客户端还在一直等待响应结果。举个例子,假设现在有5个节点,过期时间=100ms,第一个节点获取锁花费10ms,第二个节点花费20ms,第三个节点花费30ms,那么最后锁的过期时间就是100-(10+20+30),这样就是加锁成功,反之如果最后时间<0,那么加锁失败
  3. 如果加锁失败,那么要释放所有节点上的锁

RedLock 存在问题

  • 性能、资源 : 对多个节点加锁,耗时加长
  • 节点崩溃重启

​ 比如有1~5号五个节点,并且没有开启持久化,客户端A在1,2,3号节点加锁成功,此时3号节点崩溃宕机后发生重启,就丢失了加锁信息,客户端B在3,4,5号节点加锁成功。

  1. Redis作者建议的方式就是延时重启,比如3号节点宕机之后不要立刻重启,而是等待一段时间后再重启,这个时间必须大于锁的有效时间,也就是锁失效后再重启,这种人为干预的措施真正实施起来就比较困难了
  2. 第二个方案那么就是开启持久化,但是这样对性能又造成了影响。比如如果开启AOF默认每秒一次刷盘,那么最多丢失一秒的数据,如果想完全不丢失的话就对性能造成较大的影响。
  • GC、网络延迟

client1线程获取到锁,然后发生GC停顿,超过了锁的有效时间导致锁被释放,然后锁被client2拿到,然后两个客户端同时拿到锁在写数据,问题产生。

  • 时钟跳跃

​ 假设发生网络分区,4、5号节点变为一个独立的子网,3号节点发生始终跳跃(不管人为操作还是同步导致)导致锁过期,这时候另外的客户端就可以从3、4、5号节点加锁成功,问题又发生了。

2.3 终极方案 redission

支持单机,集群,哨兵

1.加锁

zookeeper分布式锁

2. 释放锁

zookeeper分布式锁

  1. 如果key都不存在了,那么就直接返回
  2. 如果key、field不匹配,那么说明不是自己的锁,不能释放,返回空
  3. 释放锁,重入次数-1,如果还大于0那么久刷新过期时间,反之那么久删除锁

3. watchdog

​ 解决了锁超时导致的问题,实际上就是一个后台线程,默认每隔10秒自动延长锁的过期时间。

默认的时间就是internalLockLeaseTime / 3internalLockLeaseTime默认为30秒。只有不指定锁定时间时生效。

3. 基于关系型数据库

3.1 悲观锁

1
select id from order where order_id = xxx for update

注意:基于 MySQL 行锁的方式会出现交叉死锁, 通过超时机制解决死锁问题,高并发会出现排队现象,存在性能缺陷

3.2 基于乐观锁方案

1
2
select amount, old_ver from order where order_id = xxx
update order set ver = old_ver + 1, amount = yyy where order_id = xxx and ver = old_ver