preface

ReadWriteLock in the JDK is familiar with ReadWriteLock, but Redisson also has an implementation for ReadWriteLock.

The so-called read/write lock is that multiple clients simultaneously add a read lock, which is not mutually exclusive. Multiple clients can add the read lock at the same time. Read lock and read lock are not mutually exclusive

RedissonReadWriteLock is a subclass of RReadWriteLock to implement read/write locks. RedissonReadLock and RedissonWriteLock implement read/write locks respectively

Redisson Read/write lock usage example

Again, use cases from official documentation:

RReadWriteLock rwlock = redisson.getReadWriteLock("tryLock");

RLock lock = rwlock.readLock();
// or
RLock lock = rwlock.writeLock();

// traditional lock method
lock.lock();

// or acquire lock and automatically unlock it after 10 seconds
lock.lock(10, TimeUnit.SECONDS);

// or wait for lock aquisition up to 100 seconds 
// and automatically unlock it after 10 seconds
boolean res = lock.tryLock(100.10, TimeUnit.SECONDS);
if (res) {
   try{... }finally{ lock.unlock(); }}Copy the code

Redisson read lock logic principle

public class RedissonReadLock extends RedissonLock implements RLock {
    @Override
    <T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        internalLockLeaseTime = unit.toMillis(leaseTime);

        return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
                                "local mode = redis.call('hget', KEYS[1], 'mode'); " +
                                "if (mode == false) then " +
                                  "redis.call('hset', KEYS[1], 'mode', 'read'); " +
                                  "redis.call('hset', KEYS[1], ARGV[2], 1); " +
                                  "redis.call('set', KEYS[2] .. : '1', 1); " +
                                  "redis.call('pexpire', KEYS[2] .. ':1', ARGV[1]); " +
                                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                                  "return nil; " +
                                "end; " +
                                "if (mode == 'read') or (mode == 'write' and redis.call('hexists', KEYS[1], ARGV[3]) == 1) then " +
                                  "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]); " +
                                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                                  "return nil; " +
                                "end;" +
                                "return redis.call('pttl', KEYS[1]);", Arrays.<Object>asList(getName(), getReadWriteTimeoutNamePrefix(threadId)), internalLockLeaseTime, getLockName(threadId), getWriteLockName(threadId)); }}Copy the code

Client A (UUID_01:threadId_01) adds the read lock

Note: In the following articles, the UUID_01:threadId_01 identifier is used for client A and the UUID_02:threadId_02 identifier is used for client B

KEYS:

  • KEYS1: getName() = tryLock
  • KEYS[2]: getReadWriteTimeoutNamePrefix(threadId) =  {anyLock}:UUID_01:threadId_01:rwlock_timeout

ARGV:

  • ARGV1: internalLockLeaseTime = 30000 milliseconds
  • ARGV[2]: getLockName(threadId) = UUID_01:threadId_01
  • ARGV[3]: getWriteLockName(threadId) = UUID_01:threadId_01:write

Then read the lua script line by line in the code:

  1. Hget anyLock Mode Is empty when the first lock is added
  2. Mode = false, enter if logic
  3. Hset anyLock UUID_01:threadId_01 1 anyLock is a hash structure for setting the hash key and value
  4. Set {anyLock}:UUID_01:threadId_01: rwLOCK_timeout :1 1 Set a key value of string type
  5. Pexpire {anyLock}:UUID_01:threadId_01:rwlock_timeout:1 30000 Sets the expiration time of the key value
  6. Pexpire anyLock 30000 Sets the expiration time of anyLock

At this point, the data structure in Redis is:

anyLock: {
  "mode": "read"."UUID_01:threadId_01": 1
}

{anyLock}:UUID_01:threadId_01:rwlock_timeout:1  1

Copy the code
Client A adds the read lock for the second time

