When it comes to read/write locks, we will quickly react to them. The existence of read/write locks is to improve the concurrency of practical applications, which can ensure that reading is not mutually exclusive, read/write is mutually exclusive, and write is mutually exclusive

I. Concept and implementation

1. The concept

The official documentation

  1. Github
  • The core interface ReadWriteLock is built based on ReadWriteLock in Java. Both read lock and write lock implement RLock interface
  • Multiple ReadLock owners and only one WriteLock owner are allowed
    • It’s not mutually exclusive
    • Write about a mutex
    • Read and write the mutex
  • If the Redisson instance that acquired the lock crashes, the lock may hang in the acquired state forever
    • To avoid the Redisson maintenance lock watchdog, it extends the lock expiration period while the Redisson instance of the lock holder is alive
    • You can also set the lock holding time leaseTime

2. Implement

RReadWriteLock lock = redissonClient.getReadWriteLock("lockName");
lock.readLock().lock();
lock.readLock().unlock();

lock.writeLock().lock();
lock.writeLock().unlock();
Copy the code
  • It’s pretty simple to use

Initialize the

  1. The actual initialized object is oneRedissonReadWriteLock, this object will hold two small objectsRedissonReadLock RedissonWriteLock
  2. In this way, you can control whether you need writeLock or readLock as needed

Two, source code analysis

No more words, directly look at the source code

lock

1. Read lock

ReadLock (). Lock() ¶ tryLockInnerAsync (tryLockInnerAsync, tryLockInnerAsync, tryLockInnerAsync, tryLockInnerAsync) ¶

Add read lock lua logic

// KEY[1] = lockName
// KEY[2] = {lockName}:uuid:threadId:rwlock_timeout
// ARVG[1] = leaseTime
// ARVG[2] = uuid:threadId
// ARVG[3] = uuid:threadId:write

// hget lockName mode
local mode = redis.call('hget', KEYS[1].'mode'); // The mode attribute does not existif (mode == false) then// Set the lockName mode property toread// hset lockName moderead
  redis.call('hset', KEYS[1].'mode'.'read'); // The number of locks increases1
  // hset lockName uuid:threadId 1
  redis.call('hset', KEYS[1], ARGV[2].1);
  // set {lockName}:uuid:threadId:rwlock_timeout:1 1
  redis.call('set', KEYS[2]..'1'.1);
  // pexpire {lockName}:uuid:threadId:rwlock_timeout:1 leaseTime
  redis.call('pexpire', KEYS[2]..'1', ARGV[1]); // pexpire lockName leaseTime redis.call('pexpire', KEYS[1], ARGV[1]);
  return nil;
end; 
Copy the code
  1. Get the mode property value of the hash data lockName from Redis
    • The first time the lock is added, the key does not exist, and the mode attribute does not exist
  2. Set the hash key of lockName and the mode property to read
{
  "lockName": {
    "mode": "read"}}Copy the code
  1. Set the hash key uUID of lockName to 1
{
  "lockName": {
    "mode": "read"."uuid:threadId": 1}}Copy the code
  1. Set up the{lockName}:uuid:threadId:rwlock_timeout:1A key of is 1
{
  "lockName": {
    "mode": "read"."uuid:threadId": 1
  },
  
  "{lockName}:uuid:threadId:rwlock_timeout:1": 1
}
Copy the code
  1. Set up the{lockName}:uuid:threadId:rwlock_timeout:1The expiration time of the key is leaseTime
  2. Set up thelockNameThe expiration time of the key is leaseTime
  1. returnnull

So, if a read lock is added, two keys are generated, lockName {lockName}:uuid:threadId:rwlock_timeout:1

2. Read lock watchdog

After the success of the lock, if there is no set up leaseTime, scheduling should be executed contract time correction, found that after add read lock watchdog has been rewritten, org. Redisson. RedissonReadLock# renewExpirationAsync

// KEY[1] = lockName
// KEY[2] = {lockName}
// ARVG[1] = leaseTime
// ARVG[2] = uuid:threadId

