前言

在使用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);

}
  1. 实现接口
    脚本的泛型即为脚本返回的结果类型,按实际情况赋值
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)
                )));
    }
}