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')
编写完脚本之后,可以调用Redis
的EVAL
命令来执行,语法为:
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()); // 其他参数
}