What is FairLock

In the previous chapter, we printed the class diagram of the Redisson distributed lock core code. We can see that FairLock is based on RedissonLock subclass, which implements some other features based on RedissonLock

The core concept

Compared with ReentrantLock and FairLock, it can obviously be found that the FairLock is represented. From the previous case, it can be found that the sequence control is not carried out on the threads competing for the lock after the lock failure, but the threads are allowed to compete, which is unfair. FairLock controls this through a series of mechanisms, and the order in which locks are acquired is the same as the order in which locks are requested

Code implementation

/ / get a FairLock RLock FairLock = redissonClient. GetFairLock (" lockName "); The lock (); / / locking fairLock. // Release the lock fairlock. unlock();Copy the code
  • The implementation code is very simple, almost the same as ReentrantLock, except that when the lock object is initialized, it initializes a RedissonFairLock, instead of the previous RedissonLock

Ask questions

lock

Since FairLock can implement the sequence of locking, the whole process of locking will make people wonder how to ensure the consistency of locking

  • Redis may have a queue that uses the first-in, first-out rule of the queue to record information about locked threads
  • So what happens if you run out of time to get the lock, you know there’s probably a waitTime when you get the lock, so how do you do that if you get a waitTime
  • If leaseTime is set, which means that the timeout lock is automatically released, what difference will it make?
  • How does a reentrant lock work in this queue, and you can imagine, if it’s a reentrant lock, then the lock is successful, you just compare the element that holds the lock to the thread that is currently holding the lock, and if it’s the same, then it’s the same thread that’s holding the lock, and you can do a reentrant lock, right
  • Another problem is that when no lock is acquired, there will be a loop to acquire the lock, which should ensure that the element in the queue is unique
  • Watchdog should complete the same renewal work as before

Release the lock

There are two ways to release a lock. One is to release the lock actively. The unlock command is executed to release the lock when the lock has been held. There is also a passive release, timeout lock automatically released, that is, leaseTime is set, but the watchdog does not start again

Two, source code analysis

Initialize the

This.mandexecutor = commandExecutor; This. ThreadWaitTime = threadWaitTime; // threadWaitTime: 60000*5; Redisson_lock_queue :{lockName} threadsQueueName = prefixName("redisson_lock_queue", name); // set set name: redisson_lock_TIMEOUT :{lockName} timeoutSetName = prefixName("redisson_lock_timeout", name);Copy the code

As you can see, the whole process of initialization is to initialize some member variables.

  • ThreadWaitTime: 60000 * 5
  • Queue name: redisson_lock_queue:{lockName}
  • Set set name: redisson_lock_timeout:{lockName}

Similarly, we can guess that threadWaitTime is the wait time to acquire the lock, and then maintain a queue and a set in Redis


Through reading the source code, you can find that FairLock in the whole process of lock, almost all go RedissonLock, found that RedissonFairLock just reload RedissonLock method

  • TryLockInnerAsync lock
  • AcquireFailedAsync Unacquires the lock
  • UnlockInnerAsync releases the lock
  • ForceUnlockAsync Forces the lock to be released

The following time, we also put these several core source process to see, introduced what different mechanism to complete queuing lock

lock

Lua script anatomy

The core of the lock code is tryLockInnerAsync, the main core is a Lua script

Here are some comments to add to the Lua script for your own understanding

  • lockName
  • redisson_lock_queue:{lockName}
  • redisson_lock_timeout:{lockName}
  • Uuid :threadId lock thread
  • WaitTime = threadWaitTime = 60000*5
  • CurrentTime indicates the currentTime
