使用Spring Cache + Redis + Jackson Serializer缓存数据库查询结果中序列化问题的处理

应用场景

我们希望通过缓存来减少对关系型数据库的查询次数,减轻数据库压力。在执行DAO类的select***(), query***()方法时,先从Redis中查询有没有缓存数据,如果有则直接从Redis拿到结果,如果没有再向数据库发起查询请求取数据。

序列化问题

要把对象做为key-value对保存在redis中,就必须要解决对象的序列化问题。Spring Data Redis给我们提供了一些现成的方案:

  • JdkSerializationRedisSerializer. 使用JDK提供的序列化功能。 优点是反序列化时不需要提供类型信息(class),但缺点是序列化后的结果非常庞大,是JSON格式的5倍左右,这样就会消耗redis服务器的大量内存。

  • Jackson2JsonRedisSerializer. 使用Jackson库将对象序列化为JSON字符串。优点是速度快,序列化后的字符串短小精悍。但缺点也非常致命,那就是此类的构造函数中有一个类型参数,必须提供要序列化对象的类型信息(.class对象)。 通过查看源代码,发现其只在反序列化过程中用到了类型信息。

  • GenericJackson2JsonRedisSerializer. 和 Jackson2JsonRedisSerializer 类似。但是它不需要提供序列化对象的类型信息。

分析

如果用方案一,就必须付出缓存多占用4倍内存的代价,实在承受不起。

如果用方案二,则必须给每一种domain对象都配置一个Serializer,即如果我的应用里有100种domain对象,那就必须在spring配置文件中配置100个Jackson2JsonRedisSerializer,这显然也是不现实的。

如果用方案三,就是为了解决Jackson必须提供类型信息的问题,可以同时支持多种不同类型的domain对象。

总结

所以选择使用GenericJackson2JsonRedisSerializer来配置序列化。

1
2
3
4
5
6
7
8
9
@Bean
public RedisCacheManager jsonCacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
}

Jackson2JsonRedisSerializer和GenericJackson2JsonRedisSerializer的区别

一、使用Jackson2JsonRedisSerializer序列化反序列化带泛型的List数据

1、使用Jackson2JsonRedisSerializer序列化value的代码

1
2
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer(Object.class));
1
2
3
4
5
6
7
8
9
10
11
User user = new User();
user.setUserId(1);
user.setUsername("张三");
List<User> userList = new ArrayList<>();
userList.add(user);
//不能直接将对象存储进redis中否则在进行反序列化的时候会报
// java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to com.lx.entity.User错误
//可以先转为json字符串再进行存储
String value = JSON.toJSONString(userList);
redisTemplate.opsForValue().set("jackson2JsonRedisSerializer", value, 5,
TimeUnit.MINUTES);

2、使用Jackson2JsonRedisSerializer序列化后的数据形式

1
2
3
4
5
6
[
{
"userId":1,
"username":"张三"
}
]

3、使用Jackson2JsonRedisSerializer反序列化时报错

1
2
List<User> userListRedis = redisTemplate.opsForValue().get("jackson2JsonRedisSerializer");
userListRedis.forEach(u -> System.out.println(JSON.toJSONString(u)));

错误信息

1
java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to com.lx.entity.User
  • 原因: 序列化带泛型的数据时,会以map的结构进行存储,反序列化时不能将map解析成对象。

4、解决方案:序列化存储时,转成JSON字符串
使用jackson或者fastjson都可以,我这里使用的是fastjson,
需要的依赖

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
User user = new User();
user.setUserId(1);
user.setUsername("张三");
List<User> userList = new ArrayList<>();
userList.add(user);

redisTemplate.opsForValue().set("jackson2JsonRedisSerializer", JSON.toJSONString(userList), 5, TimeUnit.MINUTES);

String res = (String) redisTemplate.opsForValue().get("jackson2JsonRedisSerializer");
JSON.parseArray(res, User.class).forEach(u -> System.out.println(JSON.toJSONString(u)));

二、使用GenericJackson2JsonRedisSerializer序列化反序列化带泛型的List数据

1、使用GenericJackson2JsonRedisSerializer序列化value的代码

1
2
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
1
2
3
4
5
6
User user = new User();
user.setUserId(1);
user.setUsername("李四");
List<User> userList = new ArrayList<>();
userList.add(user);
redisTemplate.opsForValue().set("genericJackson2JsonRedisSerializer",userList,5,TimeUnit.MINUTES);

2、使用GenericJackson2JsonRedisSerializer序列化后的数据形式

1
2
3
4
5
6
7
8
9
10
[
"java.util.ArrayList",
[
{
"@class": "com.lx.entity.User",
"userId": 1,
"username": "李四"
}
]
]

3、使用GenericJackson2JsonRedisSerializer可以正常反序列化

1
2
List<User> userListRedis = (List<User>) redisTemplate.opsForValue().get("genericJackson2JsonRedisSerializer");
userListRedis.forEach(u -> System.out.println(JSON.toJSONString(u)));

4、可以正常反序列化的原因
使用GenericJackson2JsonRedisSerializer序列化时,会保存序列化的对象的包名和类名,反序列化时以这个作为标示就可以反序列化成指定的对象。

5、也可以以JSON字符串保存

1
2
3
4
redisTemplate.opsForValue().set("genericJackson2JsonRedisSerializer",JSON.toJSONString(userList),5,TimeUnit.MINUTES);

List<User> userListRedis = (List<User>) redisTemplate.opsForValue().get("genericJackson2JsonRedisSerializer");
userListRedis.forEach(u -> System.out.println(JSON.toJSONString(u)));

四、GenericJackson2JsonRedisSerializer和Jackson2JsonRedisSerializerdo效率

1
2
3
4
5
6
7
8
9
10
User user = new User();
user.setUserId(1);
user.setUsername("李四");
List<User> userList = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
userList.add(user);
}
redisTemplate.opsForValue().set("serializer", JSON.toJSONString(userList),5,TimeUnit.MINUTES);
long end = System.currentTimeMillis();
System.out.println("Serializer序列化需要的时间:" + (end - start));

测试后:

1
2
jackson2JsonRedisSerializer序列化需要的时间:687
genericJackson2JsonRedisSerializer序列化需要的时间:22

总结

  • 使用Jackson2JsonRedisSerializer需要指明序列化的类Class,可以使用Obejct.class

  • 使用GenericJackson2JsonRedisSerializerJackson2JsonRedisSerializerdo都可以正常序列化非泛型数组对象。GenericJackson2JsonRedisSerializer也可以正常反序列化非泛型数组对象,但是Jackson2JsonRedisSerializerdo因为“序列化带泛型的数据时,会以map的结构进行存储,反序列化时不能将map解析成对象”,所以不能反序列化,解决办法: 存储以JSON字符串存储

  • 使用GenericJacksonRedisSerializerJackson2JsonRedisSerializer效率高

  • GenericJacksonRedisSerializerJackson2JsonRedisSerializer都是以JSON格式去存储数据,都可以作为Redis的序列化方式

demo测试地址

https://gitee.com/fengzxia/spring-boot-redis-cache/blob/master/src/test/java/com/lx/TestSerializer.java

参考

https://blog.csdn.net/neosmith/article/details/46800235
https://blog.csdn.net/bai_bug/article/details/81222519

作者: 只是学习学习
邮箱: fengzxia1000@163.com
原文地址: https://fengzxia.gitee.io/posts/f6f65c4b.html
版权声明: 商业转载请联系作者获得授权,非商业转载请注明出处。