Reids实战——分布式锁优化(Lua脚本)

1 基于Redis分布式锁的问题

先来看看之前分布式锁的实现。

这个基于Redis的分布式锁仍然有着一个问题,那就是误删锁的问题。 

简单的来说,就是当第一个线程,也就是线程1,拿到锁后,但由于本身业务复杂,而导致了阻塞,超过了锁设置的超时时间,锁自动释放。这个时候,线程2进来了,也拿到了锁,但是就在线程2执行业务的途中,线程1业务完成,主动释放了锁,又因为我们释放锁的逻辑是直接删除key,这就导致了线程2的锁被误删

 

这就导致了线程安全的问题。

解决方法:在每个线程要释放锁的时候,主动判断reids中存入的线程标识,来判断是不是自己的锁,如果不是,就不能删除。

代码实现:

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 Lua脚本的使用

 上述代码仍然会在极端的情况下仍然会出现误删锁的问题。

试想一下这种情况:

扫描二维码关注公众号,回复: 15995575 查看本文章

在线程判断完线程标识时,发现是自己的id,就在准备释放锁的时候,发生了阻塞。然后就是锁超时时间到了,别的线程有获取到了锁,又出现了误删的问题。

那么,又该如何解决这个问题呢?

我们不妨这样想想:

就是我们把判断id和删除id整合成一个代码,让他一次执行,不用分成两次,这样不就好了?

这里就要使用我们Redis中的脚本:Lua 。

Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言,它的基本语法大家可以参考网站:Lua 教程 | 菜鸟教程

这里重点介绍Redis提供的调用函数,语法如下:

例如,我们要执行set name jack,则脚本是这样:

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

使用redis来运行脚本

例如,我们要使用Redis调用脚本来执行set name jack,则脚本是这样:

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

注:后面的0是参数个数。

如果脚本中的keyvalue不想写死,可以作为参数传递key类型参数会放入KEYS数组,其它参数会放入ARGV数组,在脚本中可以从KEYSARGV数组获取这些参数:

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

注:我们实际的业务就是需要这种,参数不能写死。

接下来,就是用Lua脚本编写代码逻辑

代码 :

-- 锁的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

然后在IDEA中编写此代码,并使用Redis调用。(注:编写Lua代码需下载EmmyLua插件)

具体实现代码:(主要看释放锁代码)

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
        );
    }


}

 主要还是使用了StringRedisTemplate中的excute方法,这里传一个脚本参数,一个key参数,一个value参数

1. 脚本参数:使用DefaultRedisScript 进行获取脚本,需要传入脚本文件的路径,调用new ClassPathResource("unlock.lua") 当做脚本文件路径

2. key参数,就是我们的KEYS[1],是一个集合,我们把我们使用的锁的key转成一个集合当成key参数

3. value,就是我们的线程标识。

这样,我们就使用的Lua脚本将原来两步需要实现的释放锁,合成一行代码实现,就不会出现问题啦~ 

猜你喜欢

转载自blog.csdn.net/qq_59212867/article/details/128209569