欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

如何在SpringBoot中自定义redis的lua脚本实现

程序员文章站 2022-04-10 14:55:50
...

前言

在使用redis的过程中,可能会需要自定义一些lua脚本来完成自己业务方面的实现,用来保证操作上的原子性。那么在SpringBoot中如何去实现这样一套逻辑呢?

前置准备

依赖

不说版本的操作都是刷流氓

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
	<version>2.2.5.RELEASE</version>
</dependency>

脚本

比如业务中经常会有一种限制, 某个用户对某个操作同一天内只能操作多少次。 如果使用api,就要考虑API上操作的原子性以及后续的递增和上限判断等各种问题,那么就可以提供一个脚本,如下。
注意这个脚本的过期时间是在指定的具体时间过期,而不是直接指定过期ttl,因为这个脚本的应用场景就是同一天的操作要在当天凌晨清除掉,所以需要外部直接传入想要在什么时间过期

-- string 的key
local stringKey = KEYS[1]
-- 对value的变动补偿, 可以为负数
local step = tonumber(ARGV[1])
-- 过期时间
local expireAt = tonumber(ARGV[2])
-- check 值是否已存在, 不存在先插入key,并初始化值
local keyExist = redis.call("EXISTS", KEYS[1]);
if (keyExist < 1) then
    redis.call("SET", KEYS[1], 0)
    -- 设置过期时间
    redis.call("EXPIREAT", KEYS[1], expireAt)
end
-- 做递增或递减操作
redis.call("INCRBY", KEYS[1], step)

-- 返回最新结果,由于使用 stringRedisTemplate,返回值用string,否则值转换有问题
return tostring(redis.call("GET", KEYS[1]))

代码

初始化脚本类

定义脚本文件

在项目的resources资源目录下新建文件夹lua,用来作为所有lua脚本的栖身地, 然后新建文件stringIncrementExpireAt.lua将上述脚本内容加入。

定义脚本类

spring-data-redis使用org.springframework.data.redis.core.script.RedisScript类来描述一个脚本对象,实例化一个脚本对象有如下两种方式

  • 直接使用接口org.springframework.data.redis.core.script.RedisScript的of静态方法(>=2.2.5.RELEASE版本)
  • 实现接口org.springframework.data.redis.core.script.RedisScript(低于2.2.5.RELEASE版本)
  1. RedisScript.of()静态方法(简单方便,推荐)
    新建个类用来专门存放redis脚本实例对象

    public interface RedisLuaScript {
    
    	/**
    	 * 对String类型的key进行递增递减并设置过期指定指定时间的原子脚本
    	 */
    	RedisScript<String> STRING_KEY_INCREMENT_EXPIRE_AT = RedisScript.of(
    			new ClassPathResource("lua/stringIncrementExpireAt.lua"), String.class);
    
    }
    
  2. 实现接口
    脚本的泛型即为脚本返回的结果类型,按实际情况赋值

public class RedisCustomScript implements RedisScript<String> {

    @Override
    public String getSha1() {
        return DigestUtils.sha1DigestAsHex("脚本原文内容字符串");
    }

    @Override
    public Class<T> getResultType() {
        return String.class
    }

    @Override
    public String getScriptAsString() {
        return "按实际情况返回脚本的原文内容字符串"
    }
}

定义对外方法

现在需要的一切都准备好了,直接开始定义外部方法, 脚本对象引用使用了第一种方式。

@Component
public class RedisTemplateHelper {
	@Autowired
    private StringRedisTemplate stringRedisTemplate;
	
	/**
     * 对String类型的key进行递增递减并设置过期指定指定时间的原子脚本
     *
     * @param key      key
     * @param expireAt 指定过期的具体时间
     * @return 缓存key对应的最新值
     */
    public Long incrementKeyExpireAt(String key, Date expireAt) {
        if (System.currentTimeMillis() > expireAt.getTime()) {
            throw new IllegalArgumentException("过期时间不能早于当前时间");
        }
        return Long.parseLong(Objects.requireNonNull(
             stringRedisTemplate.execute(RedisLuaScript.STRING_KEY_INCREMENT_EXPIRE_AT,
                        Collections.singletonList(key), "1",
                        // 这个单位是秒
                        String.valueOf(expireAt.getTime() / 1000)
                )));
    }
}