Skip to content

Redlock介绍

haiger edited this page Jan 31, 2018 · 1 revision

使用场景

  • 资源互斥
  • Leader选举

目标

  • 安全型:互斥;同时一时刻,只能有一个client能获取到lock。
  • 灵活性A:无死锁;不能出现死锁的情况。
  • 灵活性B:故障容忍性;只要绝大数redis节点是running的,锁服务就能正常运行。

M-S模式的不安全

当我们使用单个redis节点来实现分布式锁服务时,如果redis服务宕机,那么整个锁服务就失效,会对业务造成影响。

当然,我们可以使用sentinel模式来保证redis的可用性。但是这里任然会有一些问题:

  1. client-A 在master上获取了一个lock-A。
  2. master宕机了,并且client-A获取的这个锁的key还没来得及同步到slave。
  3. slave晋升为master。
  4. client-B 在新master上再次获取lock-A。

这样一来,就违反了锁的互斥性。

Redlock算法

由于单机/M-S模式的缺陷,现在我们使用N个相互独立、没有相互replication的redis master实例组成一个集群。N为大于等于3的奇数。那么锁的实现步骤为:

  1. 记录当前的时间。
  2. 使用相同的LockName/LockValue和ttl同时向N个redis实例发送SET lockName lockValue NX EX 15,同时记录命令执行成功次数count,这里需要注意:redis client需要设置SoTimeout
  3. 计算第2步消耗的总时间elapsed-time,有且只有count>=N/2+1,并且elapsed-time<=ttl*1/3则表示获取锁成功; ttl*1/3:留2/3的时间做为时钟漂移以及锁持有时间
  4. 如果获取锁失败,同时count>0,则需要执行释放锁操作,因为有部分redis实例已经执行成功。
  5. 锁释放逻辑有两种:忘记主动释放,让lockName自动过期;主动释放,为了保证释放的lockName是自己的,在del的时候需要判断lockValue,这就要求lockValue短暂的唯一性。为了保证该操作原子性,需要是用redis-lua来实现。
if (redis.call('get', KEYS[1]) == ARGV[1]) then
    return redis.call('del', KEYS[1])
else
    return 0
end;

伪代码

redlock

class Redlock() {
    array redisNodes = [127.0.0.1:6379,127.0.0.2:6379,127.0.0.3:6378];
    int defaultTTL = 15;// seconds
    String unlockScript = "
        if (redis.call('get', KEYS[1]) == ARGV[1]) then
            return redis.call('del', KEYS[1])
        else
            return 0
        end;
    ";
    
    List<RedisClient> redisList;
    int nodeSize;
    
    void init() {
        for (nodes : redisNodes) {
             redisList.add(new RedisClient(nodes));
        }
        nodeSize = redisList.size();
    }
    
    String uniqueId(){
        return ip + randomString;
    }
    
    boolean lock(lockName, lockValue, leaseTime) {
        while(true) {
            boolean locked = tryLock(lockName, lockValue, leaseTime);
            if (locked) {
                return true;
            }
            
            sleep(100);
        }
    }
    
    boolean tryLock(lockName, lockValue, leaseTime) {
        long start = System.currentTimeMills();
        int count = 0;
        
        for (redisClient : redisList) {
            String res = redisClient.set(lockName, lockValue, "NX", "EX", leaseTime);
            if (res == "OK") {
                count++;
            }
        }
        long elapsed = System.currentTimeMills - start;
        if (count >= nodeSize/2 +1 && elapsed/1000 <= leaseTime/3) {
            return true;
        }
        
        if (count > 0) {
            unlock(lockName, lockValue);
        }
   
        return false;
    }
    
    void unlock(lockName, lockValue) {
        for (redisClient : redisList) {
            redisClient.eval(unlockScript, [lockNane], [lockValue]);
        }
    }
}

参考资料

  1. redis官方文档:distributed locks with redis
  2. 关于redis分布式锁的讨论
  3. redlock python实现
Clone this wiki locally