当前位置:网站首页>Redis thread level reentrant distributed lock (different unique IDs can be locked cyclically)

Redis thread level reentrant distributed lock (different unique IDs can be locked cyclically)

2022-06-25 20:05:00 gwddszrrjpdr

Examples of scenarios

Because the real scenario involves specific business , Here I will give an inappropriate example to roughly compare the problems I encountered .
such as “ I go home every day - Turn on all smart devices in your home ” This matter , It can be divided into two scenarios , One is that I go home from work , Then turn on the water heater 、 Curtain and other intelligent devices ; One is that I go out on business , Go home from somewhere else and you need to turn on all the devices . The trigger action of these two scenarios is to go home , But the realization of going home is different , In our old code , These two scenarios are written as two interfaces . The problem is , If I add a new smart device in my family Air conditioner , I will add... To both interfaces at the same time “ Turn on the air conditioning ” This code , Business scenarios are increasing 、 New students are not familiar with the code , This code forgot to add , Over the years, it has led to the actions that should have been done at home , The two interfaces make a difference , Maybe I turned on the air conditioner when I came home from the company , I didn't turn on the air conditioner when I came home from outside orz.
So I recently made an optimization , Converge these two interfaces , Because there are many upstream calling systems , The calling posture of the interface cannot be changed , Then we can only take the internal action Unite , So I put “ Go home from outside ” The internal implementation in the interface is directly changed to call “ Go home from the company ” The implementation of the . It looks very simple , Just evaluate the validation logic of the input parameter and its compatibility 、 Business scenario coverage ( Although it is not so simple to do ) You can send it to the test environment . That's what I thought .

Problem generation

The implementation logic of these two interfaces opens the database transaction , As long as the propagation feature is configured , That's not a problem . The problem is that these two interfaces avoid concurrency , Also used redis Distributed lock , such as “ Turn on the smart device ” This matter , To prevent my parents and I from operating at the same time , Locks are also required , For the same device , For example, the air conditioner in the living room , Just lock it as the only sign for my family ( The only equipment in the world 69 code or “ Living room air conditioning ”) Can tell my parents that I am using this resource .
problem 1: Both methods are right 69 Code locking , Method 1 Transfer in method 2 after , Find the same 69 The code is locked , Code can't go down .

solve the problem

Because the same resource is locked and the lock is not reentrant . Digression , Because our code Spring and redis Lower version , It is not possible to set the value and expiration time at the same time , therefore redis All locks pass through StringRedisTemplate + lua Script to implement non reentrant distributed locks . The operations of throwing exceptions and logging are omitted here .

    // Business entrance , Specific business realization needs to be realized Action Of execute()
    public static <T> T doAction(Action<T> action, StringRedisTemplate redisTemplate, String key,String value, int timeout) {
        RedisLock redisLock = new RedisLock(redisTemplate, key, value, timeout, TimeUnit.SECONDS);
        try {
            if (redisLock.tryLockWithLua()) {
                return action.execute();
            } else {
                // Throw exceptions , this 69 The code has been locked  ;
            }
        } finally {
            if (redisLock.isLock()) {
                redisLock.unLockWithLua();
            }
        }
    }
    
	// Specific locking implementation 
    public boolean tryLockWithLua() {
            RedisCallback<String> callback = new RedisCallback<String>() {
                @Override
                public String doInRedis(RedisConnection connection) throws DataAccessException {
                    Object nativeConnection = connection.getNativeConnection();
                    return ((Jedis) nativeConnection).eval( Lock lua Script , 1, key, value, expireTimeStr).toString();
                }
            };
            String success = redisTemplate.execute(callback);
            if (SUCCESS.equals(success)) {
                isLock = true;
                return true;
            }
            return false;
    }

	// Specific unlocking implementation 
	    public void unLockWithLua() {
        if (isLock) {
            RedisCallback<String> callback = new RedisCallback<String>() {
                @Override
                public String doInRedis(RedisConnection connection) throws DataAccessException {
                    Object nativeConnection = connection.getNativeConnection();
                    return ((Jedis) nativeConnection).eval( Unlock lua Script , 1, key, value).toString();
                }
            };
            String success = redisTemplate.execute(callback);
            if (SUCCESS.equals(success)) {
                isLock = false;
            }
        }
    }
	//lua Script 
	 Lock lua Script  = "if redis.call('setNx',KEYS[1],ARGV[1])==1 then\n" +
                "return redis.call('expire',KEYS[1],ARGV[2])\n" +
                "else\n" +
                "return 0\n" +
                "end\n";

     Unlock lua Script  = "if redis.call('get', KEYS[1]) == ARGV[1] " +
                "then return redis.call('del', KEYS[1]) else return 0 end";

