如何设计一个分布式锁?如何对锁性能进行优化?

如何设计一个分布式锁?如何对锁性能进行优化?

分布式锁的本质:就是在所有进程都能访问到的一个地方(如数据库、zookeeper、Redis),设置一个锁资源,让这些进程都来竞争锁资源。通常对于分布式锁,会要求响应快、性能高、与业务无关。

Redis实现分布式锁

  • SETNX key value :NX表示NOT EXIST,当key不存在时就将key设置为value,并返回1,如果key不存在则直接返回0。
  • EXPIRE key locktime :设置key的有效时长。
  • DEL key:删除key
  • GETSET key value: 先GET再SET,即先返回key对应的值(如果没有就返回空),然后将key设置成value。
  1. 最简单的分布式锁:SETNX加锁,DEL解锁。存在的问题:如果获取到锁的进程执行失败,他就永远不会主动解锁,那这个锁就被锁死了。
public boolean tryLock(RedisConnect conn){
  if(conn.SETNX("mykey", "1")==1){
     return true;
  }else{
     return false;
  }
}
  1. 解决方案:给锁设置过期时间。存在的问题:SETNX和EXPIRE并不是原子性的,所以获取到锁的进程有可能还没有执行EXPIRE指令,就挂了,这样锁还是会被锁死。
public boolean tryLock(RedisConnect conn){
  if(conn.SETNX("mykey", "1")==1){
     // 给锁设置过期时间
     conn.EXPIER("mykey", 1000);
     return true;
  }else{
     return false;
  }
}
  1. 解决方案:将锁的内容设置为过期时间(客户端时间+过期时长),SETNX获取锁失败时,拿过期时间跟当前时间比对,如果是过期锁,就先删除锁,再重新上锁。存在的问题:高并发场景下,会产生多个进程同时拿到锁的情况。
public boolean tryLock(RedisConnect conn){
  long nowTime = System.currentTimeMillis();
  long expireTime = nowTime+1000;
  if(conn.SETNX("mykey", "expireTime)==1){
     // 给锁设置过期时间
     // conn.EXPIER("mykey", 1000);
     return true;
  }else{
    // SETNX获取锁失败时,拿过期时间跟当前时间比对,如果是过期锁,就先删除锁,再重新上锁。
    long val = conn.get("mykey");
    if(val!=null&&val<nowTime){
      // 此处直接覆盖锁
       conn.SET("mykey", expireTime);
       return true;
        }
     return false;
  }
}
  1. 解决方案:SETNX失败后,获取锁上面的时间戳,然后用GETSET,将自己的过期时间更新上去,并获取旧值。如果这个旧值,跟之前获得的时间戳是不一样的,就表示这个锁已经被其他进程占用了,自己就要放弃竞争锁。
public boolean tryLock(RedisConnect conn){
  // 将锁的内容设置为过期时间(客户端时间+过期时长)
  long nowTime = System.currentTimeMillis();
  long expireTime = nowTime+1000;
  if(conn.SETNX("mykey", "expireTime)==1){
     conn.EXPIER("mykey", 1000);
     return true;
  }else{
    // SETNX失败后,获取锁上面的时间戳
    long oldVal = conn.get("mykey");
    if(oldVal!=null&&oldVal<nowTime){
        // 然后用GETSET,将自己的过期时间更新上去,并获取旧值
        long currentVal = conn.GETSET("mykey", expireTime);
        if(oldVal==currentVal){
            conn.EXPIRE("mykey", 1000);
            return true;
        }
        // 如果这个旧值,跟之前获得的时间戳是不一样的,就表示这个锁已经被其他进程占用了,自己就要放弃竞争锁。
        return false;
     }
     return false;
  }
}
  1. 上面各种优化的根本问题在于:SETNX和EXPIRE两个指令无法保证原子性。
    Redis2.6提供了直接执行lua脚本的方式,通过lua脚本来保证原子性。redission
原文地址:https://www.cnblogs.com/pangqianjin/p/14616932.html