Loading

Redis 高级篇 Part 4

😉 本文共21字,阅读时间约8min

分布式锁-redission

功能介绍

基于setnx实现的分布式锁存在下面的问题:

重入问题:重入问题是指 获得锁的线程可以再次进入到相同的锁的代码块中,可重入锁的意义在于防止死锁,比如HashTable这样的代码中,他的方法都是使用synchronized修饰的,假如他在一个方法内,调用另一个方法,那么此时如果是不可重入的,不就死锁了吗?所以可重入锁他的主要意义是防止死锁,我们的synchronized和Lock锁都是可重入的。

不可重试:是指目前的分布式只能尝试一次,我们认为合理的情况是:当线程在获得锁失败后,他应该能再次尝试获得锁。

超时释放:我们在加锁时增加了过期时间,这样的我们可以防止死锁,但是如果卡顿的时间超长,虽然我们采用了lua表达式防止删锁的时候,误删别人的锁,但是毕竟没有锁住,有安全隐患

主从一致性: 如果Redis提供了主从集群,当我们向集群写数据时,主机需要异步的将数据同步给从机,而万一在同步过去之前,主机宕机了,就会出现死锁问题。

1653546070602

希望的锁:可重入、可重试、自动续期、主从一致

Redisson相当于在Redis的基础上又进行了一层封装。

包含了各种分布式锁的实现,可重入锁,公平锁,联锁,红锁,读写锁,信号量

可重入锁原理

hash结构,大key为锁名,小key为持有线程名,value为重入次数。

获取锁和释放锁底层都是lua脚本。

  1. 获取锁:

    1. 锁不存在,第一次,加锁,设置过期时间

    2. 锁已存在,为当前线程,重入计数+1,重新设置过期时间

    3. 锁已存在,非当前线程,返回的是当前锁剩余有效期

      // 锁不存在,第一次,加锁,设置过期时间
      if (redis.call('exists', KEYS[1]) == 0) then
      	redis.call('hincrby', KEYS[1], ARGV[2], 1);
      	redis.call('pexpire', KEYS[1], ARGV[1]);
      	return nil;
      end;
      // 锁已存在,为当前线程,重入计数+1,重新设置过期时间
      if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then 
      	redis.call('hincrby', KEYS[1], ARGV[2], 1);
      	redis.call('pexpire', KEYS[1], ARGV[1]);
      	return nil;
      end;
      // 锁已存在,非当前线程,返回的是当前锁剩余有效期(锁重试会有用)
      return redis.call('pttl', KEYS[1]);
      
  2. 释放锁

    1. 当前锁已被别人持有,直接返回

    2. 当前锁还是自己的,重入计数-1。若重入计数为0,直接删除锁。

      if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then 
      	return nil;
      end;
      
      local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); 
      if (counter > 0) then 
      	redis.call('pexpire', KEYS[1], ARGV[2]); 
      	return 0; 
      else 
      	redis.call('del', KEYS[1]); 
          // 释放锁发布信号量,锁重试会用
      	redis.call('publish', KEYS[2], ARGV[1]); 
      	return 1; 
      end; 
      return nil;
      

image-20230114204408895

锁重试

  1. tryAcquire()进行加锁,该加锁逻辑和之前是一样的
  2. 如果返回锁失效时间,而不是nil,代表加锁失败。
  3. 订阅持有锁进程释放锁的信号量,阻塞等待。
  4. 信号量发布,继续执行
  5. while(true)循环开始
    1. 尝试获取锁
    2. 如果失败,继续等释放锁的信号量,阻塞等待
    3. 信号量发布继续执行循环

期间会不停判断是否超过最大等待时间!

信号量机制使得不会无休止尝试,每一次尝试时都有人释放了锁

public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
    long time = unit.toMillis(waitTime);
    long current = System.currentTimeMillis();
    long threadId = Thread.currentThread().getId();
    Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
    // lock acquired
    if (ttl == null) {
        return true;
    }
	// 未获取到锁,进入重试环节
    
    // 超过wait_time,直接失败
    time -= System.currentTimeMillis() - current;
    if (time <= 0) {
        acquireFailed(waitTime, unit, threadId);
        return false;
    }

    current = System.currentTimeMillis();
    // 订阅持有锁线程释放锁的信号量
    RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
    // 阻塞等待该信号量的发布
    if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {
        // 阻塞时间超过wait_time,直接失败
        if (!subscribeFuture.cancel(false)) {
            subscribeFuture.onComplete((res, e) -> {
                if (e == null) {
                    unsubscribe(subscribeFuture, threadId);
                }
            });
        }
        acquireFailed(waitTime, unit, threadId);
        return false;
    }

    try {
        // 超过wait_time,直接失败
        time -= System.currentTimeMillis() - current;
        if (time <= 0) {
            acquireFailed(waitTime, unit, threadId);
            return false;
        }

        while (true) {
            long currentTime = System.currentTimeMillis();
            // 尝试获取锁
            ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
            // lock acquired
            if (ttl == null) {
                return true;
            }
			// 获取锁失败
            
            // 超过wait_time,直接失败
            time -= System.currentTimeMillis() - currentTime;
            if (time <= 0) {
                acquireFailed(waitTime, unit, threadId);
                return false;
            }

            // 继续等释放锁的信号量,waiting for message
            currentTime = System.currentTimeMillis();
            if (ttl >= 0 && ttl < time) {
                subscribeFuture.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
            } else {
                subscribeFuture.getNow().getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
            }
			
            // 超过wait_time,直接失败
            time -= System.currentTimeMillis() - currentTime;
            if (time <= 0) {
                acquireFailed(waitTime, unit, threadId);
                return false;
            }
        }
    } finally {
        unsubscribe(subscribeFuture, threadId);
    }
}

