Redis/Zookeeper分布式锁

102 阅读4分钟

1. SETNX方案

//加锁
SET key uuid EX 10 NX

//释放锁,先判断是否自己线程,再释放
if (redis.get("lock") == $uuid){
    redis.del("lock")
}

好处:

  1. 原子操作,加锁安全
  2. 有过期时间,不会造成死锁
  3. 释放锁时校验线程id,不会释放其他线程的锁

缺点:

  1. 释放锁不是原子操作,有并发问题
  2. 锁过期时间难以估计

解决方案:

  1. 使用lua脚本保证原子操作
  2. 增加看门狗机制,加锁时,开启守护线程,定时检测锁失效时间,重新续期

2. Redisson方案

RLock lock =redissonClient.getLock("key");
try {
    boolean tryLock = lock.tryLock(tryTime, holdTime, TimeUnit.MILLISECONDS);
    if(!tryLock){
        //加锁失败
        log.info("加锁失败");
    }
    //执行业务逻辑
} catch (Exception e) {  
    e.printStackTrace();  
} finally {  
    //解锁
    if (lock != null && lock.isLocked()&&lock.isHeldByCurrentThread()) {  
        lock.unlock();  
    }  
}

优点:

  1. api方便
  2. 集成看门狗和原子操作,且有公平/非公平锁实现,

缺点:

  1. 主从集群下可能因为主从复制而丢失分布式锁

在主节点上加锁,加锁成功后,未来得及主从复制到从节点,主节点挂掉,从节点被选为新的主节点,锁就丢失啦

解决方案: 分布式redlock

3. RedLock方案

3.1 前提

  1. 不需要哨兵和从库,只部署主库
  2. 部署至少5个redis主库(非集群模式,5个redis示例就行)

ff77a56e5eef4b2cb446111e92c39ddb.png

3.2 获取锁流程

  1. 客户端先获取「当前时间戳T1」
  2. 客户端依次向这 5 个 Redis 实例发起加锁请求(用前面讲到的 SET 命令),且每个请求会设置超时时间(毫秒级,要远小于锁的有效时间),如果某一个实例加锁失败(包括网络超时、锁被其它人持有等各种异常情况),就立即向下一个 Redis 实例申请加锁
  3. 如果客户端从 >=3 个(大多数)以上 Redis 实例加锁成功,则再次获取「当前时间戳T2」,如果 T2 - T1 < 锁的过期时间,此时,认为客户端加锁成功,否则认为加锁失败
  4. 加锁成功,去操作共享资源(例如修改 MySQL 某一行,或发起一个 API 请求)
  5. 加锁失败,向「全部节点」发起释放锁请求(前面讲到的 Lua 脚本释放锁)

3.3 总结

  1. 客户端在多个 Redis 实例上申请加锁
  2. 必须保证大多数节点加锁成功(容错,大部分成功就行)
  3. 大多数节点加锁的总耗时,要小于锁设置的过期时间(保证加锁有意义)
  4. 释放锁,要向全部节点发起释放锁请求(部分加锁请求可能是网络原因失败,实际redis已经加锁成功,这部分也需要释放掉)
分布式系统存在三座大山
-   N:Network Delay,网络延迟
-   P:Process Pause,进程暂停(GC等)
-   C:Clock Drift,时钟漂移

3.4 redlock的问题

  1. P问题导致锁超时问题,如果客户端1获取锁后遇到GC导致长时间未处理结束业务,而分布式锁超时释放了,客户端2获取锁之后,客户端1和2就会并发写数据,形成竞态条件

134332ad031a8f952197590b95978b7b.png

  1. C问题导致锁超时问题,如果客户端1获取ABC节点的锁,客户端2获取DE的锁,此时C节点发生时钟漂移,释放锁,随后被客户端2获取,这样两个客户端均认为自己获取到了锁,形成竞态条件

3.5 解决方案

引入fencing Token机制,共享资源服务对这个token进行互斥校验

  1. 客户端在获取锁时,锁服务可以提供一个「递增」的 token
  2. 客户端拿着这个 token 去操作共享资源
  3. 共享资源可以根据 token 拒绝「后来者」的请求

2afb6ab94707e9d5733aefd7b2fc2a47.png

3.6 对fencing token方案的质疑

  1. 如果NPC问题在获取锁之前发生,则再加锁流程第三步获取T2-T1时间差与锁过期时间对比,便可以解决.
  2. 如果NPC问题在获取锁之后发生,则无法避免这个问题, 因为现有的分布式锁都无法避免此问题
  3. 大部分公共资源服务(mysql可以,读写文件/http请求等服务就不行)没有互斥能力,如果有互斥能力,就不需要分布式锁啦

4. Zookeper分布式锁

Zookeeper分布式锁是基于临时节点,即与ZK的连接

4.1 加锁流程

  1. 客户端 1 和 2 都尝试创建「临时节点」,例如 /lock
  2. 假设客户端 1 先到达,则加锁成功,客户端 2 加锁失败
  3. 客户端 1 操作共享资源
  4. 客户端 1 删除 /lock 节点,释放锁

4.2 特点

  1. 持锁线程发生异常崩溃后,与ZK连接断开就会释放锁,不需要考虑锁释放时间
  2. watch 机制,加锁失败,可以 watch 等待锁释放,实现乐观锁
  3. 性能不如redis
  4. 部署和运维成本比较高
  5. 客户端长时间不像ZK发心跳,会释放锁