Then according to the existing locking code , Write reentrant distributed locks . I read this classmate's article first https://blog.csdn.net/u014634309/article/details/106755403,hash The method must not work , Because we want to control that the current thread can re-enter the lock , That's all ThreadLocal You can choose , The student's article is also very clear ,ThreadLocal The possible problem with implementation is the expiration time , It is impossible to determine whether the data stored in your current memory is redis Is it out of date , therefore ThreadLocal Put only key and count Definitely not enough . So let's expand one content, because value Time stamp information can be carried in , Then put value Also put in , Before locking, I judge that there is value and redis The same in key Pick up to value equally , You can judge that it has not expired , So I wrote . edition 1:

    private static final ThreadLocal<ThreadLocalReentrantLockContent> threadLocal = new ThreadLocal<>();


    @Data
    @Builder
    private static class ThreadLocalReentrantLockContent{
        private String key;
        private String value;
        private Integer count;
    }


	public static <T> T doActionUseReentrantLock(Action<T> action, StringRedisTemplate redisTemplate, String key, String value, int timeout) {
        RedisLock redisLock = new RedisLock(redisTemplate, key, value, timeout, TimeUnit.SECONDS);
        // Get the lock held by the current thread 
        ThreadLocalReentrantLockContent content = threadLocal.get();
        try {
            if (content == null) {
                content = ThreadLocalReentrantLockContent.builder().key("").value("").count(0).build();
                threadLocal.set(content);
            }
            // Reentrant 
            //1. content It needs to be preserved in the library key And judge the incoming key Same as in memory , Compatible with a thread lock first 69 code , Lock after handling “ Living room air conditioning ” The situation of 
            //2.  In the memory value and redis In the same way   Ensure that the lock held by the current thread does not time out (value It carries time information )
            //3.  Judge the timeout time , Can don't 
            if (StringUtils.isNotBlank(content.getKey())
                    && content.getKey().equals(key)
                    && StringUtils.isNotBlank(content.getValue())
                    && content.getValue().equals(redisTemplate.opsForValue().get(key))
                    && redisTemplate.getExpire(key) > 0) {
                content.count++;
            } else {
                // Lock 
                if (redisLock.tryLockWithLua()) {
                    content.setKey(key);
                    content.setValue(value);
                    content.setCount(1);
                } else {
                    // Throw exceptions , this key Locked 
                }
            }
            // perform 
            return action.execute();
        } finally {
            content = threadLocal.get();
            if (null != content) {
                // Release the lock 
                content.count--;
                if (redisLock.isLock() && 0 == content.getCount()) {
                    threadLocal.remove();
                    redisLock.unLockWithLua();
                }
            }
        }
    }

problem 2: So here comes the question , This posture can only achieve the first lock 69 code , Unlock this after processing key Then lock it “ Living room air conditioning ” The situation of , If the code is written abnormally in the future , Existence needs to be locked first 69 code , The next operation is direct locking “ Living room air conditioning ” The situation of , Then the code is incompatible . I discussed with my partner , Using nesting , stay content One inside content, Used to save the context of the previous lock , Is it possible to achieve , So we did . edition 2:

    @Data
    @Builder
    private static class ThreadLocalReentrantLockContent{
        private String key;
        private String value;
        private Integer count;

        /**
         *  Used for front and rear locking key A different situation , Keep the previous content
         */
        private ThreadLocalReentrantLockContent lastContent;
    }


	public static <T> T doActionUseReentrantLock(Action<T> action, StringRedisTemplate redisTemplate, String key, String value, int timeout) {
        RedisLock redisLock = new RedisLock(redisTemplate, key, value, timeout, TimeUnit.SECONDS);
        // Get the lock held by the current thread 
        ThreadLocalReentrantLockContent content = threadLocal.get();
        try {
            // Reentrant 
            if (content != null
                    && content.getKey().equals(key)
                    && content.getValue().equals(redisTemplate.opsForValue().get(key))
                    && redisTemplate.getExpire(key) > 0) {
                content.count++;
            } else {
                // Lock 
                if (redisLock.tryLockWithLua()) {
                    ThreadLocalReentrantLockContent newContent = ThreadLocalReentrantLockContent.builder().key(key).value(value).count(1).build();
                    newContent.setLastContent(content);
                    threadLocal.set(newContent);
                } else {
                   // Throw exceptions , Locked 
                }
            }
            // perform 
            return action.execute();
        } finally {
            content = threadLocal.get();
            if (null != content) {
                // Release the lock 
                content.count--;
                if (redisLock.isLock() && 0 == content.getCount()) {
                    content = content.lastContent;
                    if (content == null) {
                        //  Currently, this is the outermost layer of locking logic 
                        threadLocal.remove();
                    } else {
                        threadLocal.set(content);
                    }
                    redisLock.unLockWithLua();
                }
            }
        }
    }