自动续期:看门狗机制

  1. 传入释放时间,无看门狗机制,调用一次lua脚本设置过期时间
  2. 使用默认释放时间,看门狗逻辑自动续约,调用自动续约函数
    1. 10s后自动续约
    2. 续约完成后,递归调用自身(自动续约函数)
    3. 无限续约
// 被tryLock方法调用
private RFuture<Boolean> tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
     // 传入释放时间,无看门狗机制
     if (leaseTime != -1) {
         return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
     }
     
     // 释放时间为默认看门狗时间,30s
     RFuture<Boolean> ttlRemainingFuture = tryLockInnerAsync(waitTime,      commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
     
     // 异步任务完成后,如果获取到锁,自动更新续期
     ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
         if (e != null) {
             return;
         }

         // lock acquired
         if (ttlRemaining) {
             scheduleExpirationRenewal(threadId);
         }
     });
     return ttlRemainingFuture;
 }
private void renewExpiration() {
    Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
        @Override
        public void run(Timeout timeout) throws Exception {
           	// renewExpirationAsync,重置锁有效期
            RFuture<Boolean> future = renewExpirationAsync(threadId);
            future.onComplete((res, e) -> {
                if (e != null) {
                    log.error("Can't update lock " + getName() + " expiration", e);
                    return;
                }
                
                // 递归调用,效果是下一个10s执行
                if (res) {
                    // reschedule itself
                    renewExpiration();
                }
            });
        }
        // 10s后执行一次task
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
    
    ee.setTimeout(task);
}
  1. 锁释放时,会根据一个map找到该线程持有的看门狗线程,并将其取消掉

假设我们的线程出现了宕机他还会续约吗?当然不会,因为没有人再去调用renewExpiration这个方法,所以等到时间之后自然就释放了。

总结

Redisson分布式锁原理:

可重入:利用hash结构记录线程id和重入次数

可重试:利用信号量和PubSub功能实现等待、唤醒,获取锁失败的重试机制

超时续约:利用watchDog,每隔一段时间(releaseTime / 3),重置超时时间

image-20221210115241632

主从一致 MutiLock原理

主从同步问题

为了提高redis的可用性,我们会搭建集群或者主从,现在以主从为例

此时我们去写命令,写在主机上, 主机会将数据同步给从机,但是假设在主机还没有来得及把数据写入到从机去的时候,此时主机宕机,哨兵会发现主机宕机,并且选举一个slave变成master,而此时新的master中实际上并没有锁信息,此时锁信息就已经丢掉了。

1653553998403

联锁

为了解决这个问题,redission提出来了MutiLock锁,三个主节点都是独立节点,, 这把锁加锁的逻辑需要写入到每一个主节点上,只有所有的服务器都写入成功,此时才是加锁成功。

假设现在某个节点挂了,当前线程还有两个主节点有锁。但其他线程想去获得锁的时候,有一个节点拿不到,就无法加锁成功,就保证了加锁的可靠性。

1653554055048

那么MutiLock 加锁原理是什么呢?笔者画了一幅图来说明

当我们去设置了多个锁时,redission会将多个锁添加到一个集合中,然后用while循环去不停去尝试拿锁,但是会有一个总共的加锁时间,这个时间是用需要加锁的个数 * 1500ms ,假设有3个锁,那么时间就是4500ms,假设在这4500ms内,所有的锁都加锁成功, 那么此时才算是加锁成功,如果在4500ms有线程加锁失败,则会再次去进行重试.

1653553093967

为什么Redis这么快?

Redis 内部做了非常多的性能优化,比较重要的主要有下面 3 点:

  • Redis 基于内存,内存的访问速度是磁盘的上千倍;
  • Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型,主要是单线程事件循环和 IO 多路复用(Redis 线程模式后面会详细介绍到);
  • Redis 内置了多种优化过后的数据结构实现,性能非常高。
posted @ 2023-01-14 21:55  iterationjia  阅读(77)  评论(0编辑  收藏  举报