编码技巧——基于RedisTemplate的RedisClient实现、操作Lua脚本
1. 背景
在新公司的脚手架中开发,需要用到redis,发现没有封装好一套能集成各种常用命令、包括Lua脚本的方便使用的RedisTemplateClient,于是自己来实现下;
springboot整合redis之后,提供了操作redis的简便方式,可以通过通用对象RedisTemplate方式操作String,Hash,List,Set SortSet五大数据类型;
本次的实现就是基于Spring的RedisTemplate做的二次封装;
2. 代码
(1)RedisTemplate配置类
配置的关键是序列化器的配置;未配置好序列化器,会导致例如时间类、Json格式放入缓存中时,实际存放的值与预期不符,或是在获取值时出现预期之外的错误;
推荐下方的配置写法;
/**
* Redis配置类
*/
@Configuration
@ConditionalOnClass({RedisOperations.class})
@EnableConfigurationProperties(MyRedisCacheProperties.class)
@AutoConfigureBefore(RedisAutoConfiguration.class)
@ConditionalOnProperty(prefix = "my.project.cache", name = "enabled", havingValue = "true", matchIfMissing = true)
public class RedisCacheAutoConfiguration {
/**
* 配置RedisTemplate
*
* @param factory Redis连接工厂
* @return RedisTemplate<String, Object>
*/
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "spring.cache.type", havingValue = "redis")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
// 创建一个模板类
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 将刚才的redis连接工厂设置到模板类中
template.setConnectionFactory(factory);
// 设置key的序列化器
template.setKeySerializer(new StringRedisSerializer());
// 设置value的序列化器
// 使用Jackson 2,将对象序列化为JSON
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = this.getJackson2JsonRedisSerializer();
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}
/**
* 获取Jackson2Json序列器
*
* @return Jackson2JsonRedisSerializer
*/
protected Jackson2JsonRedisSerializer<Object> getJackson2JsonRedisSerializer() {
// 使用Jackson 2,将对象序列化为JSON
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer =
new Jackson2JsonRedisSerializer<>(Object.class);
jackson2JsonRedisSerializer.setObjectMapper(this.getObjectMapper());
return jackson2JsonRedisSerializer;
}
/**
* 获取JSON对象映射器
*
* @return ObjectMapper
*/
protected ObjectMapper getObjectMapper() {
// json转对象类,不设置默认的会将json转HashMap
ObjectMapper objectMapper = JacksonUtils.newObjectMapper();
// 设置序列化时,保留对象的全类名,这样反序列化时就不用指定类型了
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.WRAPPER_ARRAY);
return objectMapper;
}
}
(2)RedisClient的二次封装
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.support.spring.FastJsonRedisSerializer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* redisTemplate工具类 提供安全的redis操作命令
*/
@Slf4j
@Service
public class RedisService {
private static final RedisSerializer<String> stringRedisSerializer = new StringRedisSerializer();
private static final RedisSerializer<Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);
@Resource
private RedisTemplate<String, String> redisTemplate;
// =============================common============================
/**
* 指定缓存失效时间
*
* @param key 键
* @param seconds 时间(秒)
*/
public void expire(String key, long seconds) {
if (seconds > 0) {
redisTemplate.expire(key, seconds, TimeUnit.SECONDS);
} else {
throw new IllegalArgumentException("设置的redis过期时间应大于0! [badParam=" + seconds + "]");
}
}
/**
* 根据key 获取过期时间
*
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long ttl(String key) {
return Objects.requireNonNull(redisTemplate.getExpire(key, TimeUnit.SECONDS));
}
/**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
/**
* 删除key
*
* @param key 可以传一个值 或多个
*/
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
log.warn("删除redisKey成功 [key={}]", key[0]);
} else {
List<String> keys = Arrays.asList(key);
redisTemplate.delete(keys);
log.warn("批量删除redisKey成功 [keys={}]", JSON.toJSONString(keys));
}
}
}
// ============================Lua=============================
/**
* 执行lua脚本 脚本返回值必须为number
*
* @param luaScript
* @param keys
* @param args 为空时传Collections.emptyList()
* @return
*/
@SuppressWarnings({"rawtypes", "unchecked"})
public Long evalLua(String luaScript, List<String> keys, List<String> args) {
// 利用泛型擦除
RedisScript script = new DefaultRedisScript<>(luaScript, Long.class);
Object execute = redisTemplate.execute(script, stringRedisSerializer, fastJsonRedisSerializer, keys, args.toArray());
return (Long) execute;
}
/**
* 执行lua脚本 脚本返回值必须为jsonString
*
* @param luaScript
* @param keys
* @param args 为空时传Collections.emptyList()
* @return 返回类型
*/
@SuppressWarnings({"rawtypes", "unchecked"})
public String evalLuaForJsonVal(String luaScript, List<String> keys, List<String> args) {
// 利用泛型擦除
RedisScript script = new DefaultRedisScript<>(luaScript, String.class);
Object execute = redisTemplate.execute(script, stringRedisSerializer, fastJsonRedisSerializer, keys, args.toArray());
return String.valueOf(execute);
}
/**
* 执行lua脚本 脚本返回值必须为string 注意使用tostring函数或数值常量加单引号如'const'
*
* @param luaScript
* @param keys
* @param args 为空时传Collections.emptyList()
* @return 返回类型
*/
@SuppressWarnings({"rawtypes", "unchecked"})
public String evalLuaForStringVal(String luaScript, List<String> keys, List<String> args) {
// 利用泛型擦除
RedisScript script = new DefaultRedisScript<>(luaScript, String.class);
Object execute = redisTemplate.execute(script, stringRedisSerializer, stringRedisSerializer, keys, args.toArray());
return String.valueOf(execute);
}
// ============================String=============================
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public String get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public void set(String key, String value) {
redisTemplate.opsForValue().set(key, value);
}
/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param seconds 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public void setExpire(String key, String value, long seconds) {
if (seconds > 0) {
redisTemplate.opsForValue().set(key, value, seconds, TimeUnit.SECONDS);
} else {
set(key, value);
}
}
/**
* 递增
*
* @param key 键
* @param delta 要增加几(大于0)
* @return 递增后的结果
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new IllegalArgumentException("incr递增因子必须大于0");
}
return Objects.requireNonNull(redisTemplate.opsForValue().increment(key, delta));
}
/**
* 递减
*
* @param key 键
* @param delta 要减少几(小于0)
* @return 递减后的结果
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new IllegalArgumentException("decr递减因子必须大于0");
}
return Objects.requireNonNull(redisTemplate.opsForValue().increment(key, -delta));
}
// ================================Hash=================================
/**
* HashGet
*
* @param key 键 不能为null
* @param hashKey 项 不能为null
*/
public Object hget(String key, String hashKey) {
return redisTemplate.opsForHash().get(key, hashKey);
}
/**
* 获取hashKey对应的所有键值
*
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
*
* @param key 键
* @param map 对应多个键值
*/
public void hmset(String key, Map<String, Object> map) {
redisTemplate.opsForHash().putAll(key, map);
}
/**
* HashSet 并设置时间 不保证原子性
*
* @param key 键
* @param map 对应多个键值
* @param seconds 时间(秒)
*/
public void hmsetExpire(String key, Map<String, Object> map, long seconds) {
redisTemplate.opsForHash().putAll(key, map);
if (seconds > 0) {
expire(key, seconds);
}
}
/**
* 向一张hash表中放入数据,hash如果不存在将创建hash
*
* @param key 键
* @param item 项
* @param value 值
*/
public void hset(String key, String item, Object value) {
redisTemplate.opsForHash().put(key, item, value);
}
/**
* 向一张hash表中放入数据,hash如果不存在将创建hash 不保证原子性 原子性使用Lua脚本
*
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
*/
public void hsetExpire(String key, String item, Object value, long time) {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
}
/**
* 删除hash表中的值
*
* @param key 键 不能为null
* @param hashKey 项 可以使多个 不能为null
*/
public void hdel(String key, Object... hashKey) {
redisTemplate.opsForHash().delete(key, hashKey);
}
/**
* 判断hash表中是否有该项的值
*
* @param key 键 不能为null
* @param hashKey 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String hashKey) {
return redisTemplate.opsForHash().hasKey(key, hashKey);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
*
* @param key 键
* @param item 项
* @param delta 要增加几(大于0)
*/
public double hincr(String key, String item, long delta) {
if (delta < 0) {
throw new IllegalArgumentException("hincr递增因子必须大于0");
}
return redisTemplate.opsForHash().increment(key, item, delta);
}
/**
* hash递减
*
* @param key 键
* @param item 项
* @param delta 要减少记(小于0)
*/
public double hdecr(String key, String item, long delta) {
if (delta < 0) {
throw new IllegalArgumentException("hdecr递减因子必须大于0");
}
return redisTemplate.opsForHash().increment(key, item, -delta);
}
// ============================set=============================
/**
* 根据key获取Set中的所有值
*
* @param key 键
*/
public Set<String> sGet(String key) {
return redisTemplate.opsForSet().members(key);
}
/**
* 根据value从一个set中查询,是否存在
*
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
return Boolean.TRUE.equals(redisTemplate.opsForSet().isMember(key, value));
}
private String[] toStringArray(List<Object> values) {
List<String> collect = values.stream().map(String::valueOf).collect(Collectors.toList());
String[] valStrArray = new String[collect.size()];
for (int i = 0; i < valStrArray.length; i++) {
valStrArray[i] = collect.get(i);
}
return valStrArray;
}
/**
* 将数据放入set缓存
*
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, List<Object> values) {
return Objects.requireNonNull(redisTemplate.opsForSet().add(key, toStringArray(values)));
}
/**
* 将set数据放入缓存
*
* @param key 键
* @param seconds 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long seconds, List<Object> values) {
long count = Objects.requireNonNull(redisTemplate.opsForSet().add(key, toStringArray(values)));
if (seconds > 0)
expire(key, seconds);
return count;
}
/**
* 获取set缓存的长度
*
* @param key 键
*/
public long sGetSetSize(String key) {
return Objects.requireNonNull(redisTemplate.opsForSet().size(key));
}
/**
* 移除值为value的
*
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, List<Object> values) {
return Objects.requireNonNull(redisTemplate.opsForSet().remove(key, values));
}
// ===============================list=================================
/**
* 获取list缓存的内容
*
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
*/
public List<String> lrange(String key, long start, long end) {
return redisTemplate.opsForList().range(key, start, end);
}
/**
* 获取list缓存的长度
*
* @param key 键
*/
public long lsize(String key) {
return Objects.requireNonNull(redisTemplate.opsForList().size(key));
}
/**
* 通过索引 获取list中的值
*
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
*/
public String lGetIndex(String key, long index) {
return redisTemplate.opsForList().index(key, index);
}
/**
* item放入list缓存
*
* @param key 键
* @param value 值
*/
public void ladd(String key, String value) {
redisTemplate.opsForList().rightPush(key, value);
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public long lpush(String key, List<String> value) {
return Objects.requireNonNull(redisTemplate.opsForList().rightPushAll(key, value));
}
/**
* 根据索引修改list中的某条数据
*
* @param key 键
* @param index 索引
* @param updateVal 值
* @return
*/
public void lUpdateIndex(String key, long index, String updateVal) {
redisTemplate.opsForList().set(key, index, updateVal);
}
}
注意操作Lua脚本时,一般脚本预期的返回值是数值型;如果需要返回字符串或Json字符串,需要特殊指定返回类型和序列化器;如下:
// ============================Lua=============================
/**
* 执行lua脚本 脚本返回值必须为number
*
* @param luaScript
* @param keys
* @param args 为空时传Collections.emptyList()
* @return
*/
@SuppressWarnings({"rawtypes", "unchecked"})
public Long evalLua(String luaScript, List<String> keys, List<String> args) {
// 利用泛型擦除
RedisScript script = new DefaultRedisScript<>(luaScript, Long.class);
Object execute = redisTemplate.execute(script, stringRedisSerializer, fastJsonRedisSerializer, keys, args.toArray());
return (Long) execute;
}
/**
* 执行lua脚本 脚本返回值必须为jsonString
*
* @param luaScript
* @param keys
* @param args 为空时传Collections.emptyList()
* @return 返回类型
*/
@SuppressWarnings({"rawtypes", "unchecked"})
public String evalLuaForJsonVal(String luaScript, List<String> keys, List<String> args) {
// 利用泛型擦除
RedisScript script = new DefaultRedisScript<>(luaScript, String.class);
Object execute = redisTemplate.execute(script, stringRedisSerializer, fastJsonRedisSerializer, keys, args.toArray());
return String.valueOf(execute);
}
/**
* 执行lua脚本 脚本返回值必须为string 注意使用tostring函数或数值常量加单引号如'const'
*
* @param luaScript
* @param keys
* @param args 为空时传Collections.emptyList()
* @return 返回类型
*/
@SuppressWarnings({"rawtypes", "unchecked"})
public String evalLuaForStringVal(String luaScript, List<String> keys, List<String> args) {
// 利用泛型擦除
RedisScript script = new DefaultRedisScript<>(luaScript, String.class);
Object execute = redisTemplate.execute(script, stringRedisSerializer, stringRedisSerializer, keys, args.toArray());
return String.valueOf(execute);
}
(3)Lua脚本相关
可参考之前的文章编码技巧——Lua脚本的应用,这里再次附上代码;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Properties;
/**
* 文件加载类
*/
@Slf4j
public class FileLoaderUtils {
/**
* 读文件文本到String 根目录为resource
*
* @param fileName
* @return
*/
public static String loadFile2String(String fileName) {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
URL url = loader.getResource(fileName);
if (url == null) {
log.error("Cannot_locate_" + fileName + "_as_a_classpath_resource.");
throw new RuntimeException("Cannot_locate_" + fileName + "_as_a_classpath_resource.");
}
try {
return IOUtils.toString(url, StandardCharsets.UTF_8);
} catch (IOException e) {
log.error("读取文件内容为String失败! [fileName={}]", fileName, e);
throw new RuntimeException("读取文件内容为String失败! [fileName=" + fileName + "]");
}
}
/**
* 读文件文本到Property 根目录为resource
*
* @param fileName
* @return
*/
public static Properties loadFile2Property(String fileName) {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
InputStream in = loader.getResourceAsStream(fileName);
Properties properties = new Properties();
try {
properties.load(in);
if (in != null) {
in.close();
}
} catch (IOException e) {
log.error("读取文件内容到Properties失败! [fileName={}]", fileName, e);
throw new RuntimeException("读取文件内容到Properties失败! [fileName=" + fileName + "]");
}
return properties;
}
}
/**
* LuaScript注册类
*/
public class LuaScript {
// 常用原子操作
private static final String INCR_EXPIRE;
private static final String DECR_EXPIRE;
private static final String SETNX_EXPIRE;
private static final String HSET_EXPIRE;
private static final String INCRBY_EXPIRE;
private static final String SET_EXPIRE;
static {
DECR_EXPIRE = FileLoaderUtils.loadFile2String("lua/decr_expire.lua");
INCR_EXPIRE = FileLoaderUtils.loadFile2String("lua/incr_expire.lua");
SETNX_EXPIRE = FileLoaderUtils.loadFile2String("lua/setnx_expire.lua");
HSET_EXPIRE = FileLoaderUtils.loadFile2String("lua/hset_expire.lua");
INCRBY_EXPIRE = FileLoaderUtils.loadFile2String("lua/incrby_expire.lua");
SET_EXPIRE = FileLoaderUtils.loadFile2String("lua/set_expire.lua");
}
public static String incrExpireScript() {
return INCR_EXPIRE;
}
public static String decrExpireScript() {
return DECR_EXPIRE;
}
public static String setnxExpireScript() {
return SETNX_EXPIRE;
}
public static String hsetExpireScript() {
return HSET_EXPIRE;
}
public static String incrbyExpireScript() {
return INCRBY_EXPIRE;
}
public static String setExpireScript() {
return SET_EXPIRE;
}
}
以上,希望能帮到你;
参考:
Springboot整合Redis使用RedisTemplate操作redis五大类型用法详解
RedisTemplate执行lua脚本抛出异常IllegalStateException - 简书