RedissonJavaRedis

前言

上一节分析了非公平锁Lua脚本分析(RedissonLock.tryLockInnerAsync方法),下面我们分析一下公平锁执行逻辑,其执行的代码和上面非公平锁一样,只不过Lua脚本执行不一样,代码demo如下:

  1. RLock fairLock = redisson.getFairLock("anyLock");
  2. fairLock.lock();
  3. Thread.sleep(10000);
  4. 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)来进行排序
  • 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)

细节分析,假如有几个线程,具体如下:

  1. 客户端A thread01 10:00:00 获取锁(第一次加锁)
  2. 客户端B thread02 10:00:10 获取锁
  3. 客户端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脚本解锁源码分析(还没写)

截取资料

  1. 使用Redisson实现公平锁原理
  2. redisson github的wiki相关说明
  3. redisson 分布式锁lua脚本解析
  4. Redis 中文网站指令查询
  5. Redisson分布式锁学习总结:公平锁 RedissonFairLock#unLock 释放锁源码分析
  6. Redisson 分布式锁源码 06:公平锁排队加锁
  7. 分布式锁:Redisson源码解析-FairLock
  8. Redisson 分布式锁源码 07:公平锁释放