前言
上一节分析了非公平锁Lua脚本分析(RedissonLock.tryLockInnerAsync方法),下面我们分析一下公平锁执行逻辑,其执行的代码和上面非公平锁一样,只不过Lua脚本执行不一样,代码demo如下:
RLock fairLock = redisson.getFairLock("anyLock");fairLock.lock();Thread.sleep(10000);fairLock.unlock();
执行Lua脚本细节分析
具体执行Lua脚本的java代码在RedissonFairLock.tryLockInnerAsync()方法内,前面的方法和非公平一样,这里就不重复叙述了。下面直接分析里面的Lua脚本,如下:
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
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]);
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;
local firstThreadId = redis.call('lindex', KEYS[2], 0);
local ttl;
if firstThreadId ~= false and firstThreadId ~= ARGV[2] then
ttl = tonumber(redis.call('zscore', KEYS[3], firstThreadId)) - tonumber(ARGV[4]);
else
ttl = redis.call('pttl', KEYS[1]);
end;
local timeout = ttl tonumber(ARGV[3]);
if redis.call('zadd', KEYS[3], timeout, ARGV[2]) == 1 then
redis.call('rpush', KEYS[2], ARGV[2]);
end;
return ttl;
KEYS/ARGV参数分析
- KEYS =
Arrays.asList(getName(), threadsQueueName, timeoutSetName)- KEYS[1]=
getName()锁的名字,这里以“keylock”命名 - KEYS[2] =
threadsQueueName = redisson_lock_queue:{keylock},基于redis的数据结构实现的一个队列 - KEYS[3] =
timeoutSetName = redisson_lock_timeout:{keylock},基于redis的数据结构实现的一个Set数据集合,有序集合,可以自动按照你给每个数据指定的一个分数(score)来进行排序
- KEYS[1]=
- ARGV =
internalLockLeaseTime,getLockName(threadId),currentTime + threadWaitTime,currentTime- ARGV[1] = 30000毫秒
- ARGV[2] = UUID:threadId
- ARGV[3] = 当前时间(10:00:00) + 5000毫秒 = 10:00:05
- ARGV[4] = 当前时间(10:00:00)
细节分析,假如有几个线程,具体如下:
- 客户端A thread01 10:00:00 获取锁(第一次加锁)
- 客户端B thread02 10:00:10 获取锁
- 客户端C therad03 10:00:15 获取锁
Lua脚本加锁源码分析
线程1的情况
客户端A thread01 加锁分析。thread01 在10:00:00 执行加锁逻辑,下面开始分析lua脚本执行代码:while true do //开始死循环 local firstThreadId2 = redis.call('lindex', KEYS[2], 0); if firstThreadId2 == false then //如果没有到这里结束循环 break;
- 开启while的死循环
执行
redis.call('lindex', KEYS[2], 0)指令,意思是返回一个keylock队列中索引为0的元素,这个时候线程1找不到,break跳出循环,执行下面的代码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]); 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;线程1执行
redis.call('exists', KEYS[1]) == 0指令,判断keylock是否存在,这个时候key还没创建,所以这里是返回true- 线程1执行
redis.call('exists', KEYS[2]) == 0指令,判断redisson_lock_queue:{keylock}队列是否存在,这时候暂时没有,返回true,后面那个或判断指令redis.call('lindex', KEYS[2], 0) == ARGV[2]其实在redisson_lock_queue:{keylock}第一个元素是UUID:threadId,基本上这3个条件逻辑结果是返回true,执行下面操作
分析:线程1此时keylock和队列,都是不存在的,所以这个条件肯定会成立。接着执行if中的具体逻辑
- 执行
redis.call('lpop', KEYS[2])指令,从redisson_lock_queue:{keylock}队列中获取UUID:threadId的元素,这时候没有 - 执行
redis.call('zrem', KEYS[3], ARGV[2])指令,从redisson_lock_timeout:{keylock} UUID:threadId队列中删除该元素,这个时候压根也没有 执行
redis.call('hset', KEYS[1], ARGV[2], 1)指令,从keylock中添加UUID:threadId,值为1,类似下面结果keylock:{ "8743c9c0-0795-4907-87fd-6c719a6b4586:1":1 }执行
redis.call('pexpire', KEYS[1], ARGV[1])指令,把keylock这个生命周期设定30000毫秒最后返回一个nil,这就线程1加锁成功了,后面watchdog看门狗每隔10秒是否续约生命周期30000毫秒
线程2的情况
由于上面线程1已经获取到了锁了,那么 thread02在10:00:10分来执行加锁逻辑如下:
while true do //开始死循环 local firstThreadId2 = redis.call('lindex', KEYS[2], 0); if firstThreadId2 == false then //如果没有到这里结束循环 break;进入while死循环,
lindex redisson_lock_queue:{keylock} 0,获取队列的第一个元素,此时队列还是空的,所以获取到的是false,直接退出死循环,进入下面加锁环节if (redis.call('exists', KEYS[1]) == 0) and ((redis.call('exists', KEYS[2]) == 0) or (redis.call('lindex', KEYS[2], 0) == ARGV[2])) then //代码省略...... 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;此时线程2在
redis.call('exists', KEYS[1]) == 0判断为false,keylock已经创建了线程2执行
redis.call('exists', KEYS[2]) == 0指令,判断redisson_lock_queue:{keylock}队列是否存在,这里已经创建了,返回false,后面的or判断其实不太重要,这个时候就会进入下面的if判断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;线程2执行判断
redis.call('hexists', KEYS[1], ARGV[2]) == 1指令,在此时keylock下有没有uuid_02: id_02【这个时候是线程1的】,返回结果false,跳出进入下面执行代码 ```lua — 获取队列的队头元素 local firstThreadId = redis.call(‘lindex’, KEYS[2], 0); //1 local ttl;
— 如果元素不存在并且不是加锁的元素
— (判断是否是刚加锁成功的) *只用于计算超时时间
if firstThreadId ~= false and firstThreadId ~= ARGV[2] then //2
— 计算剩余的时间 (zset获取后计算)
ttl = tonumber(redis.call(‘zscore’, KEYS[3], firstThreadId)) - tonumber(ARGV[4]);
else
— 获取剩余时间 (直接获取)
ttl = redis.call(‘pttl’, KEYS[1]); //3
end;
—计算超时时间
local timeout = ttl tonumber(ARGV[3]); //4
if redis.call(‘zadd’, KEYS[3], timeout, ARGV[2]) == 1 then
redis.call(‘rpush’, KEYS[2], ARGV[2]);
end;
return ttl; //5
- 线程2执行`firstThreadId = redis.call('lindex', KEYS[2], 0)`指令:lindex指令,从`redisson_lock_queue:{keylock} 队列`中获取第一个元素,这时候队列依然为空,也没有数据,所以执行`if firstThreadId ~= false and firstThreadId ~= ARGV[2] then `判断返回false
- 执行`ttl = redis.call('pttl', KEYS[1])`指令获取ttl时间为20000毫秒【因为我们是在10:00:10 分请求的,因为`keylock`默认过期时间是30s,所以在thread02请求的时候ttl还剩下20s】
- 计算超时时间` timeout = ttl tonumber(ARGV[3])`【 20000毫秒 + 10:00:00 + 5000毫秒 = 10:00:25】
- 执行`edis.call('zadd', KEYS[3], timeout, ARGV[2])`指令,在`redisson_lock_timeout:{keylock}`队列中添加`10:00:25 UUID_02:threadId_02`,然后再执行`redis.call('rpush', KEYS[2], ARGV[2])`指令,在`redisson_lock_queue:{keylock}`队列`rpush`插入`UUID_02:threadId_02`第一个元素
- 返回ttl结果,这里是线程1剩余时间 【在没有看门狗续命时,还剩20000毫秒】
<a name="zx2F6"></a>
#### 效果图
[点击查看【processon】](https://www.processon.com/view/link/624b887cf346fb57dbe86d4f)
<a name="hMwjc"></a>
### 线程3的情况
上面线程2的状况已经分析了,那么 thread03在10:00:15分来执行Lua脚本加载逻辑如下:
```lua
while true do
local firstThreadId2 = redis.call('lindex', KEYS[2], 0); // 1
if firstThreadId2 == false then
break;
end;
local timeout = tonumber(redis.call('zscore', KEYS[3], firstThreadId2)); //2
if timeout <= tonumber(ARGV[3]) then //3
redis.call('zrem', KEYS[3], firstThreadId2);
redis.call('lpop', KEYS[2]);
else
break; //4
end;
end;
- 进入while死循环,执行
redis.call('lindex', KEYS[2], 0)指令,相当于lindex redisson_lock_queue:{keylock} 0,获取redisson_lock_queue:{keylock}队列的第一个元素,为UUID_02:threadId_02 - if判断不为false,就到
local timeout = tonumber(redis.call('zscore', KEYS[3], firstThreadId2))指令,这里意思是zscore redisson_lock_timeout:{keylock} UUID_02:threadId_02,从有序集合中获取UUID_02:threadId_02对应的分数,timeout为 10:00:25 - if判断
timeout <= tonumber(ARGV[3])为10:00:25 <= 10:00:15 吗? 答案为false,退出循环,之后执行下面的Lua脚本 ```lua //由于加锁,所以直接到达这里 local firstThreadId = redis.call(‘lindex’, KEYS[2], 0); //1 local ttl;
if firstThreadId ~= false and firstThreadId ~= ARGV[2] then //2 ttl = tonumber(redis.call(‘zscore’, KEYS[3], firstThreadId)) - tonumber(ARGV[4]); else ttl = redis.call(‘pttl’, KEYS[1]); //3 end;
local timeout = ttl tonumber(ARGV[3]); //4 if redis.call(‘zadd’, KEYS[3], timeout, ARGV[2]) == 1 then //5 redis.call(‘rpush’, KEYS[2], ARGV[2]); end; return ttl; ```
- 线程3执行
firstThreadId = redis.call('lindex', KEYS[2], 0)指令:lindex指令。从redisson_lock_queue:{keylock} 队列中获取第一个元素UUID_02:thread_02 - 线程3执行
firstThreadId ~= false and firstThreadId ~= ARGV[2]判断当前等待线程是不是自己,但是不是,当前线程是UUID_03:thread_03,firstThreadId为UUID_02:thread_02 - 执行
ttl = redis.call('pttl', KEYS[1])指令,意识是pttl keylock的剩余时间,现在是还剩5000毫秒,后面执行计算timeout时间 - 计算timeout时间指令
local timeout = ttl tonumber(ARGV[3]), 计算当前时间:5000 ms + 当前时间:10:00:15 + 5000 ms(延迟) 为10:00:30 - 将线程3放入以下队列:
edis.call('zadd', KEYS[3], timeout, ARGV[2])指令,在redisson_lock_timeout:{keylock}队列中添加10:00:30 UUID_03:threadId_03,该队列第2个元素- 然后再执行
redis.call('rpush', KEYS[2], ARGV[2])指令,在redisson_lock_queue:{keylock}队列rpush插入UUID_03:threadId_03,该队列的第2个元素效果图
点击查看【processon】Lua脚本解锁源码分析(还没写)
