Redoing is always easier than reinventing

Recently, I am working on a project to integrate the implementation system of another company (hereinafter referred to as the old system) into the system of my own company (hereinafter referred to as the new system) in a complete way. In this process, I need to complete the functions implemented by the other company in my own system.

There are still a number of existing merchants in the old system. In order not to affect the experience of existing merchants, the external interface provided by the new system must be the same as before. Finally, after the complete system switchover, the function only runs in the new system, which requires that the data of the old system also needs to be completely migrated to the new system.

Of course, I had expectations before doing this project and thought it would be difficult, but I didn’t expect it to be that difficult. Originally felt that the schedule is half a year, the time is quite comfortable, now feel is a pit, but also have to fill in the pit a little bit.

Ah, say more is the tear, not ridicule, wait until the next time finish to give you real experience.

Back to the body, the last article Redis distributed lock, we based on Redis to implement a distributed lock. The basic function of this distributed lock is ok, but it lacks reentrant feature, so in this article little black brother will take you to implement reentrant distributed lock.

This article will cover the following contents:

  • reentrant
  • Implementation scheme based on ThreadLocal
  • Implementation scheme based on Redis Hash

Like it before you look at it, make it a habit. Wechat search “program tongshi”, attention is done ~

reentrant

Speaking of reentrant locks, let’s first take a look at a wiki explanation of reentrant locks:

A program or subroutine is reentrant if it can “be interrupted at any time and the operating system schedules the execution of another piece of code that calls the subroutine without error.” That is, while the subroutine is running, the thread of execution can come in and execute it again, still getting the results expected at design time. Unlike thread-safety, where multiple threads execute concurrently, reentrant emphasizes that it is still safe to re-enter the same subroutine while executing on a single thread.

When a thread succeeds in acquiring the lock after executing a piece of code and continues to execute, reentrancy ensures that the thread can continue to execute when it encounters the locked code again. However, non-reentrancy means that it needs to wait for the lock to be released and successfully acquire the lock again before continuing to execute.

Reentrant can be explained in Java code:

public synchronized void a(a) {
    b();
}

public synchronized void b(a) {
    // pass
}
Copy the code

Suppose thread X continues to execute method B after method A acquires the lock. If it is not reentrant at this point, the thread must wait for the lock to be released and fight for the lock again.

The lock is owned by thread X, but it has to wait for itself to release the lock, and then grab it. It seems strange that I release myself

Reentrancy solves this awkward problem. When a thread has a lock and encounters a lock method in the future, it simply increments the lock count by one before executing the method logic. After exiting the lock method, the lock count is reduced by 1. When the lock count is 0, the lock is released.

As you can see, the biggest feature of reentrant locking is counting, counting the number of times the lock is held. So when a reentrant lock needs to be implemented in a distributed environment, we also need to count the number of locks.

There are two ways to implement distributed reentrant locks:

  • Implementation scheme based on ThreadLocal
  • Implementation scheme based on Redis Hash

First let’s take a look at the ThreadLocal-based implementation.

Implementation scheme based on ThreadLocal

implementation

ThreadLocal in Java allows each thread to have its own instance copy, and we can use this feature to measure thread reentrant times.

Next we define a global variable of ThreadLocal, LOCKS, which stores Map instance variables in memory.

private static ThreadLocal<Map<String, Integer>> LOCKS = ThreadLocal.withInitial(HashMap::new);
Copy the code

Each thread can obtain its own Map instance from ThreadLocal, where key stores the name of the lock and value stores the number of reentrant times of the lock.

The lock code is as follows:

/** * reentrant lock **@paramLockName specifies the lockName, which indicates the critical resource to be contended for@paramRequest unique identifier. The uUID can be used to determine whether reentrant * is allowed@paramLeaseTime Lock release time *@paramUnit Unit of lock release time *@return* /
public Boolean tryLock(String lockName, String request, long leaseTime, TimeUnit unit) {
    Map<String, Integer> counts = LOCKS.get();
    if (counts.containsKey(lockName)) {
        counts.put(lockName, counts.get(lockName) + 1);
        return true;
    } else {
        if (redisLock.tryLock(lockName, request, leaseTime, unit)) {
            counts.put(lockName, 1);
            return true; }}return false;
}
Copy the code

Ps: redisLock#tryLock distributed lock implemented for the previous article.

Because the public chain can not jump directly, pay attention to the “program”, reply to the distributed lock to obtain the source code.

The lock method first determines whether the current thread already owns the lock. If so, it directly increases the reentrant times of the lock by 1.

If you do not already own the lock, try to add the lock to Redis. After the lock is successfully added, add 1 to the reentrant times.

The lock release code is as follows:

/** * Unlocks the thread pool **@param lockName
 * @param request
 */
public void unlock(String lockName, String request) {
    Map<String, Integer> counts = LOCKS.get();
    if (counts.getOrDefault(lockName, 0) < =1) {
        counts.remove(lockName);
        Boolean result = redisLock.unlock(lockName, request);
        if(! result) {throw new IllegalMonitorStateException("attempt to unlock lock, not locked by lockName:+" + lockName + " with request: "+ request); }}else {
        counts.put(lockName, counts.get(lockName) - 1); }}Copy the code

