Lettuce 和 Jedis、Redisson
RedisTemplate 是 SpringDataRedis 中对 JedisApi 的高度封装,提供了 Redis 各种操作、 异常处理及序列化,支持发布订阅。
首先我们要知道SpringData 是 Spring 中数据操作的模块,包括对各种数据库的集成,比如我们之前学过的 Spring Data JDBC、JPA 等,其中有一个模块叫做Spring Data Redis ,而RedisTemplate 就是其中提供操作 Redis 的通用模板。
Spring Data Redis 中提供了如下的内容:
1、对不同 Redis 客户端的整合(Lettuce 和 Jedis、Redisson)
2、提供了 RedisTemplate 统一 API 操作 Redis
3、⽀持 Redis 订阅发布模型
4、⽀持 Redis 哨兵和集群
5、⽀持基于 Lettuce 的响应式编程(底层就是 Netty)
6、⽀持基于 JDK、JSON、字符串、Spring 对象的数据序列化、反序列化
使用 Spring Data Redis 需要引入 RedisTemplate 依赖和 commons-pool 连接池依赖,Jedis 与 RedisTemplate 底层使用的连接池都是 commons-pool2,所以需要导入它
1
2
3
4
5
6
7
8
|
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
|
这⾥我们可以看⼀下 spring-boot-starter-data-redis 底层,发现并没有引入 Jedis

原因:
在 SpringBoot 2.x 版本以后,从原来的 Jedis 替换成了 lettuce,所以 2.x 以后开始默认使用 Lettuce 作为 Redis 客户端,Lettuce 客户端基于 Netty 的 NIO 框架实现,只需要维持单一的连接(非阻塞式 IO)即可高效支持业务端并发请求。同时,Lettuce 支持的特性更加全面,其性能表现并不逊于,甚至优于 Jedis。
简单理解:
- Jedis:
采用的直连,多线程操作不安全 ,如果想要避免线程安全问题,就需要使用 JedisPool 连接池,但是也会有一些线程过多等其他问题,类似于BIO(阻塞式 IO)
- Lettuce:
单线程实现,线程安全,底层采用 Netty,实例可以在多个线程中进行共享,不存在线程安全问题!类似 NIO
- Redisson:支持红锁、分布式锁等
默认的 RedisTemplate 测试
1
2
3
4
5
6
7
8
9
10
11
|
@SpringBootTest
class RedisTestApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Test
void contextLoads() {
redisTemplate.opsForValue().set("CSDN","青秋.");
System.out.println(redisTemplate.opsForValue().get("CSDN"));
}
}
|
通过指令来查看发现是乱码,这就涉及到了序列化的问题了。

想要解决以上问题,需要了解 RedisTemplate 序列化的问题,首先进入 RedisTemplate 源码,发现需要设置 key、hashKey 和 value、hashValue 的序列化器

再往下看有一个默认的序列化器

也就是说,RedisTemplate 默认采用的是默认的 JDK 序列化器,这种序列化方式会有一定的问题 比如可读性差、内存占用大
所以总结来说,我们可以修改 key 和 value 的 RedisSerializer 具体实现,这⾥我们可以先看⼀下 RedisSerializer 的实现类有哪些:

