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); } }
输出结果如下:
修改失败 修改成功