Spring Data Redis 教程

Redis 脚本

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 脚本的定期运行。

示例

配置 RedisTemplate

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);
    }

}

调用 Lua 脚本

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);
    }

}

输出结果如下:

修改失败
修改成功
说说我的看法
全部评论(
没有评论
关于
本网站专注于 Java、数据库(MySQL、Oracle)、Linux、软件架构及大数据等多领域技术知识分享。涵盖丰富的原创与精选技术文章,助力技术传播与交流。无论是技术新手渴望入门,还是资深开发者寻求进阶,这里都能为您提供深度见解与实用经验,让复杂编码变得轻松易懂,携手共赴技术提升新高度。如有侵权,请来信告知:hxstrive@outlook.com
公众号