It looks perfect , But in debug I found a problem , I am re entering the number of times ++ And reentry times – When , All you get is ThreadLocal The latest in content, If it is locked first 69 code , Relock “ Living room air conditioning ”, Relock 69 code What about this situation ? The current code will report an error in the third stage , He didn't find 69 Code lock and put count++, But try to give 69 Code locking , This time will certainly be abnormal .
that , This complex and deformed situation must be recursive . Although this scenario should not happen in my career , But it's all written here , Just finish it . So I tried again . edition 3:

 /**
     *  Reentrant lock 
     * 1.  Can be realized key Different locking logic   For example, lock first 69 Code and lock again “ Living room air conditioning ”
     * 2.  Can be realized key Same lock reentry   For example, lock first 69 Code and lock again 69 code 
     * 3.  Multiple cycle locking can be realized   For example, lock first 69 code , Relock “ Living room air conditioning ”, Relock 69 code 
     *
     * @param Action
     * @param redisTemplate
     * @param key
     * @param value
     * @param timeout
     * @param <T>
     * @return
     */
    public static <T> T doActionUseReentrantLock(Action<T> action, StringRedisTemplate redisTemplate, String key, String value, int timeout) {
        RedisLock redisLock = new RedisLock(redisTemplate, key, value, timeout, TimeUnit.SECONDS);
        //threadLocal get To the latest content, It should be stored temporarily , Because we have to put count The update of is put into memory 
        ThreadLocalReentrantLockContent latestContent = threadLocal.get();
        try {
            // Reentrant ,curContent For the current key The use of content
            ThreadLocalReentrantLockContent curContent = checkAndGetReentrant(latestContent, key, redisTemplate.opsForValue().get(key));
            if (null != curContent) {
                curContent.count++;
                // Reset threadLocal Medium count
                threadLocal.set(latestContent);
            } else {
                // Lock 
                if (redisLock.tryLockWithLua()) {
                    ThreadLocalReentrantLockContent newContent = ThreadLocalReentrantLockContent.builder().key(key).value(value).count(1).build();
                    newContent.setLastContent(latestContent);
                    threadLocal.set(newContent);
                } else {
                    // Throw exceptions , Locked 
                }
            }
            // perform 
            return action.execute();
        } finally {
            // If there is a lock 69 code -“ Living room air conditioning ”-69 code   The situation of , The following two content inequality 
            latestContent = threadLocal.get();
            ThreadLocalReentrantLockContent curContent = checkAndGetReentrant(latestContent, key, redisTemplate.opsForValue().get(key));
            if (null != curContent) {
                // Release the lock 
                curContent.count--;
                threadLocal.set(latestContent);
                if (redisLock.isLock() && 0 == curContent.getCount()) {
                    if (curContent.lastContent == null) {
                        //  Currently, this is the outermost layer of locking logic 
                        threadLocal.remove();
                    } else {
                        threadLocal.set(curContent.lastContent);
                    }
                    redisLock.unLockWithLua();
                }
            }
        }
    }


	    /**
     *  Recursively check for reentrancy , Compatible with lock first 69 code   Relock “ Living room air conditioning ”  Relock 69 code … The logic of 
     *
     * @param content
     * @param curKey
     * @param redisValue
     * @return  Used in the current round content
     */
    private static ThreadLocalReentrantLockContent checkAndGetReentrant(ThreadLocalReentrantLockContent content, String curKey, String redisValue) {
        // First lock 
        if (null == content) {
            return null;
        }
        // And last time key Same reentry 
        if (content.getKey().equals(curKey)
                && content.getValue().equals(redisValue)) {
            return content;
        } else {
            // Recursive reentry 
            if (null != content.getLastContent()) {
                content = content.getLastContent();
                return checkAndGetReentrant(content, curKey, redisValue);
            } else {
                // Non reentrant locking 
                return null;
            }
        }
    }

complete ! This set of code can realize various postures , Different and unique id The situation of locking in a different way . If the actual situation is not so complicated , So the version 1 You can fully realize your needs !

原网站

版权声明
本文为[gwddszrrjpdr]所创,转载请带上原文链接,感谢
https://yzsam.com/2022/02/202202190508362007.html