Client A has already added A read lock. What will happen if client A continues to add A read lock?

  1. Hget anyLock mode When mode=read, the second if judgment is entered
  2. Hincrby anyLock UUID_01:threadId_01 1 The value in the hash is increased by 1 to become 2
  3. Set {anyLock}:UUID_01:threadId_01:rwlock_timeout:2 1 Ind indicates the result of hincrby. Hincrby returns 2
  4. pexpire anyLock 30000
  5. pexpire {anyLock}:UUID_01:threadId_01:rwlock_timeout:2 30000

At this point, the data structure in Redis is:

AnyLock: {" mode ":" read ", "UUID_01:threadId_01" :2
}

{anyLock}:UUID_01:threadId_01:rwlock_timeout:1  1
{anyLock}:UUID_01:threadId_01:rwlock_timeout:2  1
Copy the code

Client B (UUID_02:threadId_02) adds the read lock for the first time

The data in redis after locking is:

anyLock: {
  "mode": "read"."UUID_01:threadId_01": 2."UUID_02:threadId_02": 1
}

{anyLock}:UUID_01:threadId_01:rwlock_timeout:1  1
{anyLock}:UUID_01:threadId_01:rwlock_timeout:2  1
{anyLock}:UUID_02:threadId_02:rwlock_timeout:1  1
Copy the code

Increment increment the value of field in the hash table key. If the key does not exist, a new hash table is created and the HINCRBY command is executed.

Redisson add write lock logic principle

RedissonWriteLock RedissonWriteLock RedissonWriteLock RedissonWriteLock

public class RedissonWriteLock extends RedissonLock implements RLock {
    @Override
    <T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        internalLockLeaseTime = unit.toMillis(leaseTime);

        return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
                            "local mode = redis.call('hget', KEYS[1], 'mode'); " +
                            "if (mode == false) then " +
                                  "redis.call('hset', KEYS[1], 'mode', 'write'); " +
                                  "redis.call('hset', KEYS[1], ARGV[2], 1); " +
                                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                                  "return nil; " +
                              "end; " +
                              "if (mode == 'write') then " +
                                  "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                                      "redis.call('hincrby', KEYS[1], ARGV[2], 1); " + 
                                      "local currentExpire = redis.call('pttl', KEYS[1]); " +
                                      "redis.call('pexpire', KEYS[1], currentExpire + ARGV[1]); " +
                                      "return nil; " +
                                  "end; " +
                                "end;" +
                                "return redis.call('pttl', KEYS[1]);", Arrays.<Object>asList(getName()), internalLockLeaseTime, getLockName(threadId)); }}Copy the code

Again, analyze the lua script execution semantics line by line as above.

Client A adds read/write and then write locks

KEYS and ARGV parameters:

  • KEYS1 = anyLock
  • ARGV1 = 30000
  • ARGV[2] = UUID_01:threadId_01:write
  1. Hget anyLock mode, where no one is locking, mode=false
  2. hset anyLock mode write
  3. hset anyLock UUID_01:threadId_01:write 1
  4. pexpire anyLock 30000

At this point, the data format in Redis is:

anyLock: {
    "mode": "write"."UUID_01:threadId_01:write": 1
}
Copy the code

Now add the lock again, directly to another if statement:

  1. hexists anyLock UUID_01:threadId_01:write
  2. hincrby anyLock UUID_01:threadId_01:write 1
  3. pexpire anyLock pttl + 30000

At this point, the data format in Redis is:

anyLock: {
    "mode": "write"."UUID_01:threadId_01:write": 2
}
Copy the code

Client A and client B first add the read lock, and client C adds the write lock

After the read lock is added, the redis data format is:

anyLock: {
  "mode": "read"."UUID_01:threadId_01": 1."UUID_02:threadId_02": 1
}

{anyLock}:UUID_01:threadId_01:rwlock_timeout:1    1
{anyLock}:UUID_02:threadId_02:rwlock_timeout:1    1
Copy the code

The parameters of client C are:

  • KEYS1 = anyLock
  • ARGV1 = 30000
  • ARGV[2] = UUID_03:threadId_03:write