// hget lockName uuid:threadId
local counter = redis.call('hget', KEYS[1], ARGV[2]); 
if (counter ~= false) then 
    // pexpire lockName leaseTime
    redis.call('pexpire', KEYS[1], ARGV[1]); 
    // hlen lockName > 1
    if (redis.call('hlen', KEYS[1) >1) then 
        // hkeys lockName
        local keys = redis.call('hkeys', KEYS[1]);
        for n, key in ipairs(keys) do
            // hget lockName key
            counter = tonumber(redis.call('hget', KEYS[1], key)); // If you get it, renew itif type(counter) == 'number' then
                for i=counter, 1.- 1 do 
                    redis.call('pexpire', KEYS[2]..':'. key ..':rwlock_timeout:'. i, ARGV[1]);
                end; 
            end; 
        end; 
      return 1;
    end;
end; 
return 0;
Copy the code

Renew redis lock

  1. To obtainlockNameThe key ofuuid:threadIdValue, which represents the number of read locks acquired by this thread. After the first read lock is added, this value should be 1
  2. If the current thread still holds the read lock, the watchdog logic will be followed
  3. Renew the key of lockName and set the expiration time to leaseTime
  4. Get all the attributes in the lockName key and iterate over them
  5. The key property with a numeric value is processed
  6. traverses{lockName}:uuid:threadId:rwlock_timeout:iRenew leaseTime and return 1 if the renewal is successful

3. Add a reentrant read lock

In the locked state, the same thread now adds a read lock, also known as a reentrant lock

// KEY[1] = lockName
// KEY[2] = {lockName}:uuid:threadId:rwlock_timeout
// ARVG[1] = leaseTime
// ARVG[2] = uuid:threadId
// ARVG[3] = uuid:threadId:write// If there is already a read lock or a write lock, it is oneselfif (mode == 'read'// Add write lock and lockName uuid:threadId:writeAttributes existor (mode == 'write' and redis.call('hexists', KEYS[1], ARGV[3= =])1) thenThe number of locks increased1
  // incrby lockName uuid:threadId 1
  local ind = redis.call('hincrby', KEYS[1], ARGV[2].1); 
  local key = KEYS[2]..':'. ind; redis.call('set', key, 1); 
  redis.call('pexpire', key, ARGV[1]); 
  local remainTime = redis.call('pttl', KEYS[1]);
  redis.call('pexpire', KEYS[1].math.max(remainTime, ARGV[1]));
  return nil;
end;

return redis.call('pttl', KEYS[1]);
Copy the code

There is also the following branch judgment in the lock read code

If the resource has been read locked, or has been write locked by the current thread

  1. Increment lockName uUID :threadId value, which indicates the number of locks +1
  2. Set the key to{lockName}:uuid:threadId:rwlock_timeout:iValue of 1
  3. Set up the{lockName}:uuid:threadId:rwlock_timeout:iThe expiration time is leaseTime
  4. Get TTL of lockName, set the expiration time of lockName to a greater value between TTL and leaseTime, so there may be cases where TTL is larger than leaseTime, usually under a reentrant write lock
  5. Returns NULL if the lock was successful

4. Write locks

Write lock is the same, in redis data structure, try to add write lock

// KEY[1] lockName
// ARGV[1] leaseTime
// ARGV[2] uuid:threadId:write

// hget lockName mode
local mode = redis.call('hget', KEYS[1].'mode');
if (mode == false) then
  // hset lockName mode write
  redis.call('hset', KEYS[1].'mode'.'write');
  // hset lockName uuid:threadId:write 1
  redis.call('hset', KEYS[1], ARGV[2].1);
  // pexpire lockName leaseTime
  redis.call('pexpire', KEYS[1], ARGV[1]);
  return nil;
end;
Copy the code
  1. First go to Redis to obtain the mode attribute of lockName
  2. If there is no lock, then the mode does not exist
  3. Set lockName key mode to write
{
  "lockName": {
    "mode": "write"}}Copy the code
  1. Set the lockName propertyuuid:threadId:writeValue of 1
{
  "lockName": {
    "mode": "write"."uuid:threadId:write": 1}}Copy the code
  1. Set the expiration time of lockName to leaseTime
  2. Returns NULL if the lock was successful

5. Write lock watchdog

  • Check that the attribute uUID :threadId exists in lockName
  • Set the expiration time to leaseTime

6. Reentrant plus write lock

// KEY[1] lockName
// ARGV[1] leaseTime
// ARGV[2] uuid:threadId:write

if (mode == 'write') then 
  // hexists lockName uuid:threadId:write
  if (redis.call('hexists', KEYS[1], ARGV[2= =])1) then
    // hincrby lockName uuid:threadId:write 1
    redis.call('hincrby', KEYS[1], ARGV[2].1);
    // pttl lockName
    local currentExpire = redis.call('pttl', KEYS[1]);
    // pexpire lockName ttl+leaseTime
    redis.call('pexpire', KEYS[1], currentExpire + ARGV[1]);
    return nil;
  end;
end;

return redis.call('pttl', KEYS[1]);
Copy the code

Adding a write lock to the same thread indicates a reentrant write lock

  1. Get the mode attribute of lockName, if it is equal to write, it is already in the state of write lock acquisition, and then judge the attribute in lockNameuuid:threadId:writeCheck whether a reentrant lock exists
  2. The lockName attribute is first passeduuid:threadId:writeThe value of + 1
  3. Get the TTL of lockName
  4. Set the expiration time of lockName to TTL +leaseTime
  5. Returns NULL if the lock was successful

7. Other threads add write locks

Add lock lua logic, there are two judgments, one is to determine whether the lockName is added lock, one is the current thread will go to the corresponding logic, otherwise it will directly return the TTL of the lockName

8. Reentrant add read lock

This is consistent with the logic of 3 above

9. Read lock

If the lock is not added, it will directly add the read lock. If the lock is added, it will determine whether the lock is read or not, or whether the lock is written by its own thread

10. Add write locks to read locks

Without a ripple, it returned TTL directly

Release the lock

1. Release the read lock

// KEY[1] lockName
// KEY[2] redisson_rwlock:{lockName}
// KEY[3] {lockName}:uuid:threadId:rwlock_timeout
// KEY[4] {lockName}
// ARVG[1] 0
// ARVG[2] uuid:threadId


// hget lockName mode
local mode = redis.call('hget', KEYS[1].'mode'); 
if (mode == false) then
  // publish redisson_rwlock:{lockName} 0
  redis.call('publish', KEYS[2], ARGV[1]); 
  return 1;
end;   

// hexists lockName uuid:threadId
local lockExists = redis.call('hexists', KEYS[1], ARGV[2]);
if (lockExists == 0) then
  return nil;
end;

// hincrby lockName uuid:threadId - 1
local counter = redis.call('hincrby', KEYS[1], ARGV[2].- 1); 
if (counter == 0) then
  // hdel lockName uuid:threadId
  redis.call('hdel', KEYS[1], ARGV[2]);
end;

// del {lockName}:uuid:threadId:rwlock_timeout:(counter+1)
redis.call('del', KEYS[3]..':'. (counter+1)); 

// hlen lockName > 1
if (redis.call('hlen', KEYS[1) >1) then
  local maxRemainTime = - 3;
  // hkeys lockName
  local keys = redis.call('hkeys', KEYS[1]);
  for n, key in ipairs(keys) do
    // hget lockName keys
    counter = tonumber(redis.call('hget', KEYS[1], key));
    if type(counter) == 'number' then// Iterate to get the maximum TTLfor i=counter, 1.- 1 do
        // pttl {lockName}:key:rwlock_timeout:i
        local remainTime = redis.call('pttl', KEYS[4]..':'. key ..':rwlock_timeout:'. i); // maxRemainTime =math.max(remainTime, maxRemainTime);
      end;
    end;
  end;
  
  if maxRemainTime > 0 then
    // pexpire lockName maxRemainTime
    redis.call('pexpire', KEYS[1], maxRemainTime);
    return 0;
  end;
  
  if mode == 'write' then 
    return 0;
  end;
end;

redis.call('del', KEYS[1]);
redis.call('publish', KEYS[2], ARGV[1]);
return 1;
Copy the code
  1. Get the mode attribute of lockName from Redis. If it does not exist, no one holds the lock. Return 1
  2. Check whether there is a uUID :threadId attribute in lockName. If there is no uUID, return null
  3. LockName: uuid:threadId attribute -1, if the attribute value is 0, delete uuid:threadId attribute directly
  4. Delete the corresponding key:{lockName}:uuid:threadId:rwlock_timeout:(counter+1)
  5. Get all attributes in lockName, get keys{lockName}:uuid:threadId:rwlock_timeout:iMaximum TTL in
  6. Set the expiration time of lockName to the maximum TTL. 0 is returned, indicating that the lock is released successfully
  7. If it is already held by a write lock, 0 is returned indicating that the lock was released successfully
  8. In other cases, the lockName key is deleted

2. Release the write lock

// KEY[1] = lockName
// KEY[2] = redisson_rwlock:lockName
// ARVG[1] = 0 // LockPubSub.READ_UNLOCK_MESSAGE
// ARVG[2] = leaseTime
// ARVG[3] = uuid:threadId:write// Get and verify the modelocal mode = redis.call('hget', KEYS[1].'mode'); 
if (mode == false) then// It does not exist1
  redis.call('publish', KEYS[2], ARGV[1]); 
  return 1;
end; // The current lock iswriteThe lockif (mode == 'write') then// Check whether there is a write lock added by the current thread uuid:threadId:write
  local lockExists = redis.call('hexists', KEYS[1], ARGV[3]);
  if (lockExists == 0) then// There is no direct return null, there is no write lock, cannot be releasedreturn nil;
  else
    // uuid:threadId:writevalue- 1
    local counter = redis.call('hincrby', KEYS[1], ARGV[3].- 1);
    if (counter > 0) then// Reset the expiration time if there is a reentrant lock'pexpire', KEYS[1], ARGV[2]);
      return 0;
    else/ / delete uuid: threadId:writeAttribute redis. Call ('hdel', KEYS[1], ARGV[3]); // Check if only one write lock is heldif (redis.call('hlen', KEYS[1= =])1) then// Delete key redis.call('del', KEYS[1]);
        redis.call('publish', KEYS[2], ARGV[1]);
      elseRedis.call (redis.call();'hset', KEYS[1].'mode'.'read');
      end;
      return 1;
    end;
  end;
end;
return nil;
        
Copy the code

Three, think

Read lock

  1. {lockName}:uuid:threadId:rw_timeout:1 redis key {lockName}:uuid:threadId:rw_timeout:1 redis key {lockName}:uuid:threadId:rw_timeout:1 redis key {lockName}:uuid:threadId:rw_timeout:1

Write lock

  1. Write lock is a lock, but its attribute value is changed, uuid:threadId:write, but at the same time, the current thread may acquire the read lock