References:

  1. Use Redisson to implement the principle of fair locking

conclusion

Redisson implements the principle of fair lock by using List as a first-in, first-out queue and SortedSet to save the expected time for the thread to obtain the lock. If the thread does not obtain the lock before this time, the thread will be removed from the queue.

Redisson implements the principle of fair locking by using lists and sortedSets

  • The List acts as a FIFO queue, entering on the right and leaving on the left, saving threads in order of request
  • SortedSet is the time point at which the lock is expected to be obtained. If the lock is not obtained before this time point, it will be removed from the queue, and the lock acquisition request fails this time

Fair lock use

public static void getFairLock (a) {
    // Get a fair lock
    RLock fairLock = redissonClient.getFairLock("fairLockName");
    fairLock.lock();
    fairLock.unlock();
}
Copy the code

Parameters that

Before analyzing the core Lua script, take a look at the parameters.

KEYS = Arrays.asList(getName(), threadsQueueName, timeoutSetName)

  • KEYS[1]: the name of the lock, that is, we set “fairLockName”
  • KEYS[2]: threadsQueueName, the name of the thread queueRedisson_lock_queue :{lock name}
  • KEYS[3]: timeoutSetName, timeoutSetName, format isRedisson_lock_timeout: {lock name}

To achieve fair locking, Redisson uses queues to store the threads that need to acquire locks sequentially. In addition, in order to clean up invalid requests, use SortedSet to record the timeout for obtaining the lock

ARGV = internalLockLeaseTime, getLockName(threadId), threadWaitTime, currentTime

  • ARGV[1] : Lock release time, default is 30s
  • ARGV[2] : thread name in the formatUUID:threadId
  • ARGV[3] : Thread wait time (5s)
  • ARGV[4] : current time

Core Lua script analysis

if (command == RedisCommands.EVAL_LONG) {
	return evalWriteAsync(getName(), LongCodec.INSTANCE, command,
		// remove stale threads
		// The main function of this loop is to delete the thread that is in the failed queue
		"while true do " +
			// Get the first thread of the queue. If the queue is empty, exit the loop
			"local firstThreadId2 = redis.call('lindex', KEYS[2], 0);" +
			"if firstThreadId2 == false then " +
				"break;" +
			"end;" +
			// Get the timeout time of the first thread. If it is less than the current time, it is invalid and needs to be removed from the queue and collection
			// Otherwise exit the loop
			"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
		// This code is used to determine whether the current thread can acquire the lock
		// If the current lock is empty and the queue is empty or the first thread in the queue is the current thread
		"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
			// Remove the current thread from the queue and collection
			"redis.call('lpop', KEYS[2]);" +
			"redis.call('zrem', KEYS[3], ARGV[2]);" +

			// decrease timeouts for all waiting in the queue
			// Get and iterate over all threads in the collection, subtracting the timeout for each thread by 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
			// The current thread acquires the lock and sets the expiration time (default is 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
		// If the current thread has already acquired the lock, value+1 (reentrant). Also reset the expiration time
		"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
		// The lock cannot be obtained
		// If the current thread is already in the queue, return the timeout - wait time (5s) - current time
		"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
		// Get the last thread in the queue
		"local lastThreadId = redis.call('lindex', KEYS[2], -1);" +
		/ / calculate TTL
		// If the last thread exists and is not the current thread, TTL is equal to the timeout of the last thread minus the current time
		// If not, the queue is empty
		"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;" +
		// Count the timeout and the current thread is enqueued
		// The timeout is TTL + thread waiting time + current time
		"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;" +
		
		// If the TTL returned is a number, the lua script is repeated
		"return ttl;",
		
		Arrays.asList(getName(), threadsQueueName, timeoutSetName), 
			internalLockLeaseTime, getLockName(threadId), wait, currentTime);
}
Copy the code

Supplement:

  1. It’s going to return nil, so in the outer code, it’s going to be considered lock success. At this time, it will start a watchdog watchdog scheduling program, every 10 seconds to judge whether the current thread still holds the lock, if so, the survival time of the refresh lock key is 30000 milliseconds

  2. The return TTL is a number, so the client will enter a while true loop and try to lock it every once in a while to re-execute the lua script

Specific lock process can see the reference document, more detailed.

doubt

The biggest question after reading this Lua script is, when a thread obtains a lock, why should the wait time be deducted?