Reids combat - distributed lock optimization (Lua script)

1 Problems based on Redis distributed locks

Let's take a look at the previous implementation of distributed locks.

This Redis-based distributed lock still has a problem, that is, the problem of accidentally deleting locks

To put it simply, when the first thread, that is, thread 1, gets the lock, but due to its complex business, it causes blocking, and the lock is automatically released after the timeout period set by the lock is exceeded . At this time, thread 2 came in and got the lock, but on the way of thread 2 executing the business, thread 1 completed the business and released the lock actively, and because our logic of releasing the lock is to directly delete the key, this leads to thread The lock of 2 was deleted by mistake .

 

This leads to thread safety issues.

Solution: When each thread wants to release the lock, actively judge the thread ID stored in reids to determine whether it is its own lock. If not, it cannot be deleted.

Code:

package com.hmdp.utils;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.lang.UUID;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.util.CollectionUtils;

import java.util.Collections;
import java.util.concurrent.TimeUnit;

/**
 * @Author 华子
 * @Date 2022/12/3 20:53
 * @Version 1.0
 */
public class SimpleRedisLock implements ILock {

    //Redis
    private StringRedisTemplate stringRedisTemplate;
    //业务名称,也就是锁的名称
    private String name;
    public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.name = name;
    }

    //key的前缀
    private static final String KEY_PREFIX = "lock:";
    //线程标识的前缀
    private static final String ID_PREFIX = UUID.randomUUID().toString(true);


    @Override
    public boolean tryLock(long timeoutSec) {
        //获取线程id,当作set的value
        String threadId = Thread.currentThread().getId()+ID_PREFIX;

        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }


    //释放锁
    @Override
    public void unlock() {
        String threadId = Thread.currentThread().getId()+ID_PREFIX;
        String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
        if (threadId.equals(id)){
            //删除key
            stringRedisTemplate.delete(KEY_PREFIX+name);
        }
    }
}

 2 Use of Lua script

 The above code will still have the problem of accidentally deleting locks in extreme cases .

Imagine this situation:

When the thread finishes judging the thread ID, it finds that it is its own id, and when it is about to release the lock, blocking occurs. Then the lock timeout expired, other threads acquired the lock, and the problem of accidental deletion occurred again.

So, how to solve this problem?

Let's think about it this way:

That is, we integrate the judging id and deleting the id into one code, let him execute it once, instead of dividing it into two, isn't that all right?

Here we will use the script in our Redis: Lua.

Redis provides the Lua scripting function to write multiple Redis commands in one script to ensure the atomicity of multiple commands execution. Lua is a programming language. For its basic grammar, you can refer to the website: Lua Tutorial | Novice Tutorial

Here we focus on the call function provided by Redis , the syntax is as follows:

For example, if we want to execute set name jack , the script is as follows:

 -- 执行reids命令
redis.call('set','name','jack')

Use redis to run scripts

For example, if we want to use Redis to call the script to execute set name jack , the script is as follows:

EVAL "return redis.call('set', 'name', 'jack')"  0

Note: The following 0 is the number of parameters.

If the key and value in the script do not want to be hardcoded, they can be passed as parameters . Key type parameters will be placed in the KEYS array, and other parameters will be placed in the ARGV array. These parameters can be obtained from the KEYS and ARGV arrays in the script :

EVAL "return redis.call('set',KEYS[1],ARGV[1])" 1 name  Rose

Note: This is what our actual business needs, and the parameters cannot be hard-coded.

Next, write code logic with Lua script

code:

-- 锁的key
-- local key = KEYS[1]

-- 线程标识
-- local threadId = ARGV[1];

--获取锁中的线程标识,get key
-- local id = redis.call('get',KEYS[1]);

--具体脚本
if(redis.call('get',KEYS[1]) == ARGV[1]) then
    --释放锁 del key
    return redis.call('del',KEYS[1])
end 
return 0

Then write this code in IDEA and call it using Redis. (Note: To write Lua code, you need to download the EmmyLua plug-in)

The specific implementation code: (mainly look at the release lock code)

package com.hmdp.utils;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.lang.UUID;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.util.CollectionUtils;

import java.util.Collections;
import java.util.concurrent.TimeUnit;

/**
 * @Author 华子
 * @Date 2022/12/3 20:53
 * @Version 1.0
 */
public class SimpleRedisLock implements ILock {

    //Redis
    private StringRedisTemplate stringRedisTemplate;
    //业务名称,也就是锁的名称
    private String name;
    public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.name = name;
    }

    //key的前缀
    private static final String KEY_PREFIX = "lock:";
    //线程标识的前缀
    private static final String ID_PREFIX = UUID.randomUUID().toString(true);

    //获取lua脚本
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    static {
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }

    @Override
    public boolean tryLock(long timeoutSec) {
        //获取线程id,当作set的value
        String threadId = Thread.currentThread().getId()+ID_PREFIX;

        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
        //调用Lua脚本
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name),
                Thread.currentThread().getId()+ID_PREFIX
        );
    }


}

 Mainly use the excute method in StringRedisTemplate, here pass a script parameter, a key parameter, a value parameter

1. Script parameters: use DefaultRedisScript to obtain the script, you need to pass in the path of the script file, call new ClassPathResource("unlock.lua") as the path of the script file

2. The key parameter is our KEYS[1], which is a collection. We convert the key of the lock we use into a collection as the key parameter

3. value is our thread ID.

In this way, the Lua script we use will release the lock that needs to be implemented in the original two steps, and synthesize a line of code to implement it, and there will be no problem~ 

Guess you like

Origin blog.csdn.net/qq_59212867/article/details/128209569
Recommended