// KEY[1] lockName
// KEY[2] threadsQueueName -> redisson_lock_queue:{lockName}
// KEY[3] timeoutSetName -> redisson_lock_timeout:{lockName}
// ARGV[1] unit.toMillis(leaseTime)
// ARGV[2] uuid:threadId 
// ARGV[3] waitTime
// ARGV[4] currentTime indicates the currentTime-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- cycle -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --/ / (1If there is no element, it is not processed.2) or give expired elements toremoveoffwhile true do// lindex redisson_lock_queue:{lockName}0
  local firstThreadId2 = redis.call('lindex', KEYS[2].0); // If you don't get it, just jump outif firstThreadId2 == false then
    break;
  end; // Get the score (timeout) of the header element in setlocal timeout = tonumber(redis.call('zscore', KEYS[3], firstThreadId2)); // If the timeout is smaller than the current time, remove it from the queue and setif timeout <= tonumber(ARGV[4]) then 
    redis.call('zrem', KEYS[3], firstThreadId2);
    redis.call('lpop', KEYS[2]);
  else
    break;
  end;
end;

-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- lock -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --// The current lock lockName is not available and the lockName queue does not exist or the head element is locked from the same thread in the queue--> indicates that it can be locked// (exists lockName ==0) and 
// ((exists redisson_lock_queue:{lockName} == 0) / /or (lindex redisson_lock_queue:{lockName} 0 ==  uuid:threadId))
if (redis.call('exists', KEYS[1= =])0) 
  and ((redis.call('exists', KEYS[2= =])0) 
    or (redis.call('lindex', KEYS[2].0) == ARGV[2])) then// Add the currently locked thread to the queue and setremoveLpop redisson_lock_queue:{lockName} redis. Call ('lpop', KEYS[2]);
  // zrem redisson_lock_timeout:{lockName} uuid:threadId 
  redis.call('zrem', KEYS[3], ARGV[2]); // zrange redisson_lock_timeout:{lockName}0 - 1
  local keys = redis.call('zrange', KEYS[3].0.- 1); / / traversal keysfor i = 1, #keys, 1 do// Zscore is set to: Zincrby redisson_lock_timeout:{lockName} - waitTime keys[I]  redis.call('zincrby', KEYS[3] -tonumber(ARGV[3]), keys[i]);
  end; // hset lockName uuid:threadId1
  redis.call('hset', KEYS[1], ARGV[2].1); // pexpire lockName leaseTime redis.call('pexpire', KEYS[1], ARGV[1]); / / returnnil, the lock is successfully added, and a watchdog scheduling task is startedreturn nil;
end;

-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- reentrant lock -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --// hexists lockName uuid:threadIdif 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;

-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- have thread blocking in the queue, while acquiring a lock -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -// select * from set where uuid:threadId key; Zscore redisson_lock_timeout:{lockName} uuid:threadIdlocal timeout = redis.call('zscore', KEYS[3], ARGV[2]);
if timeout ~= false then
  return timeout - tonumber(ARGV[3]) - tonumber(ARGV[4]);
end;


-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- for the first time to come over to the mutex -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --// Count and return the TTL of the last thread in the queue and add to the queue and set // get the last element in the queue // lindex threadsQueueName- 1
local lastThreadId = redis.call('lindex', KEYS[2].- 1);
localttl; // Check that the last element in the queue is not empty and does not equal uuid:threadIdif lastThreadId ~= false and lastThreadId ~= ARGV[2] then// zscore redisson_lock_TIMEOUT :{lockName} lastThreadId - Current time TTL =tonumber(redis.call('zscore', KEYS[3], lastThreadId)) - tonumber(ARGV[4]);
elsePTTL lockName TTL = redis.call('pttl', KEYS[1]);
end;