- JacksonJsonRedisSerializer: 序列化 object 对象为 json 字符串
- Jackson2JsonRedisSerializer: 跟 JacksonJsonRedisSerializer 实际上是一样的
- JdkSerializationRedisSerializer: 序列化 java 对象
- GenericToStringSerializer: 可以将任何对象泛化为字符串并序列化
- StringRedisSerializer: 简单的字符串序列化
各个序列化器性能测试对比
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
@Test
public void testSerial(){
UserPO userPO = new UserPO(1111L,"小明_testRedis1",25);
List<Object> list = new ArrayList<>();
for(int i=0;i<200;i++){
list.add(userPO);
}
JdkSerializationRedisSerializer j = new JdkSerializationRedisSerializer();
GenericJackson2JsonRedisSerializer g = new GenericJackson2JsonRedisSerializer();
Jackson2JsonRedisSerializer j2 = new Jackson2JsonRedisSerializer(List.class);
Long j_s_start = System.currentTimeMillis();
byte[] bytesJ = j.serialize(list);
System.out.println("JdkSerializationRedisSerializer序列化时间:"+(System.currentTimeMillis()-j_s_start) + "ms,序列化后的长度:" + bytesJ.length);
Long j_d_start = System.currentTimeMillis();
j.deserialize(bytesJ);
System.out.println("JdkSerializationRedisSerializer反序列化时间:"+(System.currentTimeMillis()-j_d_start));
Long g_s_start = System.currentTimeMillis();
byte[] bytesG = g.serialize(list);
System.out.println("GenericJackson2JsonRedisSerializer序列化时间:"+(System.currentTimeMillis()-g_s_start) + "ms,序列化后的长度:" + bytesG.length);
Long g_d_start = System.currentTimeMillis();
g.deserialize(bytesG);
System.out.println("GenericJackson2JsonRedisSerializer反序列化时间:"+(System.currentTimeMillis()-g_d_start));
Long j2_s_start = System.currentTimeMillis();
byte[] bytesJ2 = j2.serialize(list);
System.out.println("Jackson2JsonRedisSerializer序列化时间:"+(System.currentTimeMillis()-j2_s_start) + "ms,序列化后的长度:" + bytesJ2.length);
Long j2_d_start = System.currentTimeMillis();
j2.deserialize(bytesJ2);
System.out.println("Jackson2JsonRedisSerializer反序列化时间:"+(System.currentTimeMillis()-j2_d_start));
}
|
测试结果
1
2
3
4
5
6
|
JdkSerializationRedisSerializer序列化时间:8ms,序列化后的长度:1325
JdkSerializationRedisSerializer反序列化时间:4
GenericJackson2JsonRedisSerializer序列化时间:52ms,序列化后的长度:17425
GenericJackson2JsonRedisSerializer反序列化时间:60
Jackson2JsonRedisSerializer序列化时间:4ms,序列化后的长度:9801
Jackson2JsonRedisSerializer反序列化时间:4
|
- JdkSerializationRedisSerializer 序列化后长度最小,Jackson2JsonRedisSerializer 效率最高。
- 如果综合考虑效率和可读性,牺牲部分空间,推荐 key 使用 StringRedisSerializer,保持的 key 简明易读;value 可以使用 Jackson2JsonRedisSerializer
- 如果空间比较敏感,效率要求不高,推荐 key 使用 StringRedisSerializer,保持的 key 简明易读;value 可以使用 JdkSerializationRedisSerializer
自定义 RedisTemplate
新建 RedisConfig 配置类,以下是固定模板,可以直接用
这个模板我们采用的 Json 序列化 Value,String 序列化 Key
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String,Object>
redisTemplate(RedisConnectionFactory factory){
// 为了研发⽅便 key直接为String类型
RedisTemplate<String,Object> template = new RedisTemplate<>();
// 设置连接⼯⼚
template.setConnectionFactory(factory);
//设置key序列化 用的string序列化
template.setKeySerializer(RedisSerializer.string());
template.setHashKeySerializer(RedisSerializer.string());
//序列化配置,通过JSON解析任意对象
GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
//设置value序列化,采用的是Json序列化方式
template.setValueSerializer(jsonRedisSerializer);
template.setHashKeySerializer(jsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
|
Json 序列化 Value + RedisTemplate 测试
1
2
3
4
5
6
7
8
9
10
|
@SpringBootTest
class RedisTestApplicationTests {
@Autowired
private RedisTemplate<String,Object> redisTemplate;
@Test
void contextLoads() {
redisTemplate.opsForValue().set("CSDN","青秋.");
System.out.println(redisTemplate.opsForValue().get("CSDN"));
}
}
|
此时用 keys * 查看,没有乱码。那么再储存一个对象试试!
1
2
3
4
5
|
@Test
void saveUser(){
redisTemplate.opsForValue().set("stringredistemplate",new User("Mask",20));
System.out.println(redisTemplate.opsForValue().get("stringredistemplate"));
}
|
用可视化工具查看,发现JSON 序列化 Value 后多了个@Class 字段

虽然实现了对象的序列化和反序列化,但这是因为添加了@class 字段,会导致额外的内存开销,在数据量特别大的时候就会有影响,但是如果没有@class 就不会实现自动序列化和反序列化。
实际开发中,如果为了节省空间,并不会完全使用 JSON 序列化来处理 value, 而是统一采用 String 序列化器,储存 Java 对象也是如此,这就意味着我们需要重新编写 RedisTemplate,但是 SpringBoot 其实提供了一个 String 序列化器实现的 StringRedisTemplate,通过它可以完成以上的需求。

String 序列化 Value + StringRedisTemplate 测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
@SpringBootTest
class RedisTestApplicationTests {
@Autowired
private StringRedisTemplate stringRedisTemplate;
//Json⼯具
private ObjectMapper mapper = new ObjectMapper();
@Test
void StringTemplate() throws JsonProcessingException {
User user = new User("青秋",18);
//⼿动序列化
String json = mapper.writeValueAsString(user);
//写⼊数据
stringRedisTemplate.opsForValue().set("stringredistemplate",json);
//读取数据
String val =
stringRedisTemplate.opsForValue().get("stringredistemplate");
//反序列化
User u = mapper.readValue(val,User.class);
System.out.println(u);
}
}
|
总结
- RedisTemplate 的 Key 和 Value 的序列化器可以根据需要分别设置。
- RedisTemplate 默认使用 JdkSerializationRedisSerializer 存入数据,会将数据先序列化成字节数组然后在存入 Redis 数据库,这种 value 不可读。
- 如果数据是 Object 类型,取出的时候又不想做任何的数据转换,直接从 Redis 里面取出一个对象,那么使用 RedisTemplate 是更好的选择。
- 当然任何情况下从 Redis 获取数据的时候,都会默认将数据当做字节数组转化,这样就会导致一个问题:当需要获取的数据不是以字节数组存在 redis 当中,而是正常的可读的字符串的时候,RedisTemplate 就无法获取数据,获取到的值是 NULL。这时就需要用 StringRedisTempate 或者专门设置 RedisTemplate 的序列化器!