参考资料:
总结
Redisson 实现公平锁的原理,需要借助 List 作为 先进先出的队列,和 SortedSet,用来保存线程获取到锁的预计时间,如果在这个时间前还没有获取到锁,那么这个线程会被移出队列。
Redisson 实现公平锁的原理,需要借助 List 和 SortedSet
- List 作为一个FIFO的队列,右边进,左边出,按照请求顺序保存线程
- SortedSet 是保存预计获取到锁的时间点,如果在这个时间点前还没有获取到锁,那么会被移出队列,这次获取锁请求失败
公平锁使用
public static void getFairLock () {
// 获取一个公平锁
RLock fairLock = redissonClient.getFairLock("fairLockName");
fairLock.lock();
fairLock.unlock();
}
参数说明
要分析核心lua脚本前,先了解下相关参数。
KEYS = Arrays.asList(getName(), threadsQueueName, timeoutSetName)
- KEYS[1]: 锁的名称,即我们设置的 "fairLockName"
- KEYS[2]: threadsQueueName,线程队列名称,格式是
redisson_lock_queue:{锁名称} - KEYS[3]: timeoutSetName,超时集合名称,格式是
redisson_lock_timeout:{锁名称}
为了实现公平锁,Redisson 使用队列按照顺序存储需要获取锁的线程。另外,为了可以清除失效的请求,使用SortedSet记录获取锁的超时时间
ARGV = internalLockLeaseTime, getLockName(threadId), threadWaitTime, currentTime
- ARGV[1]:锁的释放时间,默认是30s
- ARGV[2]:线程名称,格式是
UUID:threadId - ARGV[3]:线程等待时间(5s)
- ARGV[4]:当前时间
核心lua脚本分析
if (command == RedisCommands.EVAL_LONG) {
return 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
// 获取并遍历集合中所有的线程,每个线程的超时时间都减去5s
"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
// 当前线程获取到锁,并设置过期时间,默认是30s
"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
// 如果当前线程已经获取到锁,则value+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;" +
// the lock cannot be acquired
// check if the thread is already in the queue
// 获取不到锁
// 如果当前线程已经在队列里面,返回 超时时间-等待时间(5s)-当前时间
"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);" +
// 计算ttl
// 如果最后一个线程存在,且不是当前线程的话,ttl就等于最后一个线程的超时时间 减去 当前时间
// 否则的话,那么就是队列为空,看下锁剩余多少时间
"if lastThreadId ~= false and lastThreadId ~= ARGV[2] then " +
"local ttl;" +
"ttl = tonumber(redis.call('zscore', KEYS[3], lastThreadId)) - tonumber(ARGV[4]);" +
"else " +
"ttl = redis.call('pttl', KEYS[1]);" +
"end;" +
// 计算超时时间 并 当前线程入队
// 超时时间 等于 ttl + 线程等待时间 + 当前时间
"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;" +
// 如果返回的ttl为数字,会重复执行这段lua脚本
"return ttl;",
Arrays.asList(getName(), threadsQueueName, timeoutSetName),
internalLockLeaseTime, getLockName(threadId), wait, currentTime);
}
补充:
-
返回的是nil,在外层代码中,就会认为是加锁成功。此时就会开启一个watchdog看门狗定时调度的程序,每隔10秒判断一下,当前这个线程是否还持有锁,如果是,则刷新锁key的生存时间为30000毫秒
-
返回的是ttl,是一个数字的话,那么此时客户端就会进入一个while true的死循环,每隔一段时间都尝试去进行加锁,重新执行这段lua脚本
具体的加锁过程可以看参考文档,比较详细。
疑问
看完这个lua脚本最大的疑问是,当某个线程获取到锁,为什么要扣减等待时间?