分布式锁

分布式场景下,保证同一时刻对共享数据只能被一个应用的一个线程操作。用来保证共享数据的安全性和一致性。

常见实现方式:

  • 基于数据库实现
  • 基于ZooKeeper实现
  • 基于Redis实现

分布式锁特性

  • 排他性:同一时刻只能有一个应用的一个线程可以执行加锁的方法,获取到锁。
  • 高性能:分布式锁可能会有很多的服务器来获取,高性能的获取和释放,不能因为某一个分布式锁获取的服务不可用,导致所有服务都拿不到或释放锁,所以要满足高可用要求。
  • 锁失效机制,防止死锁:假设某个应用获取到锁之后,一直没有来释放锁,可能服务本身已经挂掉了,不能一直不释放,导致其他服务一直获取不到锁。
  • 可重入性:一个应用如果成功获取到锁之后,再次获取锁也可以成功
  • 非堵塞:在某个服务来获取锁时,假设该锁已经被另一个服务获取,我们要能直接返回失败,不能一直等待。

基于数据库

  • 利用数据库的唯一约束 UNIQUE KEY
  • 排他锁 for update
  1. 排他性:具备

  2. 高可用:多机部署,主从同步、主备切换等。

  3. 高性能:没有 nosql 数据库并发性能高。

  4. 失效机制:单独增加定时任务,按照记录的更新时间定时清除或者增加超时时间字段,sql 中进行优化

  5. 可重入性:通过增加字段,记录占有锁的应用节点信息和线程信息,再次获取锁时判断是否是当前线程获取的锁达到可重入的特性。

  6. 非阻塞特性:具备,在获取锁失败时,会直接返回失败。但是无法满足超时获取的场景,比如5秒内获取不到锁再失败等。

基于ZooKeeper

临时 znode

  1. 节点尝试创建临时 znode,此时创建成功了就获取了这个锁
  2. 其他客户端来创建锁会失败,只能注册个监听器监听这个锁
  3. 释放锁就是删除这个 znode,一旦释放掉就会通知客户端
  4. 等待着的客户端就可以再次重新加锁。

羊群效应:如果有1000个客户端发起请求并创建临时节点,都会去监听A结点的变化,然后A删除节点的时候会通知其他节点,耗费资源了

临时顺序节点

临时节点和顺序节点的结合体,每个节点创建时会指定顺序编号,并且在客户端与ZK服务端断开时,节点会被删除。

  1. 创建一个持久节点表示一个分布式锁节点
  2. 所有客户端都尝试去创建临时有序节点以获取锁
  3. 序号最小的临时有序节点获得锁
  4. 未获取到锁的客户端给自己的上一个临时有序节点添加监听
  5. 获得锁的客户端进行自己的操作,操作完成之后删除自己的临时有序节点
  6. 当监听到自己的上一个临时有序节点释放了锁,尝试自己去加锁
  7. 操作完成之后释放锁
  8. 之后剩下的客户端重复加锁和解锁的操作

JVM 调优,尽量避免长时间 GC 的情况发生:

由于ZK依靠session定期的心跳来维持客户端,如果客户端进入长时间的GC,可能会导致ZK认为客户端宕机而释放锁,让其他的客户端获取锁,但是客户端在GC回复后,会认为自己还持有锁。

基于Redis

基于 lua 脚本

采用Lua脚本,来保证SETNX+EXPIRE+随机value操作的原子性。

public boolean getLock(String key,String uniId,Long expireTime){
    //加锁
    return jedis.set(key, uniId, "NX", "EX", expireTime) == 1;
}

// 解锁
public boolean releaseLock(String key,String uniId){
    // 因为get和del操作并不是原子的,所以使用lua脚本
    String lua_script = "if redis.call('get',KEYS[1]) == ARGV[1] then  return redis.call('del',KEYS[1]) 
        +"else return 0  end;";
    Object result = jedis.eval(lua_scripts,Collections.singletonList(key),Collections.singletonList(uniId));
    return result.equals(1L);
}

随机值防止锁误删,一些情况下,执行时间超过了锁过期时间,此时其他线程也获取到了锁,如果直接删除,导致更多的客户端能够获取到锁。

RedLock 算法

RedLock的核心原理:

  • 在Redis集群中选出多个Master节点,保证这些Master节点不会同时宕机;
  • 并且各个Master节点之间相互独立,数据不同步;
  • 使用与Redis单实例相同的方法来加锁和解锁。

那么RedLock到底是如何来保证在有节点宕机的情况下,还能安全的呢?

  1. 假设集群中有N台Master节点,首先,获取当前时间戳;
  2. 客户端按照顺序使用相同的key,value依次获取锁,并且获取时间要比锁超时时间足够小;比如超时时间5s,那么获取锁时间最多1s,超过1s则放弃,继续获取下一个;
  3. 客户端通过获取所有能获取的锁之后减去第一步的时间戳,这个时间差要小于锁超时时间,并且要至少有N/2 + 1台节点获取成功,才表示锁获取成功,否则算获取失败;
  4. 如果成功获取锁,则锁的有效时间是原本超时时间减去第三不得时间差;
  5. 如果获取锁失败,则要解锁所有的节点,不管该节点加锁时是否成功,防止有漏网之鱼。

方案总结

  • 分布式锁,数据库本身就不适合分布式锁,并发不高的情况下可以尝试
  • redis 分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能。
  • zk 分布式锁,获取不到锁,注册个监听器即可,不需要不断主动尝试获取锁,性能开销较小。
  • Redis 获取锁的那个客户端 出现 bug 挂了,那么只能等待超时时间之后才能释放锁;而 zk 的话,因为创建的是临时 znode,只要客户端挂了,znode 就没了,此时就自动释放锁。
Last Updated:
Contributors: 拔土豆的程序员