Redis 2.6 及更高版本支持通过 eval 和 evalsha 命令运行 Lua 脚本。Spring Data Redis 为运行脚本提供了一个高级抽象,用于处理序列化并自动使用 Redis 脚本缓存。
可以通过调用 RedisTemplate 和 ReactiveRedisTemplate 的 execute() 方法来运行脚本。两者都使用可配置的ScriptExecutor(或 ReactiveScriptExecuter)来运行提供的脚本。默认情况下,ScriptExecutor(或ReactiveScriptExecuter)负责序列化提供的键和参数,并反序列化脚本结果。这是通过模板(RedisTemplate)的键和值的序列化器完成的。还有一个额外的重载,允许您为脚本参数和结果传递自定义序列化程序,例如:
<T> T execute(RedisScript<T> script, RedisSerializer<?> argsSerializer, RedisSerializer<T> resultSerializer, List<K> keys, Object... args)
其中,argsSerializer 和 resultSerializer 可以是自定义的序列化器
默认的 ScriptExecutor 通过检索脚本的 SHA1 并尝试首先运行 evalsha 来优化性能,如果 Redis 脚本缓存中还没有脚本,则返回 eval。
下面的示例使用 Lua 脚本运行一个常见的 “检查并设置” 场景。这是 Redis 脚本的理想用例,因为它要求以原子(atomically)方式运行一组命令,并且一个命令的行为会受到另一个命令结果的影响。
@Bean
public RedisScript<Boolean> script() {
ScriptSource scriptSource = new ResourceScriptSource(
new ClassPathResource("META-INF/scripts/checkandset.lua"));
return RedisScript.of(scriptSource, Boolean.class);
}
public class Example {
@Autowired
RedisScript<Boolean> script;
public boolean checkAndSet(String expectedValue, String newValue) {
// 执行lua脚本
return redisTemplate.execute(script, singletonList("key"), asList(expectedValue, newValue));
}
}checkandset.lua:
local current = redis.call('GET', KEYS[1])
if current == ARGV[1]
then redis.call('SET', KEYS[1], ARGV[2])
return true
end
return false前面的代码配置了指向名为 checkandset.lua 脚本文件的 RedisScript。lua 脚本它应该返回一个布尔值。脚本 resultType 应为 Long、Boolean、List 或反序列化值类型之一。如果脚本返回丢弃状态(指定 OK),它也可以为空。
注意,最好在应用程序上下文中配置 DefaultRedisScript 的单个实例,以避免在每次运行脚本时重新计算脚本的 SHA1。
然后,上面的 checkAndSet 方法运行脚本。脚本可以作为事务或管道的一部分在 SessionCallback 中运行。有关详细信息,请参见 “Redis事务” 和 “管道”。
Spring Data Redis 提供的脚本支持还允许您使用 Spring Task 和 Scheduler 抽象来安排 Redis 脚本的定期运行。
package com.hxstrive.redis.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.serializer.*;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String,String> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String,String> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(factory);
// 设置键序列化方式
redisTemplate.setKeySerializer(new StringRedisSerializer());
// 设置简单类型值的序列化方式
redisTemplate.setValueSerializer(new StringRedisSerializer());
// 设置 hash 类型键的序列化方式
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
// 设置 hash 类型值的序列化方式
redisTemplate.setHashValueSerializer(new StringRedisSerializer());
// 设置默认序列化方式
redisTemplate.setDefaultSerializer(new StringRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
@Bean
public RedisScript<Boolean> script() {
Resource scriptSource = new ClassPathResource("META-INF/scripts/checkandset.lua");
return RedisScript.of(scriptSource, Boolean.class);
}
}package com.hxstrive.redis.scripts;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import java.util.Collections;
import java.util.List;
/**
* Spring Data Redis 脚本
* @author hxstrive.com 2022/2/26
*/
@SpringBootTest
public class ScriptDemo {
@Autowired
private RedisScript<Boolean> script;
@Autowired
private RedisTemplate<String,String> redisTemplate;
@Test
public void contextLoads() {
// 当 redis 中没有 key 时,不满足预期值,因此返回 false
boolean flag = checkAndSet("hello", "world");
System.out.println(flag ? "修改成功" : "修改失败");
// 手工添加一个值,再试试
redisTemplate.opsForValue().set("key", "hello");
flag = checkAndSet("hello", "world");
System.out.println(flag ? "修改成功" : "修改失败");
}
/**
* 检查且设置。只有当 key 中的值为预期值时,才将新值设置给key
* @param expectedValue 预期值
* @param newValue 新值
* @return
*/
private boolean checkAndSet(String expectedValue, String newValue) {
List<String> keyList = Collections.singletonList("key");
return redisTemplate.execute(script, keyList, expectedValue, newValue);
}
}输出结果如下:
修改失败 修改成功