前言
在使用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版本)
-
RedisScript.of()
静态方法(简单方便,推荐)
新建个类用来专门存放redis脚本实例对象public interface RedisLuaScript { /** * 对String类型的key进行递增递减并设置过期指定指定时间的原子脚本 */ RedisScript<String> STRING_KEY_INCREMENT_EXPIRE_AT = RedisScript.of( new ClassPathResource("lua/stringIncrementExpireAt.lua"), String.class); }
-
实现接口
脚本的泛型即为脚本返回的结果类型,按实际情况赋值
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)
)));
}
}