Redis的Lua脚本

Redis的Lua脚本

本篇将以redis实现分布式锁的案例,来讲解为什么要使用Redis的Lua脚本,并且如何使用


Redis 从 2.6.0 版本开始支持 Lua 脚本。Redis 使用 Lua 脚本主要是为了实现原子性操作。在 Redis 中,一个 Lua 脚本的执行是原子的,这意味着当一个 Lua 脚本开始执行时,其他客户端的命令必须等待这个脚本执行完才能执行。

分布式锁

比如当我们利用Redis去实现一个分布式锁的时候 ,需要频繁的获取锁和释放锁,来保证业务串行执行,防止发生并发修改异常或其他并发异常,最简单的释放锁操作就是将这个key删除掉:

public void unlock() {
            redisTemplate.delete(LOCK_KEY);        
}

但是在高并发的业务场景下,同时会有多个线程来获取锁和释放锁,虽然只有一个线程可以获取到锁,但是为了防止死锁问题的发生,我们通常会给锁加上一个过期时间TTL,如果某个线程在获取到锁之后,由于某些原因线程被阻塞,并且阻塞时间超过了锁的过期时间,那么锁就被自动释放掉了,此时别的线程就可以获取锁了,但是由于被阻塞的线程还未完全执行,当阻塞结束后,依然会执行释放锁的操作,此时由于他的锁已经超时自动释放了,所以就会将其他线程的锁给释放掉,从而导致其他线程也会给另外线程的锁释放等等一系列的并发异常

于是我们在释放锁之前进行比对,验证将要释放的锁释放为本线程的锁,我们会在获取锁的时候,将value设置为线程标识,在释放之前获取锁中的线程标识与当前线程标识进行比对,如果一致才进行释放锁的操作:

// 利用UUID生成线程标识前缀,防止重复
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-"; 

public boolean tryLock(long timeoutSec) {
        // 当前获取线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 获取锁
        Boolean success = redisTemplate
                .opsForValue()
                .setIfAbsent(LOCK_KEY,  threadId, timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }

public void unlock() {
        // 获取锁中的线程标识
        String lockThreadId = redisTemplate.opsForValue().get(LOCK_KEY);
        // 获取当前线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId(); // 拼接了线程id
        // 判断是否一致
        if (lockThreadId.equals(threadId)) {
            redisTemplate.delete(KEY_PREFIX + name); // 一致再释放
        }
    }

但是此时又会出现另一个并发问题,释放逻辑是当前线程判断线程标识一致后再去释放锁,若在释放之前线程被阻塞,阻塞结束后就不会再判断标识一致,而是直接释放锁,假如阻塞时间也超过了TTL,那么依旧会导致误释放了其他线程的锁

出现这种问题的原因是锁的读取操作和释放操作是两个单独的执行,两次执行之间就有可能被打断或阻塞,我们希望这两次的执行放在一起,成为一个原子性的执行

首先想到的就是利用Redis中的事务来保证原子性,他确实可以保证两条命令同时被执行或同时不被执行,但是我们的两条命令从逻辑上是需要在中间进行判断的,如果标识一致才进行释放操作,不一致的话就不能释放,而Redis中的事务并不能实现逻辑判断的过程,因此这种方法是不行的,此时就要使用 Lua 脚本


Lua脚本

Lua 是一种轻量级、高效的脚本语言,语法比较简单,使用Lua 去调用Redis时,语法为:

redis.call('命令名称', 'key','参数')

比如执行一个set操作时,lua脚本为:

redis.call('set','name','jack')

编写完脚本之后,可以调用RedisEVAL命令来执行,语法为:

EVAL script numkeys key [key...] arg [arg...]

numkeys 为key类型参数的数量,后面是全部key类型的参数,最后是全部的普通参数,例如执行set操作:

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

完善分布式锁

利用RedisTemplate提供的execute方法
1.编写lua脚本
那么要完成上面分布式锁的优化,就要使用Lua脚本了,首先我们先根据业务逻辑使用Lua语言来编写完整的脚本代码:

-- 比较锁中的线程标识与当前线程标识是否一致
if (redis.call("get", KEYS[1]) == ARGV[1])then
    -- 释放锁
    return redis.call("del", KEYS[1])
end
return 0

2.创建lua文件
在项目的resources目录下新建一个lua文件,把写好的lua脚本粘贴进去
在这里插入图片描述

在这里插入图片描述
3.注入DefaultRedisScript对象
它是连接 Java 代码与 Lua 脚本的关键纽带,确保后续操作能顺利调用脚本并获取正确结果

private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    static {
        UNLOCK_SCRIPT = new DefaultRedisScript<>(); // 初始化值
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));// 设置lua脚本
        UNLOCK_SCRIPT.setResultType(Long.class); // 设置返回值
    }

使用 setLocation 方法指明了 Lua 脚本文件的位置,ClassPathResource将其指向项目的 resources 目录下的 unlock.lua 文件
4.编写释放锁方法

public void unlock() {
        redisTemplate.execute(UNLOCK_SCRIPT, // lua脚本对象
                Collections.singletonList(KEY_PREFIX + name), // key参数
                ID_PREFIX + Thread.currentThread().getId()); // 其他参数
    }
### 如何在 Redis 中使用 Lua 脚本 #### 使用 EVAL 命令执行 Lua 脚本 Redis 支持通过 `EVAL` 命令来执行 Lua 脚本。此命令允许传递一个字符串形式的 Lua 代码给服务器并立即运行它。 ```lua EVAL "return {KEYS[1], ARGV[1]}" 1 mykey first ``` 上述例子返回两个值组成的数组,分别是键名和第一个参数[^1]。 #### KEYS 和 ARGV 参数说明 当编写 Lua 脚本时,可以利用两种特殊的全局变量: - **KEYS**: 表示由客户端传入的所有键名称列表。 - **ARGV**: 存储除键之外其他额外参数的表。 这些参数使得同一个脚本能处理不同的数据集而无需硬编码特定的键名或数值到源码里去。 #### 应用场景实例:原子计数器操作 假设有一个需求是要实现一个安全可靠的分布式锁机制,在这种情况下就可以借助于 Lua 来保证整个过程中的事务性。下面是一个简单的例子展示了如何创建一个带有过期时间设置的计数器,并且每次增加都会更新其 TTL (Time To Live): ```lua local current_value = redis.call('GET', KEYS[1]) if not current_value then -- 如果不存在该 key,则初始化为0并设置生存时间为 argv[1] redis.call('SET', KEYS[1], '0') redis.call('EXPIRE', KEYS[1], tonumber(ARGV[1])) end -- 执行 INCR 操作同时重置TTL redis.call('INCR', KEYS[1]) redis.call('EXPIRE', KEYS[1], tonumber(ARGV[1])) return redis.call('GET', KEYS[1]) ``` 这段 Lua 脚本首先尝试获取指定键对应的值;如果这个键还不存在的话就将其设为零并且赋予一定期限内的有效期。之后无论之前是否存在都对其进行自增运算以及重新设定存活周期。最后返回最新的计数值。 #### 性能优化建议 对于频繁使用的 Lua 脚本来说,应该考虑采用 `SCRIPT LOAD` 加上 `EVALSHA` 的方式代替直接使用 `EVAL` 。前者会先加载一次完整的脚本内容至缓存区中获得 SHA1 散列作为标识符,后续只需要提供散列即可快速定位已存在的版本从而减少网络传输开销提高效率。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值