When the lock is released, the number of reentrant times is determined first. If the number is greater than 1, it indicates that the lock is owned by the thread. Therefore, you can directly reduce the number of reentrant times by 1.

If the current reentrant count is less than or equal to 1, remove the key corresponding to the lock in the Map first, and then release the lock in Redis.

It should be noted that if the lock is not owned by the thread, the lock can be unlocked directly, and the reentrant times are less than or equal to 1. This time, the lock may not be unlocked directly.

When using ThreadLocal, remember to clean up the internal storage instance variables to prevent memory leaks and contextual data string usage.

Next time, let’s talk about some recent bugs with ThreadLocal.

Issues related to

While it’s really simple and efficient to use ThreadLocal to locally record reentrant counts, there are some problems.

Expiration date

As you can see from the above locking code, reentrant locking only increments the local count by one. This can lead to a situation where Redis has expired to release the lock due to a long business execution.

However, when the lock is re-entered, the local data still exists, so the lock is still held, which is not consistent with the actual situation.

If you want to increase the expiration time locally, you also need to consider the consistency of the local and Redis expiration times, and the code becomes very complicated.

Different threads/processes can reentrant problems

In a narrow sense, reentrancy should only be for the same thread, but real business may require that different application threads can reentrant the same lock.

However, ThreadLocal’s solution only supports the same thread reentrant, and cannot solve the problem of reentrant between different threads/processes.

Different thread/process reentrant problems need to be solved using the following Redis Hash scheme.

Reentrant locking is based on Redis Hash

implementation

ThreadLocal uses a Map to count the number of reentrant locks, while Redis also provides Hash data structures that store key-value pairs. So we can use the Redis Hash to store the lock reentrant count, and then use the Lua script to determine the logic.

Lua lock script is as follows:

---- 1 indicates true
---- 0 stands for false

if (redis.call('exists', KEYS[1= =])0) then
    redis.call('hincrby', KEYS[1], ARGV[2].1);
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return 1;
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 1;
end ;
return 0;
Copy the code

If the KEYS: [lock], ARGV [1000, uuid]

Don’t be afraid if you are not familiar with Lua, the logic above is relatively simple.

The lock code first uses the Redis exists command to determine whether the current lock exists.

If the lock does not exist, use hincrby to create a hash table with the lock key, initialize the hash table with the key UUID to 0, then add 1 again, and set the expiration time.

If the current lock exists, run the hEXISTS command to check whether the uUID key exists in the hash table corresponding to the current lock. If yes, run the hincrby command to add 1 and set the expiration time again.

Finally, if the above two logic does not match, directly return.

The locking code is as follows:

// Initialize the code

String lockLuaScript = IOUtils.toString(ResourceUtils.getURL("classpath:lock.lua").openStream(), Charsets.UTF_8);
lockScript = new DefaultRedisScript<>(lockLuaScript, Boolean.class);

/** * reentrant lock **@paramLockName specifies the lockName, which indicates the critical resource to be contended for@paramRequest unique identifier. The uUID can be used to determine whether reentrant * is allowed@paramLeaseTime Lock release time *@paramUnit Unit of lock release time *@return* /
public Boolean tryLock(String lockName, String request, long leaseTime, TimeUnit unit) {
    long internalLockLeaseTime = unit.toMillis(leaseTime);
    return stringRedisTemplate.execute(lockScript, Lists.newArrayList(lockName), String.valueOf(internalLockLeaseTime), request);
}
Copy the code

Spring – the Boot 2.2.7. RELEASE

As long as you understand the Lua script locking logic, the Java code implementation is quite simple, directly using the StringRedisTemplate provided by SpringBoot.

The unlocked Lua script is as follows:

-- Determines whether the hash set reentrant key is equal to 0
-- If the value is 0, the reentrant key does not exist
if (redis.call('hexists', KEYS[1], ARGV[1= =])0) then
    return nil;
end ;
-- Counts the current reentrant count
local counter = redis.call('hincrby', KEYS[1], ARGV[1].- 1);
-- If the value is less than or equal to 0, the account can be unlocked
if (counter > 0) then
    return 0;
else
    redis.call('del', KEYS[1]);
    return 1;
end ;
return nil;
Copy the code

First, hEXISTS is used to determine whether the Redis Hash table stores the given field.

If the Hash table corresponding to lock does not exist, or the Hash table does not have the uuid key, return nil.

If exists, it means that the current lock is held by the lock. First, use hincrby to reduce the reentrant times by 1, and then determine the number of reentrant times after calculation. If the number is less than or equal to 0, use del to delete the lock.

The Java code to unlock is as follows:

// Initialize code:


String unlockLuaScript = IOUtils.toString(ResourceUtils.getURL("classpath:unlock.lua").openStream(), Charsets.UTF_8);
unlockScript = new DefaultRedisScript<>(unlockLuaScript, Long.class);

<br> * 0: Indicates that the lock is not released, and the number of reentrant times is reduced by 1 <br> * nil: <br> * <p> * If using DefaultRedisScript<Boolean>, due to spring-data-redis eval type conversion, <br> * When Redis returns Nil bulk, this will be converted to false by default, which will affect the unlock semantics, so the following is used: <br> * DefaultRedisScript<Long> * <p> * <br> * JedisScriptReturnConverter<br> * *@paramLockName lockName *@paramRequest unique identifier, which can be uUID *@throwsIllegalMonitorStateException unlock before, please lock. Unlocking a lock will throw the error */
public void unlock(String lockName, String request) {
    Long result = stringRedisTemplate.execute(unlockScript, Lists.newArrayList(lockName), request);
    // If no value is returned, another thread is trying to unlock
    if (result == null) {
        throw new IllegalMonitorStateException("attempt to unlock lock, not locked by lockName:+" + lockName + " with request: "+ request); }}Copy the code

