LUA脚本改造redis分布式锁

在redis集群模式下,我们会启动多个tomcat实例,每个tomcat实例都有一个JVM,且不共享。而synchronize锁的作用范围仅仅是当前JVM,所以我们需要一个作用于集群下的锁,也就是分布式锁。(就是不能用JVM自带的锁了,需要一个第三方应用实现锁)

在redis集群中我们是用setnx实现互斥锁来实现分布式锁。

setnx key value NX EX 时间 是往redis中创建一个key-value键值对,如果redis中没有该key,则创建成功,返回true;如果redis已经有了该key,则创建失败,返回null。NX是互斥,EX是超时时间,后跟具体时间+单位(s),EX用于过期时间,过了过期时间自动删除该key-value键值对。

我们用setnx命令实现分布式锁时,key和value都是String类型,key一般是特定前缀+该锁的名字,

value为线程id。

为什么value要存线程id呢?

因为存在这样情况:线程一获取锁,去执行逻辑,过程中遇到网络动荡,线程一卡住了,然后一段时间后,线程一获取的锁的过期时间到了,线程一的锁自动释放。然后线程二来获取锁,线程二获取成功,去执行逻辑,而在这个过程中线程一的网络动荡恢复了,线程一继续执行,线程一先于线程二执行完,线程一去释放锁,但此时线程一创建的锁已经因超时而自动释放了,所以此时线程一会去错误的释放线程二的锁,所以我们要把线程id存入加以判断,防止误删其他线程的id。

代码实现获取锁和释放锁:

java 复制代码
package com.hmdp.utils;

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.data.redis.core.script.RedisScript;

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

public class SimpleRedisLock implements ILock {

    private String name;
    private StringRedisTemplate stringRedisTemplate;

    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    private static final String KEY_PREFIX = "lock:";

    //randomUUID生成的数字会带一个横线,toString(true)方法就是去掉横线
    //因为不同JVM中,可能存在线程号相同的情况,所以需要用UUID来区分不同的JVM
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";

    @Override
    public boolean tryLock(long timeoutSec) {
        // 获取线程标示
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
        //不能直接返回success,因为会有自动拆箱的风险,如果success是null,就会返回true,返回错误数据
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
        // 获取线程标示
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 获取锁中的标示
        String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
        // 判断标示是否一致
        if(threadId.equals(id)) {
            // 释放锁
            stringRedisTemplate.delete(KEY_PREFIX + name);
        }
    }
}

我们需要新建一个类实现ILock接口,然后去重写其中的tryLock获取锁、unlock释放锁方法。

往SimpleRedisLock的构造方法传入name变量,方便我们在实例化SimpleRedisLock类中设置该锁的名字。我们后面用setnx命令创建锁时的key值就是特定前缀KEY_PREFIX加上这个name;value值是randomUUID生成的唯一一串数字加上该线程id,因为不同JVM中,可能存在线程号相同的情况,所以需要用UUID来区分不同的JVM。

问题:

当本线程1在进入这个if判断后(释放锁之前),突然阻塞(比如full GC,该JVM上全服务堵塞)阻塞时间过长,锁超时释放,这时另一个jvm的线程2获取到锁,然后线程1继续执行释放锁

这样就又会出现同一个用户会有俩个线程在同时运行,所以需要保证判断和释放锁这俩步为一个原子性,同成功或同失败

这样很容易想到事务,redis也有事务,但redis的事务可以保证原子性,但不能保证一致性,而且redis的事务,是最后事务中的步骤同时完成。并不是一步一步的执行,所以只能用乐观锁但不建议用乐观锁,推荐用lua脚本

一个lua脚本中可以编写多条redis命令,确保多条命令的原子性。即实现判断和释放锁一起执行的原子性。

我们需要把这个脚本写在resources目录下

释放锁的lua脚本:

Lua 复制代码
local key=KEYS[1]  -- 锁的key
local threadId=ARGV[1]  --线程唯一标识
-- 比较线程标示与锁中的标示是否一致
if(redis.call('get', key) ==  threadId) then
    -- 释放锁 del key
    return redis.call('del', key)
end
return 0;

if() then end 相当于Java命令中的 if( ) { } 。

KEYS[1]:为需要传入的key值,当需要传入多个key值时,声明KEYS[2]、KEYS[3]...等就行。

ARGV[1]:为需要传入的其他非key的变量,声明方法与key一样。例如:

Lua 复制代码
local key= KEYS[1] 
local key2= KEYS[2]
local threadId=ARGV[1]
local releaseTime=ARGV[2]

redis.call(' ', ....)是执行的redis命令,命令中单引号' '中写要执行的redis操作,后面的参数为执行该redis命令所需的参数,例如,get命令,需要知道key值。

然后改写我们的分布式锁:

我们需要先加载我们的lua脚本:

java 复制代码
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    static {
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua")); //加载的脚本的位置,如果脚本文件在resources目录下,则只需写脚本名称即可。
        UNLOCK_SCRIPT.setResultType(Long.class); //返回值的类型
    }
@Override
    public void unlock() {
        // 调用lua脚本  ,原来的多行代码变成了现在的单行代码就保证了原子性
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name), //生成单元素的集合,即脚本中的需要的KETS[1]参数
                ID_PREFIX + Thread.currentThread().getId()); //即脚本中需要的ARVG[1]参数
    }

完整代码:

java 复制代码
package com.hmdp.utils;

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.data.redis.core.script.RedisScript;

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

public class SimpleRedisLock implements ILock {

    private String name;
    private StringRedisTemplate stringRedisTemplate;

    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    private static final String KEY_PREFIX = "lock:";

    //randomUUID生成的数字会带一个横线,toString(true)方法就是去掉横线
    //因为不同JVM中,可能存在线程号相同的情况,所以需要用UUID来区分不同的JVM
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";

    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) {
        // 获取线程标示
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
        //不能直接返回success,因为会有自动拆箱的风险,如果success是null,就会返回true,返回错误数据
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
        // 调用lua脚本  ,原来的多行代码变成了现在的单行代码就保证了原子性
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name), //生成单元素的集合,即脚本中的需要的KETS[1]参数
                ID_PREFIX + Thread.currentThread().getId()); //即脚本中需要的ARVG[1]参数
    }

}
相关推荐
懒羊羊大王呀2 小时前
Ubuntu20.04中 Redis 的安装和配置
linux·redis
禺垣3 小时前
区块链技术概述
大数据·人工智能·分布式·物联网·去中心化·区块链
John Song4 小时前
Redis 集群批量删除key报错 CROSSSLOT Keys in request don‘t hash to the same slot
数据库·redis·哈希算法
zhuhit6 小时前
FASTDDS的安全设计
分布式·机器人·嵌入式
暗影八度6 小时前
Spark流水线+Gravitino+Marquez数据血缘采集
大数据·分布式·spark
q567315237 小时前
IBM官网新闻爬虫代码示例
开发语言·分布式·爬虫
不爱学英文的码字机器7 小时前
数据网格的革命:从集中式到分布式的数据管理新范式
分布式
优秀的颜10 小时前
计算机基础知识(第五篇)
java·开发语言·分布式
Zfox_12 小时前
Redis:Hash数据类型
服务器·数据库·redis·缓存·微服务·哈希算法
呼拉拉呼拉12 小时前
Redis内存淘汰策略
redis·缓存