The execution process is as follows:

  1. Hget anyLock mode, mode = read, the if statement will not hold
  2. PTTL anyLock, returns the remaining lifetime of an anyLock

If client C fails to lock, it will try to lock again and again

Client A first adds the write lock, and client B then adds the read lock

After the write lock is added, Redis data format is:

anyLock: {
  "mode": "write"."UUID_01:threadId_01:write": 1
}
Copy the code

The logical parameters of client B’s read lock are as follows:

  • KEYS1 = anyLock
  • KEYS[2] = {anyLock}:UUID_02:threadId_02:rwlock_timeout
  • ARGV1 = 30000 milliseconds
  • ARGV[2] = UUID_02:threadId_02
  • ARGV[3] = UUID_02:threadId_02:write

Let’s look at the locking logic:

As shown in the figure above, client B’s read lock goes to the if logic in the red box:

  1. Hget anyLock mode, mode = write Client A has added A write lock
  2. Hexists anyLock UUID_02:threadId_02:write. If this branch exists, client B can access this branch only if it has added a write lock before
  3. PTTL anyLock is displayed, causing lock failure

Client A first adds the write lock, and client A then adds the read lock

Continue with the above logic:

  1. Hget anyLock mode, mode = write Client A has added A write lock
  2. Hexists anyLock UUID_01:threadId_01:write. This key exists and the if branch can be entered
  3. Hincrby anyLock UUID_01:threadId_01 1, that is, a read lock is added
  4. set {anyLock}:UUID_01:threadId_01:rwlock_timeout:1 1,
  5. pexpire anyLock 30000
  6. pexpire {anyLock}:UUID_01:threadId_01:rwlock_timeout:1 30000

At this point, the data format in Redis is:

anyLock: {
  "mode": "write"."UUID_01:threadId_01:write": 1."UUID_01:threadId_01": 1
}

{anyLock}:UUID_01:threadId_01:rwlock_timeout:1    1
Copy the code

Client A first adds the read lock, and client A then adds the write lock

After client A adds the read lock, the data structure in Redis is:

anyLock: {
  "mode": "read"."UUID_01:threadId_01": 1
}

{anyLock}:UUID_01:threadId_01:rwlock_timeout:1  1
Copy the code

Client A then adds the write lock. The logic is as follows:

In this case, client A added the read lock first, mode=read, so the write lock cannot be added again

For the same client and thread, the write lock can be added once and then the read lock can be added successfully. By default, the read lock can be added multiple times during the write lock of the same thread

And the same client and the same thread, first added a read lock, is not allowed to be added to the write lock

conclusion

Write lock and write lock mutually exclusive logic will not be analyzed, through the analysis of some scenarios above, we can know:

  • Read locks and read locks are not mutually exclusive
  • Read and write locks are mutually exclusive
  • Write locks and write locks are mutually exclusive
  • Read, write the same client and the same thread can be reentrant
  • Write locks followed by read locks can be reentrant
  • A lock that is read before it is written cannot be reentrant

Redisson Read-write lock release principle

Redission Mechanism of release of read locks

Different clients added read locks/same client + thread multiple reentrant added read locks

For example, client A first adds the read lock, then adds the read lock again, and finally, client B adds the read lock

At this point, the data format in Redis is:

anyLock: {
  "mode": "read"."UUID_01:threadId_01": 2."UUID_02:threadId_02": 1
}

{anyLock}:UUID_01:threadId_01:rwlock_timeout:1		1
{anyLock}:UUID_01:threadId_01:rwlock_timeout:2		1
{anyLock}:UUID_02:threadId_02:rwlock_timeout:1		1
Copy the code

Let’s take a look at the core code for releasing locks:

public class RedissonReadLock extends RedissonLock implements RLock {
    @Override
    protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        String timeoutPrefix = getReadWriteTimeoutNamePrefix(threadId);
        String keyPrefix = getKeyPrefix(threadId, timeoutPrefix);

