简介
上一章介绍了非公平锁,这里再来介绍一下公平锁。在特定场合下,比如开放式的名额申请,如果某一个客户端直到等待超时也获取不到锁,一直不能拿到名额走下一步业务流程,会造成平台不公平的非议。对于类似的情况,需要用公平锁来保障用户的体验和权益。
首先还是来看看代码声明
/**
* 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>fair</b> locking so it guarantees an acquire order by threads.
*
* @author Nikita Koksharov
*
*/
public class RedissonFairLock extends RedissonLock implements RLock {...}
和非公平锁类似,也是可重入的,但是可以保证各客户端的请求顺序。
锁获取
怎么保证公平
在创建资源公平锁时,redisson同时维护了两个集合,一个客户端线程队列,名称为redisson_lock_queue:{lockname},一个客户端超时有序集合,名称为redisson_lock_timeout:{lockname},其中队列用来保证请求的顺序,一般是先进先出【FIFO】。
如何实现
实现的方法是tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand command),重写了RedissonLock的同名方法,主体也是lua脚本。在上一章非公平锁里讲解了EVAL_NULL_BOOLEAN和EVAL_LONG两种语义,公平锁里也对这两种语义进行区别实现。
EVAL_NULL_BOOLEAN
进入的契机:redissonClient.getFairLock("test").tryLock(),不填写等待时间,执行时获取默认的看门狗超时时间。
分为了三个独立的部分,一个while循环,两个if逻辑分支,下面来进行拆解逐个分析。
最终的效果:如果当前线程没有立即获取到锁,则立刻失败。
evalWriteAsync(getName(), LongCodec.INSTANCE, command,
// remove stale threads
"while true do " +
"local firstThreadId2 = redis.call('lindex', KEYS[2], 0);" +
"if firstThreadId2 == false then " +
"break;" +
"end;" +
"local timeout = tonumber(redis.call('zscore', KEYS[3], firstThreadId2));" +
"if timeout <= tonumber(ARGV[3]) then " +
// remove the item from the queue and timeout set
// NOTE we do not alter any other timeout
"redis.call('zrem', KEYS[3], firstThreadId2);" +
"redis.call('lpop', KEYS[2]);" +
"else " +
"break;" +
"end;" +
"end;" +
"if (redis.call('exists', KEYS[1]) == 0) " +
"and ((redis.call('exists', KEYS[2]) == 0) " +
"or (redis.call('lindex', KEYS[2], 0) == ARGV[2])) then " +
"redis.call('lpop', KEYS[2]);" +
"redis.call('zrem', KEYS[3], ARGV[2]);" +
// decrease timeouts for all waiting in the queue
"local keys = redis.call('zrange', KEYS[3], 0, -1);" +
"for i = 1, #keys, 1 do " +
"redis.call('zincrby', KEYS[3], -tonumber(ARGV[4]), keys[i]);" +
"end;" +
"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 1;",
Arrays.<Object>asList(getName(), threadsQueueName, timeoutSetName),
internalLockLeaseTime, getLockName(threadId), currentTime, threadWaitTime);
while循环
执行目标:删除已经超时的客户端线程。
- 判断锁等待队列中,首元素是否为空,如果没有客户端等待锁,直接跳出循环;
- 判断首元素是否已经等待超时,如果没有超时,执行后续IF分支;
- 如果超时,将这个客户端从等待队列和超时集合中删除,继续取下一个进行判断;
IF分支一
执行目标:当前申请锁的客户端线程获得锁。
前提条件:资源锁在redis中不存在,且等待队列不存在,或者等待队列第一个元素是当前申请锁的客户端线程;
- 将这个客户端从等待队列和超时集合中删除;
- 遍历超时集合中所有元素,将等待超时剩余时间减去当前客户端等待锁的时间;
- 将资源锁与当前客户端绑定,设定锁自动释放时间;
IF分支二
执行目标:当前申请锁的客户端线程重入锁。
前提条件:锁的持有者是当前申请锁的线程。
- 累加持有次数;
- 延迟持有过期时间;
EVAL_LONG
进入的契机:redissonClient.getFairLock("test").tryLock(2, TimeUnit.SECONDS);申请锁时,写明具体的等待时间。
分为了四个独立的部分,一个while循环,两个if逻辑分支,和等待处理环节。前面三个和EVAL_NULL_BOOLEAN里的一致,这里主要说明一下等待处理环节。
执行效果:
evalWriteAsync(getName(), LongCodec.INSTANCE, command,
// remove stale threads
"while true do " +
"local firstThreadId2 = redis.call('lindex', KEYS[2], 0);" +
"if firstThreadId2 == false then " +
"break;" +
"end;" +
"local timeout = tonumber(redis.call('zscore', KEYS[3], firstThreadId2));" +
"if timeout <= tonumber(ARGV[4]) then " +
// remove the item from the queue and timeout set
// NOTE we do not alter any other timeout
"redis.call('zrem', KEYS[3], firstThreadId2);" +
"redis.call('lpop', KEYS[2]);" +
"else " +
"break;" +
"end;" +
"end;" +
// check if the lock can be acquired now
"if (redis.call('exists', KEYS[1]) == 0) " +
"and ((redis.call('exists', KEYS[2]) == 0) " +
"or (redis.call('lindex', KEYS[2], 0) == ARGV[2])) then " +
// remove this thread from the queue and timeout set
"redis.call('lpop', KEYS[2]);" +
"redis.call('zrem', KEYS[3], ARGV[2]);" +
// decrease timeouts for all waiting in the queue
"local keys = redis.call('zrange', KEYS[3], 0, -1);" +
"for i = 1, #keys, 1 do " +
"redis.call('zincrby', KEYS[3], -tonumber(ARGV[3]), keys[i]);" +
"end;" +
// acquire the lock and set the TTL for the lease
"redis.call('hset', KEYS[1], ARGV[2], 1);" +
"redis.call('pexpire', KEYS[1], ARGV[1]);" +
"return nil;" +
"end;" +
// check if the lock is already held, and this is a re-entry
"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;" +
// the lock cannot be acquired
// check if the thread is already in the queue
"local timeout = redis.call('zscore', KEYS[3], ARGV[2]);" +
"if timeout ~= false then " +
// the real timeout is the timeout of the prior thread
// in the queue, but this is approximately correct, and
// avoids having to traverse the queue
"return timeout - tonumber(ARGV[3]) - tonumber(ARGV[4]);" +
"end;" +
// add the thread to the queue at the end, and set its timeout in the timeout set to the timeout of
// the prior thread in the queue (or the timeout of the lock if the queue is empty) plus the
// threadWaitTime
"local lastThreadId = redis.call('lindex', KEYS[2], -1);" +
"local ttl;" +
"if lastThreadId ~= false and lastThreadId ~= ARGV[2] then " +
"ttl = tonumber(redis.call('zscore', KEYS[3], lastThreadId)) - tonumber(ARGV[4]);" +
"else " +
"ttl = redis.call('pttl', KEYS[1]);" +
"end;" +
"local timeout = ttl + tonumber(ARGV[3]) + tonumber(ARGV[4]);" +
"if redis.call('zadd', KEYS[3], timeout, ARGV[2]) == 1 then " +
"redis.call('rpush', KEYS[2], ARGV[2]);" +
"end;" +
"return ttl;",
Arrays.<Object>asList(getName(), threadsQueueName, timeoutSetName),
internalLockLeaseTime, getLockName(threadId), threadWaitTime, currentTime);
等待处理
执行目标:当前申请锁的客户端线程没有获取到锁,如果在队列中,返回超时时间;如果不在队列中,将其添加到队列末尾,并返回超时时间;
- 获取当前线程的超时时间,如果不为空,则返回超时时间-当前线程等待时间-当前时间;
- 设定ttl变量,获取队列最后一个客户端线程,如果此客户端不为当前线程,则ttl = 此线程的超时时间 - 当前时间;
- 否则ttl = 当前资源锁的过期时间;
- 设定一个超时时间变量,等于ttl + 当前线程等待时间 + 当前时间;
- 将当前线程添加到有序集合中,然后将当前线程放到等待队列末尾;
- 返回ttl变量【超时时间】;
获取失败注销
@Override
protected RFuture<Void> acquireFailedAsync(long threadId) {
return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_VOID,
// get the existing timeout for the thread to remove
"local queue = redis.call('lrange', KEYS[1], 0, -1);" +
// find the location in the queue where the thread is
"local i = 1;" +
"while i <= #queue and queue[i] ~= ARGV[1] do " +
"i = i + 1;" +
"end;" +
// go to the next index which will exist after the current thread is removed
"i = i + 1;" +
// decrement the timeout for the rest of the queue after the thread being removed
"while i <= #queue do " +
"redis.call('zincrby', KEYS[2], -tonumber(ARGV[2]), queue[i]);" +
"i = i + 1;" +
"end;" +
// remove the thread from the queue and timeouts set
"redis.call('zrem', KEYS[2], ARGV[1]);" +
"redis.call('lrem', KEYS[1], 0, ARGV[1]);",
Arrays.<Object>asList(threadsQueueName, timeoutSetName),
getLockName(threadId), threadWaitTime);
}
执行目标:调整在这个客户端之后的其他客户端的剩余等待时间,并将此客户端从队列中移除;
执行条件:等待锁超时
执行步骤:
- 提取在队列中等待的所有客户端线程,找到当前客户端的位置;
- 调整队列中,在当前客户端线程之后其他客户端的等待时间,等待时间 = 剩余等待时间 - 当前客户端线程的等待时间;
- 把当前客户端线程从队列和超时集合中移除;
锁释放
强制释放
强行释放是不带参数的,不指定客户端线程。
@Override
public RFuture<Boolean> forceUnlockAsync() {
cancelExpirationRenewal(null);
return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
// remove stale threads
"while true do "
+ "local firstThreadId2 = redis.call('lindex', KEYS[2], 0);"
+ "if firstThreadId2 == false then "
+ "break;"
+ "end; "
+ "local timeout = tonumber(redis.call('zscore', KEYS[3], firstThreadId2));"
+ "if timeout <= tonumber(ARGV[2]) then "
+ "redis.call('zrem', KEYS[3], firstThreadId2); "
+ "redis.call('lpop', KEYS[2]); "
+ "else "
+ "break;"
+ "end; "
+ "end;"
+
"if (redis.call('del', KEYS[1]) == 1) then " +
"local nextThreadId = redis.call('lindex', KEYS[2], 0); " +
"if nextThreadId ~= false then " +
"redis.call('publish', KEYS[4] .. ':' .. nextThreadId, ARGV[1]); " +
"end; " +
"return 1; " +
"end; " +
"return 0;",
Arrays.<Object>asList(getName(), threadsQueueName, timeoutSetName, getChannelName()),
LockPubSub.UNLOCK_MESSAGE, System.currentTimeMillis());
}
执行目标:释放锁,通知其他客户端继续尝试。
执行步骤:
- 第一步的while循环和之前获取锁的目的一致,删除过期的客户端线程;
- 如果锁存在,强行删除资源锁,然后取出等待队列中的第一个客户端,如果不为空,则发布此客户端是否锁的消息,通知其他等待线程继续尝试,返回true;
- 如果锁不存在,直接返回false;
手动释放
非公平锁的手动释放比较简单,删除资源占用锁即可,公平锁则稍微复杂一点,需要做一下前置判断。
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
// remove stale threads
"while true do "
+ "local firstThreadId2 = redis.call('lindex', KEYS[2], 0);"
+ "if firstThreadId2 == false then "
+ "break;"
+ "end; "
+ "local timeout = tonumber(redis.call('zscore', KEYS[3], firstThreadId2));"
+ "if timeout <= tonumber(ARGV[4]) then "
+ "redis.call('zrem', KEYS[3], firstThreadId2); "
+ "redis.call('lpop', KEYS[2]); "
+ "else "
+ "break;"
+ "end; "
+ "end;"
+ "if (redis.call('exists', KEYS[1]) == 0) then " +
"local nextThreadId = redis.call('lindex', KEYS[2], 0); " +
"if nextThreadId ~= false then " +
"redis.call('publish', KEYS[4] .. ':' .. nextThreadId, ARGV[1]); " +
"end; " +
"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; " +
"end; " +
"redis.call('del', KEYS[1]); " +
"local nextThreadId = redis.call('lindex', KEYS[2], 0); " +
"if nextThreadId ~= false then " +
"redis.call('publish', KEYS[4] .. ':' .. nextThreadId, ARGV[1]); " +
"end; " +
"return 1; ",
Arrays.<Object>asList(getName(), threadsQueueName, timeoutSetName, getChannelName()),
LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId), System.currentTimeMillis());
}
执行目的:
执行步骤:
- 第一步的while循环和之前获取锁的目的一致,删除过期的客户端线程;
- 如果锁不存在,取出等待队列中的第一个客户端,如果不为空,则发布此客户端是否锁的消息,通知其他等待线程继续尝试,返回true;
- 如果持有锁的不是当前参数中的客户端线程,则返回null;
- 如果持有锁的是当前线程,持有次数减一之后如果大于零,重置过期时间,返回false;
- 如果持有次数减一之后等于0,删除资源锁,取出等待队列中的第一个客户端,如果不为空,则发布此客户端是否锁的消息,通知其他等待线程继续尝试,返回true;
自动释放
与非公平锁类似,锁到期之后,资源被redis自动回收,其他客户端线程继续尝试获取锁。