Redisson 介绍

对于分布式或者多节点应用,一个分布式锁对于多并发场景显得尤为重要。一般分布式锁要支持和满足以下特性:

1、互斥:同一时刻只能有一个线程获得锁。

2、防止死锁:分布式锁非常有必要设置锁的有效时间,确保系统出现故障后,在一定时间内能够主动去释放锁,避免造成死锁的情况。

3、性能:需要考虑减少锁等待的时间,避免导致大量线程阻塞。在锁的设计时,需要考虑两点。1、锁的颗粒度要尽量小 2、锁的范围尽量要小

4、重入:同一个线程可以重复拿到同一个资源的锁。重入锁非常有利于资源的高效利用

Redisson 作为 java 的 Redis 客户端之一,是 Redis 官网推荐的 java 语言实现分布式锁的项目。 Redisson 就是提供了一堆锁... 也是目前大部分公司使用 Redis 分布式锁最常用的一种方式。

可重入锁(Reentrant Lock)/公平锁(Fair Lock)/联锁(MultiLock)/红锁(RedLock)/读写锁(ReadWriteLock)/信号量(Semaphore) 等等

Redis锁-Redisson_redis

项目结构

Redisson有两个主要的接口类,RedissonClient和RLock。其中RedissonClient定义了客户端的相关方法,如创建锁。RLock则定义了锁的具体操作,若加锁,释放锁等。其中Redisson是RedissonClient的实现类,RedissonLock是RLock的实现类。当然这里只展示了几个重要的的接口和实现类。

RLock

RLock则定义了锁的具体操作,若加锁,释放锁等,详情如下

public interface RLock extends Lock, RExpirable, RLockAsync {

    /**
     * 中断锁 和上面中断锁差不多,只是这里如果获得锁成功,添加锁的有效时间
     * @param leaseTime  锁有效时间
     * @param unit       时间单位 小时、分、秒、毫秒等
    */
  void lockInterruptibly(long var1, TimeUnit var3) throws InterruptedException;

    /**
     * 这里比上面多一个参数,多添加一个锁的有效时间
     * 尝试使用定义的等待时间获取锁。如有必要会等待定义的最长等待时间,直到锁可用。
     * 锁定将在定义的等待时间间隔后自动释放。会出 InterruptedException 异常
     * @param waitTime  等待时间
     * @param leaseTime 锁有效时间
     * @param unit      时间单位 小时、分、秒、毫秒等
     */
    boolean tryLock(long var1, long var3, TimeUnit var5) throws InterruptedException;
    
    /**
     * 加锁 上面是默认30秒这里可以手动设置锁的有效时间
     *
     * @param leaseTime 锁有效时间
     * @param unit      时间单位 小时、分、秒、毫秒等
     */
    void lock(long var1, TimeUnit var3);

     /**
     * 检验该锁是否被线程使用,如果被使用返回True
     */
    boolean isLocked();

    /**
     * 检查当前线程是否获得此锁(这个和上面的区别就是该方法可以判断是否当前线程获得此锁,而不是此锁是否被线程占有)
     * 这个比上面那个实用
     */
    boolean isHeldByCurrentThread();

    /**
    * 如果锁存在并且现在已解锁,则返回 true,否则返回 false
    */
    boolean forceUnlock();
  
    /**
    * 获取当前线程持有此锁的次数,如果当前线程未持有此锁,则为 0
    */
    int getHoldCount();
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.

RedissonClient

RedissonClient定义了客户端的相关方法,如创建锁

public class RedissonLock extends RedissonExpirable implements RLock {

    public RLock getLock(String name) {
        return new RedissonLock(this.connectionManager.getCommandExecutor(), name);
    }

    public RLock getFairLock(String name) {
        return new RedissonFairLock(this.connectionManager.getCommandExecutor(), name);
    }

