当前位置: 首页 > news >正文

编码技巧——基于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,将对象序列化为JSONJackson2JsonRedisSerializer<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,将对象序列化为JSONJackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer =new Jackson2JsonRedisSerializer<>(Object.class);jackson2JsonRedisSerializer.setObjectMapper(this.getObjectMapper());return jackson2JsonRedisSerializer;}/*** 获取JSON对象映射器** @return ObjectMapper*/protected ObjectMapper getObjectMapper() {// json转对象类,不设置默认的会将json转HashMapObjectMapper 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);@Resourceprivate 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 - 简书

相关文章:

  • python二维数组初始化的一个极其隐蔽的bug(浅拷贝)
  • Win32 SDK Gui编程系列之--ListView自绘OwnerDraw(续)
  • 幻兽帕鲁(Palworld)允许自建私服,它是怎么挣钱的呢?
  • 融资项目——配置redis
  • Go语言每日一练——链表篇(四)
  • c实现链表
  • 【网工】华为设备命令学习(Telnet)
  • sklearn实现数据标准化(Standardization)和归一化(Normalization)
  • vue3 解决ionic安卓版顶部状态栏空白问题
  • node.js 使用 elementtree 生成思维导图 Freemind 文件
  • Linux操作系统基础(一):操作系统概述
  • TCP和UDP相关问题(重点)(4)——4.使用TCP的协议有哪些?使用UDP的协议有哪些?
  • Cilium CNI深度指南
  • 【FFmpeg】ffplay 命令行参数 ⑤ ( 设置音频滤镜 -af 参数 | 设置统计信息 -stats 参数 | 设置同步时钟类型 -sync 参数 )
  • TCP的连接和断开详解
  • 【跃迁之路】【519天】程序员高效学习方法论探索系列(实验阶段276-2018.07.09)...
  • 0x05 Python数据分析,Anaconda八斩刀
  • android图片蒙层
  • angular2开源库收集
  • css布局,左右固定中间自适应实现
  • CSS进阶篇--用CSS开启硬件加速来提高网站性能
  • ES学习笔记(12)--Symbol
  • JavaScript的使用你知道几种?(上)
  • Java多态
  • MySQL-事务管理(基础)
  • oldjun 检测网站的经验
  • SOFAMosn配置模型
  • TypeScript实现数据结构(一)栈,队列,链表
  • 初识 beanstalkd
  • 从0到1:PostCSS 插件开发最佳实践
  • 从零到一:用Phaser.js写意地开发小游戏(Chapter 3 - 加载游戏资源)
  • 大整数乘法-表格法
  • 猫头鹰的深夜翻译:JDK9 NotNullOrElse方法
  • 那些被忽略的 JavaScript 数组方法细节
  • 使用权重正则化较少模型过拟合
  • 我的业余项目总结
  • elasticsearch-head插件安装
  • Semaphore
  • ​二进制运算符:(与运算)、|(或运算)、~(取反运算)、^(异或运算)、位移运算符​
  • # 数论-逆元
  • #Java第九次作业--输入输出流和文件操作
  • (12)Hive调优——count distinct去重优化
  • (附源码)计算机毕业设计ssm基于B_S的汽车售后服务管理系统
  • (三维重建学习)已有位姿放入colmap和3D Gaussian Splatting训练
  • (五)网络优化与超参数选择--九五小庞
  • .NET Core 2.1路线图
  • .NET Windows:删除文件夹后立即判断,有可能依然存在
  • .NET大文件上传知识整理
  • .NET的微型Web框架 Nancy
  • @selector(..)警告提示
  • [ 常用工具篇 ] AntSword 蚁剑安装及使用详解
  • [20150707]外部表与rowid.txt
  • [AIR] NativeExtension在IOS下的开发实例 --- IOS项目的创建 (一)
  • [Android] Android ActivityManager
  • [ASP]青辰网络考试管理系统NES X3.5