// timeout = ttl + waitTime + currentTime
local timeout = ttl + tonumber(ARGV[3]) + tonumber(ARGV[4]); Zadd redisson_lock_timeout:{lockName} timeout uuid:threadIdif redis.call('zadd', KEYS[3], timeout, ARGV[2= =])1 thenRpush redisson_lock_queue:{lockName} uuid:threadId redis.call(rpush redisson_lock_queue:{lockName} uuid:threadId redis.call('rpush', KEYS[2], ARGV[2]);
end; / / returns the TTLreturn ttl;
Copy the code

Flowchart for locking

From the general flow chart we can probably enumerate several cases:

  • First lock: Directly access the hash key that holds the lockName
  • Lock already held, reentrant lock: directly enter the hash key counter+1, reset the expiration time
  • Get hash expiration time + waitTime + currentTime1 for lockName and set timeout to queue and set

So let’s say that 10ms has passed, so currentTime2 minus currentTime1 = 10ms

  • (currentTime2) select * from queue where timeout is smaller than currentTime2 Timeout1 + currentTime2 = TTL + waitTime + currentTime2; This ensures that the returned TTL is decremented over time and that timeout2 is always larger than timeout1
  • Now that the lock has been held, the first competing lock will be removed. If the timeout time of each element is smaller than currentTime2, the lock will be removed. Return the lock timeout-waittime-currentTime (); return the lock timeout-waittime-currentTime ()
  • The lock has been held for a long time, and the first time there is a competing lock again: Timeout = TTL +waitTime+currentTime1 = timeout= TTL +waitTime+currentTime1 If the lock is not acquired within the TTL of the lock, the element will be removed from the queue and set, and the timeout of the other set elements will be subtracted from the waitTime released
  • The lock is released and the second thread to lock comes: At this point, the discovery is the same as the presence of elements in the queue, once again competing for the lockThe lock has been held, and the first competitive lock appears again
  • When the lock is released, the queue header element is released: delete the queue and set elements, enter the lock logic, and subtract the set elements by waitTime

Locking scenario diagram

I simply drew a sequence diagram of several scenes with locks – ha ha ha pseudo sequence diagram. I didn’t have a good idea about how to draw it at the beginning

Features in fair locking

Queue lock

Queuing locking is also the main difference between fair locking and RLock. Locks can be acquired in the order in which they are requested

The core is to introduce a queue to store the order in which locks are acquired, and to control lock timeouts by maintaining an ordered set of Zsets

  • If there is a lock contention, the lock will be added to the queue. At the same time, by setting a timeout time, the timemout time will be relatively large, adding a waitTime=60000 × 5, 5 minutes
  • At the same time, if the previous data in the queue is locked successfully, then the timeout of every element after it needs to be deducted. In fact, I think it can be interpreted as that it thinks that a lock has been locked for 5 minutes at most + TTL does not obtain the lock, which means that it has failed
  • Other and RLock is the same, if the lock is successful, will pass a watchdog to renew the lease
  • When the lock fails, a loop is entered to try to acquire the lock

Obtain lock timeout automatically release the lock

  • Each time a lock request comes in, it looks at the queue header to see if there is a timeout. If there is a timeout, it is removed from the queue and zset
  • The timeout is removed from the Redis queue and zset. If the background while loop acquires the lock again, it will re-join the queue

Automatic refresh timeout period

  • If the lock is successful, subtract waitTime from all the collections in zSet and refresh the score

Release the lock

Can be seen in the code structure. The lock is released in fact is to reconstruct the org redisson. RedissonFairLock# unlockInnerAsync method

The core is also a Lua script

-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- cycle -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- / / element (1) or not, 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; -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- to release is not held by -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- the 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 not holding the lock people -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- the if (redis. Call (' hexists, KEYS [1], ARGV[3]) == 0) then return nil; end; --------------------------- If it is a reentrant lock, Just count - 1 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- the local counter = redis. Call (' hincrby, KEYS [1], ARGV [3], 1); if (counter > 0) then redis.call('pexpire', KEYS[1], ARGV[2]); return 0; end; -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- if you hold the lock is released, can send a broadcast message -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - 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;Copy the code
  • If the owner of the lock is not the owner, null is returned, and the code returns a failure to release the lock
  • If the lock is successfully released or the lock is not held, that is, another thread is required to lock it, a message will be sent, returning 1
  • If the lock was successfully released but is a re-entrant, count-1 returns 0

The lock release is successful if null is not returned

Core fair lock knowledge about these, mainly is a detailed look at the lock and lock release lua script, and lock release process