        return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                "local mode = redis.call('hget', KEYS[1], 'mode'); " +
                "if (mode == false) then " +
                    "redis.call('publish', KEYS[2], ARGV[1]); " +
                    "return 1; " +
                "end; " +
                "local lockExists = redis.call('hexists', KEYS[1], ARGV[2]); " +
                "if (lockExists == 0) then " +
                    "return nil;" +
                "end; " +
                    
                "local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1); " + 
                "if (counter == 0) then " +
                    "redis.call('hdel', KEYS[1], ARGV[2]); " + 
                "end;" +
                "redis.call('del', KEYS[3] .. ':'.. (counter+1)); " +
                
                "if (redis.call('hlen', KEYS[1]) > 1) then " +
                    "local maxRemainTime = -3; " + 
                    "local keys = redis.call('hkeys', KEYS[1]); " + 
                    "for n, key in ipairs(keys) do " + 
                        "counter = tonumber(redis.call('hget', KEYS[1], key)); " + 
                        "if type(counter) == 'number' then " + 
                            "for i=counter, 1, -1 do " + 
                                "local remainTime = redis.call('pttl', KEYS[4] .. ':'.. key .. ':rwlock_timeout:' .. i); " + 
                                "maxRemainTime = math.max(remainTime, maxRemainTime);" + 
                            "end; " + 
                        "end; " + 
                    "end; " +
                            
                    "if maxRemainTime > 0 then " +
                        "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; ", Arrays.<Object>asList(getName(), getChannelName(), timeoutPrefix, keyPrefix), LockPubSub.unlockMessage, getLockName(threadId)); }}Copy the code

Client A releases the lock: the corresponding KEYS and ARGV arguments are:

  • KEYS1 = anyLock

  • KEYS[2] = redisson_rwlock:{anyLock}

  • KEYS[3] = {anyLock}:UUID_01:threadId_01:rwlock_timeout

  • KEYS[4] = {anyLock}

  • ARGV1 = 0

  • ARGV[2] = UUID_01:threadId_01

Next, execute the action:

  1. Hget anyLock mode, mode = read
  2. Hexists anyLock UUID_01:threadId_01. This parameter must exist because client A has A read lock
  3. Hincrby anyLock UUID_01: threadid_01-1, decrease the lock count for this client by 1, now it is 1, counter = 1
  4. Del {anyLock}:UUID_01:threadId_01:rwlock_timeout:2, a timeout key is deleted

At this point, the data structure in Redis is:

anyLock: {
  "mode": "read"."UUID_01:threadId_01": 1."UUID_02:threadId_02": 1
}

{anyLock}:UUID_01:threadId_01:rwlock_timeout:1    1
{anyLock}:UUID_02:threadId_02:rwlock_timeout:1    1
Copy the code

The specific logic is shown in the figure below:

  1. Hlen anyLock > 1, that is, there is more than one hash element
  2. PTTL {anyLock}:UUID_01:threadId_01:rwlock_timeout:1, the number of milliseconds left to obtain the timeout key, for example, the remaining time of the key is 20000 milliseconds

The meaning of this for loop is that the maximum remaining lifetime of all timeout keys is obtained, assuming that the maximum remaining lifetime is 25,000 milliseconds

Client A proceeds to release the lock:

At this point, the execution process of client A will be the same as above, and the data structure in Redis after execution is as follows:

anyLock: {
  "mode": "read"."UUID_02:threadId_02": 1
}

{anyLock}:UUID_02:threadId_02:rwlock_timeout:1    1
Copy the code

“Redis. Call (‘hdel’, KEYS[1], ARGV[2]); “

Client B proceeds to release the lock:

Client B process is the same as above, after execution will delete anyLock key

The same client/program is locked first by writing and then by reading

This situation has been analyzed above. After the operation, the data structure in Redis is:

