水煮Redisson(十)-非公平锁

379 阅读5分钟

前言

上一章介绍了分布式锁的基本概念和一些实现方式,其中就有Redis的实现方案。Redisson是Redis官方推荐的Java客户端,Redisson有几种不同锁的实现,包括公平锁,非公平锁,联合锁【MultiLock】,红锁【RedLock】,读写锁【ReadWriteLock】,后续几章我们将一一介绍。


介绍

日常使用的时候,我们通过Redisson获取一个分布式锁,默认是非公平锁。也就是【RedissonLock】,先看看类说明:

/**
 * Distributed implementation of {@link java.util.concurrent.locks.Lock}
 * Implements reentrant lock.<br>
 * Lock will be removed automatically if client disconnects.
 * <p>
 * Implements a <b>non-fair</b> locking so doesn't guarantees an acquire order.
 *
 * @author Nikita Koksharov
 *
 */

可以看出RedissonLock是非公平可重入锁的一个实现,当客户端失联以后,会自动释放锁。注意两个关键词,可重入非公平,在后续的源码分析中,会对其进行解释。

获取锁的步骤一般分为几个步骤:1.尝试获取锁;2.判断是否获取成功,并执行业务代码;3.释放锁。如下所示:

RLock testLock = redissonClient.getLock("TEST_LOCK");
try{
	// 尝试获取锁
	boolean lock = testLock.tryLock(10, 100, TimeUnit.SECONDS);
	if (lock){
		// 执行业务代码
	}
}finally {
	if (testLock.isLocked()){
		log.info("wait lock :{}",testLock.getHoldCount());
		// 释放锁
		testLock.unlock();
	}
}

获取锁

什么是可重入?

前文提到,RedissonLock是可重入的。那么什么是可重入?是说对于同一个资源,在客户端线程A拿到锁以后,在释放锁之前,可以多次成功获取锁;不需要等待当前锁释放,也不与其他线程竞争。从源码中可以看出参数中带上了当前线程ID,就是可重入的一个重要标识。

	/**
     * Tries to acquire the lock by thread with specified <code>threadId</code> and  <code>leaseTime</code>.
     * Waits up to defined <code>waitTime</code> if necessary until the lock became available.
     *
     * Lock will be released automatically after defined <code>leaseTime</code> interval.
     * 
     * @param threadId id of thread
     * @param waitTime time interval to acquire lock
     * @param leaseTime time interval after which lock will be released automatically 
     * @param unit the time unit of the {@code waitTime} and {@code leaseTime} arguments
     * @return <code>true</code> if lock acquired otherwise <code>false</code>
     */
    @Override
    public RFuture<Boolean> tryLockAsync(long waitTime, long leaseTime, TimeUnit unit) {
		// 当前线程的id
        long currentThreadId = Thread.currentThread().getId();
        return tryLockAsync(waitTime, leaseTime, unit, currentThreadId);
    }

参数解释

  • waitTime:等待获取锁的时间
  • leaseTime:自动释放锁的时间,如果不设置此参数,默认是Config.lockWatchdogTimeout = 30 * 1000,30秒。
    这里需要注意EVAL_NULL_BOOLEAN和EVAL_LONG两种语义,

EVAL_NULL_BOOLEAN

底层实现是RFuture tryAcquireOnceAsync(...),返回boolea值,只执行一次,等待过程中,不进行重试获取锁。

EVAL_LONG

底层实现是RFuture tryAcquireAsync(...),返回具体等待时间,在等待过程中,一直等待其他客户端释放锁的信号,一旦有此信号发出,立即执行重新获取锁动作。

下面是获取锁的具体方法,依然是用LUA脚本实现。

    <T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        internalLockLeaseTime = unit.toMillis(leaseTime);

        return evalWriteAsync(getName(), LongCodec.INSTANCE, command,
                "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; " +
                        "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(getName()), internalLockLeaseTime, getLockName(threadId));
    }

首先看看其中的几个指令,hincrby,hexists,pexpire。其中hincrby是给hash表的字段加上指定的增量值,hexists是判断hash表中是否存在指定的字段,可以看出Redisson将锁的持有状态记录在hash表中,每重入一次加一。
执行过程:

  1. 判断资源是否被锁定,redis.call('exists', KEYS[1]),如果返回1,则表示资源已经被其他线程持有,继续执行后续逻辑;如果没有被锁定,则更新锁的持有者,并设定过期时间;
  2. 判断资源是否被当前线程锁定,如果是,则累加持有次数,并重置过期时间;
  3. 如果被持有的不是当前线程,则返回当前资源锁的过期时间,return redis.call('pttl', KEYS[1]);
  4. 返回的是RFuture,当lua脚本return nil时,返回的就是0或者true。当return pttl时间时,返回的就是具体超时时间数值或者false。
    注意第一行的变量【internalLockLeaseTime】,在后续锁释放时还会用到。

什么是非公平?

从获取锁的过程中,哪里体现了非公平呢?非公平锁,不能保证请求的顺序,就是说A和B先后请求同一个key的锁,A可能在B之后拿到锁,因为线程在等待拿锁的过程中,没有一个队列来维护其先后顺序,而是由各个线程进行竞争,先到先得。


 @Override
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();
    RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
	// 等待限定的时间,如果超时之前收到锁释放的通知,则继续竞争,否则返回失败
	if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {
    	//... to do sth.
    	acquireFailed(threadId);
    	return false;
	}
    // ...
    try {
        time -= System.currentTimeMillis() - current;
        if (time <= 0) {
            acquireFailed(threadId);
            return false;
        }
    	// 循环竞争锁
        while (true) {
            long currentTime = System.currentTimeMillis();
            ttl = tryAcquire(leaseTime, unit, threadId);
            // lock acquired
            if (ttl == null) {
                return true;
            }

            // 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);
            }

            time -= System.currentTimeMillis() - currentTime;
            if (time <= 0) {
                acquireFailed(threadId);
                return false;
            }
        }
    // ... 
}

释放锁

一般在业务代码中,获取锁和释放锁需要成对出现,否则会出现问题。

手动释放

业务逻辑执行完成以后,在finally代码块中,执行锁释放操作

    protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                "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(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
    }

释放逻辑:

  1. 判断当前资源是否有锁,如果没锁【可能已经自动释放】直接返回成功;
  2. 计算当前线程持有锁次数减一之后的值,如果持有次数任然大于零,则重置超时时间;超时时间是internalLockLeaseTime,在获取锁时设置的参数,如果没有设置,则默认为lockWatchdogTimeout = 30 * 1000;
  3. 如果锁持有次数小于等于0,则删除资源锁,并发布锁释放指令,通知其他线程进行锁竞争。

强行释放

在多次重入之后,如果lock/unlock在代码中不是成对出现,那么会导致手动释放不能成功,这时就需要强制释放了,无论该线程重入了多少次。

 	@Override
    public RFuture<Boolean> forceUnlockAsync() {
        cancelExpirationRenewal(null);
        return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                "if (redis.call('del', KEYS[1]) == 1) then "
                        + "redis.call('publish', KEYS[2], ARGV[1]); "
                        + "return 1 "
                        + "else "
                        + "return 0 "
                        + "end",
                Arrays.asList(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE);
    }

源码中直接删除资源锁,如果删除成功,则通知其他线程进行锁竞争,否则直接返回。

自动释放

自动释放依赖redis的key过期机制,前面在获取锁时,设定了过期时间,一旦时间过期,且资源锁还未释放的情况下,由redis进行清除。