SpringBoot+Redis存储时序列化怎么选择
在刚开始学习Redis时,我们在使用SpringBoot+Redis配置value的序列化方式时应该都是选择的jackson的GenericJackson2JsonRedisSerializer
或者是fastjson的GenericFastJsonRedisSerializer
两种序列化器,而key一般就是StringRedisSerializer
。本文将探索,如果将Redis的value序列化器也改为StringRedisSerializer
效率会有什么变化呢?
前言
最近项目中遇到了一个问题:有一个部门表里面近3w条数据,需要将这些数据一次性全部查出,并且这些数据使用的频率中等偏上,而项目中有用到了mybatis-plus,mybatis-plus中开了一个配置log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
就是将数据的日志全给打印了,然后发现一个接口调用用了20s左右,接口内部实现不但是进行了部门表的全部数据查询,而且还进行了后续的很多业务逻辑处理,导致接口超时问题。
后来在本地进行调试,将mybatis-plus的sql日志关闭后,接口时间不超过4s,这是最小改动的做法了,但是有个问题:项目已经上线了,并且管理员那边说只给部署项目,不能修改配置,只能下次上线时提供配置修改的文档,可现在生产上数据加载不出来(接口超时,wtf)。
看了下上次同事写的sql,直接select *
…,直接用解释器看了下,3w条数据没走索引,把*换了,搞个覆盖索引,结果0.5s都没超过。再想想mybatis-plus的sql日志问题,决定把查出来的数据给放入到redis中。
那么就回到标题的问题,序列化方式改怎么选择呢?
刚开始想的是用redis的list数据类型,但是发现这个类型好像不能一次性全部将数据放入,只能一条一条加到list中,我裂开,那这加到啥时候了。后来决定就用普通的string类型吧,然后考虑下序列化方式使用字符串还是json类型。最终选择了StringRedisSerializer序列化器
。
比较
先贴个Demo出来,后面咱们看调试结果。
redis的配置类,这边是定义了两种template,一种是jackson序列化的,一种是string的
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
@Bean(name = "redisTemplate")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
//参照StringRedisTemplate内部实现指定序列化器
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(keySerializer());
redisTemplate.setHashKeySerializer(keySerializer());
redisTemplate.setValueSerializer(valueSerializer());
redisTemplate.setHashValueSerializer(valueSerializer());
return redisTemplate;
}
@Bean(name = "redisTemplate2")
public RedisTemplate<String, Object> redisTemplate2(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
//参照StringRedisTemplate内部实现指定序列化器
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(keySerializer());
redisTemplate.setHashKeySerializer(keySerializer());
redisTemplate.setValueSerializer(keySerializer());
redisTemplate.setHashValueSerializer(keySerializer());
return redisTemplate;
}
private RedisSerializer<String> keySerializer(){
return new StringRedisSerializer();
}
//使用Jackson序列化器
private RedisSerializer<Object> valueSerializer(){
return new GenericFastJsonRedisSerializer();
}
}
接口,后面的业务层就不贴了,具体实现都写在controller里了。
@Resource(name = "redisTemplate")
private RedisTemplate<String, Object> redisTemplate;
@Resource(name = "redisTemplate2")
private RedisTemplate<String, Object> redisTemplate2;
@GetMapping("/addRedis1")
public Student addRedis1() {
StopWatch sw = new StopWatch();
sw.start();
List<Department> departments = studentService.listDept();
redisTemplate.opsForValue().set("depts1", departments, 5, TimeUnit.MINUTES);
sw.stop();
System.out.println("数据量:"+ departments.size() + "Jackson序列总计耗时" + sw.getTotalTimeMillis());
return null;
}
@GetMapping("/addRedis2")
public Student addRedis2() {
StopWatch sw = new StopWatch();
sw.start();
List<Department> departments = studentService.listDept();
redisTemplate2.opsForValue().set("depts2", JSON.toJSONString(departments), 5, TimeUnit.MINUTES);
sw.stop();
System.out.println("数据量:"+ departments.size() + "String序列总计耗时" + sw.getTotalTimeMillis());
return null;
}
事不宜迟,抓紧进行测试吧。每个接口调用10次,咱们看表说话。
jackson的序列化十次放入redis
string序列化十次放入redis
看表
第一次 | 第二次 | 第三次 | 第四次 | 第五次 | 第六次 | 第七次 | 第八次 | 第九次 | 第十次 | 平均 | |
---|---|---|---|---|---|---|---|---|---|---|---|
Jackson | 7705 | 2889 | 5695 | 2536 | 5526 | 2482 | 3529 | 5348 | 5467 | 2288 | 4346.5 |
string | 1123 | 1116 | 1071 | 1160 | 1167 | 1212 | 4649 | 1163 | 1155 | 4539 | 1835.5 |
这差距,离谱了。string序列化省2倍多时间,并且稳定性比jackson也更好一点。
读取数据代码,这边都是用的json的parseArray来解析json字符串。
@GetMapping("/getRedis1")
public Student getRedis1() {
StopWatch sw = new StopWatch();
sw.start();
List<Department> o = JSON.parseArray(redisTemplate2.opsForValue().get("depts1").toString(), Department.class);
sw.stop();
System.out.println("数据量:"+ o.size() + "Jackson序列总计耗时" + sw.getTotalTimeMillis());
return null;
}
@GetMapping("/getRedis2")
public Student getRedis2() {
StopWatch sw = new StopWatch();
sw.start();
List<Department> o = JSON.parseArray(redisTemplate2.opsForValue().get("depts2").toString(), Department.class);
sw.stop();
System.out.println("数据量:"+ o.size() + "String序列总计耗时" + sw.getTotalTimeMillis());
return null;
}
这两种序列化器其实并不局限于某一种,可以根据数据的实际情况进行选择,甚至可以设置多个template来进行使用。需要知道:redis中单个key和value的最大值都是512m,如果数据量过多的情况下请谨慎选择,如上例:或许可以将3w左右数据的给拆分为几个集合,然后分别存到几个key中
配置类、工具类
最后提供一个redis的配置类和工具类,方便后续使用时及时查找。
配置类
@Configuration
@EnableCaching
public class RedisConfig {
@Autowired
private RedisConnectionFactory factory;
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
// 序列化方式自行选择,String或是Jackson,FastJson也可以
redisTemplate.setHashValueSerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(factory);
return redisTemplate;
}
@Bean
public ValueOperations<String, String> valueOperations(RedisTemplate<String, String> redisTemplate) {
return redisTemplate.opsForValue();
}
}
redis工具类,此工具类中获取之后用到了Gson工具来进行解析数据,也可以自行更换为其他的如fastjson等。
@Component
public class RedisUtils {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private ValueOperations<String, String> valueOperations;
/**
* 默认过期时长,单位:秒
*/
public final static long DEFAULT_EXPIRE = 60 * 60 * 24;
/**
* 默认过期时长,单位:秒
*/
public final static long DEFAULT_URL_EXPIRE = 60;
/**
* 五分钟
*/
public final static long FIVE_MIN = 60 * 5;
/**
* 不设置过期时长
*/
public final static long NOT_EXPIRE = -1;
private final static Gson gson = new Gson();
public void set(String key, Object value, long expire) {
valueOperations.set(key, toJson(value));
if (expire != NOT_EXPIRE) {
redisTemplate.expire(key, expire, TimeUnit.SECONDS);
}
}
public void set(String key, Object value) {
set(key, value, DEFAULT_EXPIRE);
}
public void setT(String key, Object value) {
set(key, value, DEFAULT_URL_EXPIRE);
}
public <T> T get(String key, Class<T> clazz, long expire) {
String value = valueOperations.get(key);
if (expire != NOT_EXPIRE) {
redisTemplate.expire(key, expire, TimeUnit.SECONDS);
}
return value == null ? null : fromJson(value, clazz);
}
public <T> T get(String key, Class<T> clazz) {
return get(key, clazz, NOT_EXPIRE);
}
public String get(String key, long expire) {
String value = valueOperations.get(key);
if (expire != NOT_EXPIRE) {
redisTemplate.expire(key, expire, TimeUnit.SECONDS);
}
return value;
}
public String get(String key) {
return get(key, NOT_EXPIRE);
}
public String getT(String key) {
return get(key, DEFAULT_URL_EXPIRE);
}
public void delete(String key) {
redisTemplate.delete(key);
}
/**
* Object转成JSON数据
*/
private String toJson(Object object) {
if (object instanceof Integer || object instanceof Long || object instanceof Float ||
object instanceof Double || object instanceof Boolean || object instanceof String) {
return String.valueOf(object);
}
return gson.toJson(object);
}
/**
* JSON数据,转成Object
*/
private <T> T fromJson(String json, Class<T> clazz) {
return gson.fromJson(json, clazz);
}
}
任何技术选型都是需要根据项目的实际情况不断尝试和探索,不可能一步到位。警醒自己:使用任何技术前考虑实际情况!!!