anyLock: {
  "mode": "write"."UUID_01:threadId_01:write": 1."UUID_01:threadId_01": 1
}

{anyLock}:UUID_01:threadId_01:rwlock_timeout:1    1
Copy the code

Client A releases the read lock:

  1. Hincrby anyLock UUID_01: threadid_01-1, decrease the lock count for this client by 1, now it is 1, counter = 0
  2. Hdel anyLock UUID_01:threadId_01: deletes the lock record of client A from the hash data structure
  3. Del {anyLock}:UUID_01:threadId_01:rwlock_timeout:1, a timeout key is deleted

At this point, the data in Redis becomes:

anyLock: {
  "mode": "write"."UUID_01:threadId_01:write": 1
}
Copy the code

Redisson write lock release principle

Let’s look at the core logic of write lock release:

public class RedissonWriteLock extends RedissonLock implements RLock {
    @Override
    protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                "local mode = redis.call('hget', KEYS[1], 'mode'); " +
                "if (mode == false) then " +
                    "redis.call('publish', KEYS[2], ARGV[1]); " +
                    "return 1; " +
                "end;" +
                "if (mode == 'write') then " +
                    "local lockExists = redis.call('hexists', KEYS[1], ARGV[3]); " +
                    "if (lockExists == 0) then " +
                        "return nil;" +
                    "else " +
                        "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
                        "if (counter > 0) then " +
                            "redis.call('pexpire', KEYS[1], ARGV[2]); " +
                            "return 0; " +
                        "else " +
                            "redis.call('hdel', KEYS[1], ARGV[3]); " +
                            "if (redis.call('hlen', KEYS[1]) == 1) then " +
                                "redis.call('del', KEYS[1]); " +
                                "redis.call('publish', KEYS[2], ARGV[1]); " + 
                            "else " +
                                // has unlocked read-locks
                                "redis.call('hset', KEYS[1], 'mode', 'read'); " +
                            "end; " +
                            "return 1; "+
                        "end; " +
                    "end; " +
                "end; "
                + "return nil;", Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId)); }}Copy the code
The same client can re-enter the write lock multiple times or the same client can re-enter the write lock and then the read lock

Client A adds two write locks to release:

At this point, the data in Redis is:

anyLock: {
  "mode": "write"."UUID_01:threadId_01:write": 2."UUID_01:threadId_01": 1
}

{anyLock}:UUID_01:threadId_01:rwlock_timeout:1    1
Copy the code

Client A releases lock KEYS and ARGV arguments:

  • KEYS1 = anyLock

  • KEYS[2] = redisson_rwlock:{anyLock}

  • ARGV1 = 0

  • ARGV[2] = 30000

  • ARGV[3] = UUID_01:threadId_01:write

Direct analysis of Lua code:

  1. If mode=write and -1 is used for hincrby, count=1
  2. If count>0, use pexpire and then return 0
  3. Then client A releases the write lock. Count =0
  4. hdel anyLock UUID_01:threadId_01:write

Data in Redis at this point:

anyLock: {
  "mode": "write"."UUID_01:threadId_01": 1
}

{anyLock}:UUID_01:threadId_01:rwlock_timeout:1    1
Copy the code

Hset anyLock mode read hset anyLock mode read hset anyLock mode read hset anyLock mode read hset anyLock mode read

The final Redis data is:

anyLock: {
  "mode": "read"."UUID_01:threadId_01": 1
}

{anyLock}:UUID_01:threadId_01:rwlock_timeout:1    1
Copy the code

conclusion

Redisson has also updated several articles, staying at home and studying Redisson during the pandemic.

Read and write lock this part of the content is really a lot, this article is very long, if you learn this article best read with the source code, the follow-up will continue to update Redisson related content, if there is an incorrect place, welcome to correct!

statement

This article first from my public number: a flower is not romantic, if reprinted please indicate the source!

Interested partners can pay attention to personal public account: a flower is not romantic