    public RReadWriteLock getReadWriteLock(String name) {
        return new RedissonReadWriteLock(this.connectionManager.getCommandExecutor(), name);
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

加锁过程

加锁和解锁过程都是RedissonLock为例。

lock方法调用了lockInterruptibly方法,该方法翻译过来为可中断锁,若没有获取锁,则会进行阻塞等待锁。

Redis锁-Redisson_有效时间_02

该方法最重要的一步是调用tryAcquire来获取ttl(Time To Live,存活时间)。若为null,说明加锁成功。若ttl不为null,说明加锁失败,则会阻塞进行等到锁。具体有以下几步操作,

首先使用subscribe()给当前加锁失败的线程去订阅一个channel(当该线程调用unlock()或者interrupt()时,redis会发送事件通知,让该线程不再阻塞等待锁)。

然后进行下面的while循环,尝试加锁刷新这个ttl的时间, 分析ttl >= 0的逻辑,等待ttl秒获取许可, 假设现在ttl是10秒,这个方法就会阻塞在这里等待10s之后去循环这个while继续尝试加锁。

public void lock(long leaseTime, TimeUnit unit) {
    try {
        this.lockInterruptibly(leaseTime, unit);
    } catch (InterruptedException var5) {
        Thread.currentThread().interrupt();
    }
}

public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
    long threadId = Thread.currentThread().getId();
    Long ttl = this.tryAcquire(leaseTime, unit, threadId);
    if (ttl != null) {
        RFuture<RedissonLockEntry> future = this.subscribe(threadId);
        this.commandExecutor.syncSubscription(future);
        try {
            while(true) {
                ttl = this.tryAcquire(leaseTime, unit, threadId);
                if (ttl == null) {
                    return;
                }
                if (ttl >= 0L) {
                    // .getLatch()是一个Semaphore 信号量 
                    // 这个方法就会阻塞在这里等待ttl之后去循环这个while继续尝试加锁
                    this.getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                } else {
                    this.getEntry(threadId).getLatch().acquire();
                }
            }
        } finally {
            this.unsubscribe(future, threadId);
        }
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.

lock方法方法中最重要的还是调用了tryAcquire()方法来获取ttl。其中tryAcquire()分两步。调用tryAcquireAsync()方法,由于 leaseTime == -1,于是又调用 tryLockInnerAsync()方法。若leaseTime==-1,则加锁时间默认为是30s。

private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {
      return (Long)this.get(this.tryAcquireAsync(leaseTime, unit, threadId));
}

 private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
     if (leaseTime != -1L) {
         return this.tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
     } else {
         RFuture<Long> ttlRemainingFuture = this.tryLockInnerAsync(this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
         ttlRemainingFuture.addListener(new FutureListener<Long>() {
             public void operationComplete(Future<Long> future) throws Exception {
                 if (future.isSuccess()) {
                     Long ttlRemaining = (Long)future.getNow();
                     if (ttlRemaining == null) {
                         RedissonLock.this.scheduleExpirationRenewal(threadId);
                     }
                 }
             }
         });
         return ttlRemainingFuture;
     }
 }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.

真正加锁的操作还是用的lua语法实现的,如下所示。其

  1. 根据key查询是否存在,若不存在。则是在一个 getLockName(threadId),值为1的键值对,并设置过期时间,返回null。
  2. 如果key查询存在,则根据getLockName(threadId)判断是否存在,若存在,则在其值上+1,并重新设置过期时间,返回null。(可重入锁)
  3. 否则(即key存在,且被其他线程占有),则返回ttl。
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
    this.internalLockLeaseTime = unit.toMillis(leaseTime);
    return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, command, "
                                               if (redis.call('exists', KEYS[1]) == 0) then 
                                                 redis.call('hset', KEYS[1], ARGV[2], 1); 
                                                 redis.call('pexpire', KEYS[1], ARGV[1]); 
                                                 return nil; 
                                                end; 
                                                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]);", 
        Collections.singletonList(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)});
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.

KEYS[1] 为 getName(),ARGV[2] 为 getLockName(threadId)。ARGV[1] internalLockLeaseTime,锁的有效时间。

释放锁过程

public void unlock() {
    this.get(this.unlockAsync(Thread.currentThread().getId()));        
}

public RFuture<Void> unlockAsync(final long threadId) {
    final RPromise<Void> result = new RedissonPromise();
    RFuture<Boolean> future = this.unlockInnerAsync(threadId);
    future.addListener(new FutureListener<Boolean>() {
        public void operationComplete(Future<Boolean> future) throws Exception {
            if (!future.isSuccess()) {
                RedissonLock.this.cancelExpirationRenewal(threadId);
                result.tryFailure(future.cause());
            } else {
                Boolean opStatus = (Boolean)future.getNow();
                if (opStatus == null) {
                    IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: " + RedissonLock.this.id + " thread-id: " + threadId);
                    result.tryFailure(cause);
                } else {
                    if (opStatus) {
                        RedissonLock.this.cancelExpirationRenewal((Long)null);
                    }                   
                  result.trySuccess((Object)null);
                }
            }
        }
    });
    return result;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.

解锁过程最终还是执行的以下redis命令,其中涉及到了四个参数,分别为:

KEYS[1]:key

KEYS[2]:ChannelName,加锁的时候会生成一个channel

ARGV[1]:LockPubSub.unlockMessage,存的是0

ARGV[2]:生存时间

ARGV[3]:getLockName(threadId),这个线程对应的锁名称

释放锁的逻辑为:

Redis锁-Redisson_加锁_03

广播0表示资源可用,即通知那些等待获取锁的线程现在可以获得锁了。

protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, "
          if (redis.call('exists', KEYS[1]) == 0) then 
            redis.call('publish', KEYS[2], ARGV[1]); 
            return 1;
          end;
          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;", 
Arrays.asList(this.getName(), this.getChannelName()), new Object[]{LockPubSub.unlockMessage, this.internalLockLeaseTime, this.getLockName(threadId)});
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.