The unlock code executes similarly to the lock, except that the return type of the unlock execution is Long. The reason Boolean is not used here is because the three return values in the unlock Lua script have the following meanings:

  • 1 indicates that the lock is unlocked successfully and released
  • 0 means the number of reentrants has been subtracted by 1
  • nullIndicates that another thread attempts to unlock the account, but the account fails to be unlocked

If the return value is Boolean, spring-data-redis will convert null to false, which will affect our logic, so we have to use Long.

The following code from JedisScriptReturnConverter:

Issues related to

Spring-data-redis is a low version problem

If spring-boot uses Jedis as the connection client and Redis Cluster mode is used, the spring-boot-starter-data-redis version 2.1.9 or later must be used. Otherwise, the following command output is displayed:

org.springframework.dao.InvalidDataAccessApiUsageException: EvalSha is not supported in cluster environment.
Copy the code

If your current application cannot upgrade spring-data-redis, you can use the following method to execute lua scripts directly using native Jedis connections.

Take locking code as an example:

public boolean tryLock(String lockName, String reentrantKey, long leaseTime, TimeUnit unit) {
    long internalLockLeaseTime = unit.toMillis(leaseTime);
    Boolean result = stringRedisTemplate.execute((RedisCallback<Boolean>) connection -> {
        Object innerResult = eval(connection.getNativeConnection(), lockScript, Lists.newArrayList(lockName), Lists.newArrayList(String.valueOf(internalLockLeaseTime), reentrantKey));
        return convert(innerResult);
    });
    return result;
}

private Object eval(Object nativeConnection, RedisScript redisScript, final List<String> keys, final List<String> args) {

    Object innerResult = null;
    // Cluster mode and single point mode execute scripts in the same way, but there is no common interface, so they have to be executed separately
    / / cluster
    if (nativeConnection instanceof JedisCluster) {
        innerResult = evalByCluster((JedisCluster) nativeConnection, redisScript, keys, args);
    }
    / / a single point
    else if (nativeConnection instanceof Jedis) {
        innerResult = evalBySingle((Jedis) nativeConnection, redisScript, keys, args);
    }
    return innerResult;
}
Copy the code

Data type conversion problems

If you execute a Lua script using a Jedis native connection, you might run into a data-type conversion pit again.

As you can see, Jedis#eval returns Object. We need to convert the Object based on the value returned by the Lua script. This involves converting Lua data types to Redis data types.

The following main we will talk about Lua data conversion Redis rules in a few easier to tread pit:

1. Data type conversion between Lua Number and Redis

The number type in Lua is a double-precision floating-point number, but Redis only supports integers, so this conversion process will discard decimal places.

2, Lua Boolean and Redis type conversion

This is a bit trickier. There is no Boolean type in Redis, so true in Lua will be converted to the Redis integer 1. In Lua false does not convert an integer, but returns null to the client.

3, Lua nil and Redis type conversion

Lua nil can be considered a null value, which can be equivalent to null in Java. In Lua if nil appears in a conditional expression, it is treated as false.

So Lua nil will also be null returned to the client.

Other conversion rules are relatively simple. For details, please refer to:

Doc.redisfans.com/script/eval…

conclusion

The key of reentrant distributed lock is the count of lock reentrant. This paper mainly presents two solutions, one based on ThreadLocal, which is simple to implement and efficient to run. But to deal with lock expiration, the code implementation is more complicated.

The other is to use Redis Hash data structure to solve the problem of ThreadLocal, but the code is a bit more difficult to implement, requiring familiarity with Lua scripts and Redis commands. In addition to using Spring-data-redis and other operations redis inadvertently encountered various problems.

help

www.sofastack.tech/blog/sofa-j…

Tech.meituan.com/2016/09/29/…

One last word (for attention)

After reading the article, brothers and sisters point a thumb-up, zhou is really super tired, do not feel and write for two days, refused white piao, to point positive feedback bai ~.

Finally, thank you for your reading. It is difficult to avoid mistakes. If you find any mistakes, please leave a comment. If you don’t understand anything after reading this article, please add me to learn from each other and grow up together

Finally, thank you for your support

Finally, the important thing to say again ~

Come follow me ~ come follow me ~ come follow me ~

Welcome to pay attention to my public account: procedures to get daily dry goods push. If you are interested in my topics, you can also follow my blog: studyidea.cn