芋道 Spring Boot Redis 入门(上)
点击上方“芋道源码”,选择“设为星标”
做积极的人,而不是积极废人!
源码精品专栏
摘要: 原创出处 http://www.iocoder.cn/Spring-Boot/Redis/ 「芋道源码」欢迎转载,保留摘要,谢谢!
1. 概述
2. 快速入门
2.1 引入依赖
2.2 配置文件
2.3 简单测试
2.4 RedisTemplate
3. 序列化
3.1 RedisSerializer
3.2 配置序列化方式
3.3 自定义 RedisSerializer 实现类
1. 概述
在快速入门 Spring Boot 整合 Redis 之前,我们先来做个简单的了解。在 Spring 的生态中,我们使用 Spring Data Redis 来实现对 Redis 的数据访问。
可能这个时候,会有胖友会有疑惑,市面上已经有 Redis、Redisson、Lettuce 等优秀的 Java Redis 工具库,为什么还要有 Spring Data Redis 呢?学不动了,头都要秃了!不要慌,我们先来看一张图:
对于下层,Spring Data Redis 提供了统一的操作模板(后文中,我们会看到是 RedisTemplate 类),封装了 Jedis、Lettuce 的 API 操作,访问 Redis 数据。所以,实际上,Spring Data Redis 内置真正访问的实际是 Jedis、Lettuce 等 API 操作。
对于上层,开发者学习如何使用 Spring Data Redis 即可,而无需关心 Jedis、Lettuce 的 API 操作。甚至,未来如果我们想将 Redis 访问从 Jedis 迁移成 Lettuce 来,无需做任何的变动。😈 相信很多胖友,在选择 Java Redis 工具库,也是有过烦恼的。
目前,Spring Data Redis 暂时只支持 Jedis、Lettuce 的内部封装,而 Redisson 是由 redisson-spring-data 来提供。
OK ,哔哔结束,我们先来快速上手下 Spring Data Redis 的使用。
2. 快速入门
示例代码对应仓库:spring-data-redis-with-jedis 。
在 spring-boot-starter-data-redis
项目 2.X 中,默认使用 Lettuce 作为 Java Redis 工具库,猜测是因为 Jedis 中间有一段时间诈尸,基本不太更新。
感兴趣的胖友可以看看 https://mvnrepository.com/artifact/redis.clients/jedis 地址,会发现 2016 年到 2018 年的 Jedis 更新频率。所幸,2018 年底又突然复活了。
考虑到自己项目中,使用 Jedis 为主,并且问了几个朋友,都是使用 Jedis ,并且有吐槽 Lettuce 坑多多,所以个人推荐的话,生产中还是使用 Jedis ,稳定第一。也因此,本节我们是 Spring Data Redis + Jedis 的组合。
同时,艿艿目前使用的 SkyWalking 中间件,暂时只支持 Jedis 的自动化的追踪,那么更加考虑使用 Jedis 啦。
这里在分享一个 Jedis 和 Lettuce 的对比讨论。
2.1 引入依赖
在 pom.xml
文件中,引入相关依赖。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<!-- 实现对 Spring Data Redis 的自动化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<!-- 去掉对 Lettuce 的依赖,因为 Spring Boot 优先使用 Lettuce 作为 Redis 客户端 -->
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 引入 Jedis 的依赖,这样 Spring Boot 实现对 Jedis 的自动化配置 -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<!-- 方便等会写单元测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 等会示例会使用 fastjson 作为 JSON 序列化的工具 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.61</version>
</dependency>
<!-- Spring Data Redis 默认使用 Jackson 作为 JSON 序列化的工具 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
具体每个依赖的作用,胖友自己认真看下艿艿添加的所有注释噢。
2.2 配置文件
在 application.yml
中,添加 Redis 配置,如下:
spring:
# 对应 RedisProperties 类
redis:
host: 127.0.0.1
port: 6379
password: # Redis 服务器密码,默认为空。生产中,一定要设置 Redis 密码!
database: 0 # Redis 数据库号,默认为 0 。
timeout: 0 # Redis 连接超时时间,单位:毫秒。
# 对应 RedisProperties.Jedis 内部类
jedis:
pool:
max-active: 8 # 连接池最大连接数,默认为 8 。使用负数表示没有限制。
max-idle: 8 # 默认连接数最小空闲的连接数,默认为 8 。使用负数表示没有限制。
min-idle: 0 # 默认连接池最小空闲的连接数,默认为 0 。允许设置 0 和 正数。
max-wait: -1 # 连接池最大阻塞等待时间,单位:毫秒。默认为 -1 ,表示不限制。
具体每个参数的作用,胖友自己认真看下艿艿添加的所有注释噢。
2.3 简单测试
创建 Test01 测试类,我们来测试一下简单的 SET 指令。代码如下:
public class Test01 {
private StringRedisTemplate stringRedisTemplate;
public void testStringSetKey() {
stringRedisTemplate.opsForValue().set("yunai", "shuai");
}
}
(SpringRunner.class)通过 StringRedisTemplate 类,我们进行了一次 Redis SET 指令的执行。关于 StringRedisTemplate 是什么,我们先卖个关子,在 「2.4 RedisTemplate」 中来介绍。
我们先来执行下 #testStringSetKey()
方法这个测试方法。执行完成后,我们在控制台查询,看看是否真的执行成功了。
redis-cli get yunai
"shuai"
请大声的告诉我,Redis 是怎么夸奖
"yunai"
的,哈哈哈哈。
2.4 RedisTemplate
org.springframework.data.redis.core.RedisTemplate<K, V>
类,从类名上,我们就明明白白知道,提供 Redis 操作模板 API 。核心属性如下:
// RedisTemplate.java
// 艿艿省略了一些不重要的属性。
// <1> 序列化相关属性
("rawtypes") private RedisSerializer keySerializer = null;
("rawtypes") private RedisSerializer valueSerializer = null;
("rawtypes") private RedisSerializer hashKeySerializer = null;
("rawtypes") private RedisSerializer hashValueSerializer = null;
private RedisSerializer<String> stringSerializer = RedisSerializer.string();
// <2> Lua 脚本执行器
private ScriptExecutor<K> scriptExecutor;
// <3> 常见数据结构操作类
// cache singleton objects (where possible)
private ValueOperations<K, V> valueOps;
private ListOperations<K, V> listOps;
private SetOperations<K, V> setOps;
private ZSetOperations<K, V> zSetOps;
private GeoOperations<K, V> geoOps;
private HyperLogLogOperations<K, V> hllOps;
<1>
处,看到了四个序列化相关的属性,用于 KEY 和 VALUE 的序列化。例如说,我们在使用 POJO 对象存储到 Redis 中,一般情况下,会使用 JSON 方式序列化成字符串,存储到 Redis 中。详细的,我们在 「3. 序列化」 小节中来说明。
在上文中,我们看到了 `org.springframework.data.redis.core.StringRedisTemplate` 类,它继承 RedisTemplate 类,使用 `org.springframework.data.redis.serializer.StringRedisSerializer` 字符串序列化方式。直接点开 StringRedisSerializer 源码,看下它的构造方法,瞬间明明白白。
<2>
处,Lua 脚本执行器,提供 Redis scripting API 操作。<3>
处,Redis 常见数据结构操作类。ValueOperations 类,提供 Redis String API 操作。
ListOperations 类,提供 Redis List API 操作。
SetOperations 类,提供 Redis Set API 操作。
ZSetOperations 类,提供 Redis ZSet(Sorted Set) API 操作。
GeoOperations 类,提供 Redis Geo API 操作。
HyperLogLogOperations 类,提供 Redis HyperLogLog API 操作。
那么 Pub/Sub、Transaction、Pipeline、Keys、Cluster、Connection 等相关的 API 操作呢?它在 RedisTemplate 自身提供,因为它们不属于具体每一种数据结构,所以没有封装在对应的 Operations 类中。哈哈哈,胖友打开 RedisTemplate 类,去瞅瞅,妥妥的明白。
3. 序列化
艿艿:为了尽量把序列化说的清楚一些,所以本小节内容会略长。
因为有些地方,直接撸源码,比吓哔哔一段话更易懂,所以会有一些源码,保持淡定。
3.1 RedisSerializer
org.springframework.data.redis.serializer.RedisSerializer
接口,Redis 序列化接口,用于 Redis KEY 和 VALUE 的序列化。简化代码如下:
// RedisSerializer.java
public interface RedisSerializer<T> {
byte[] serialize( T t) throws SerializationException;
T deserialize(@Nullable byte[] bytes) throws SerializationException;
}
定义了对象 和二进制数组的转换。
啊,可能有胖友会有疑惑了:我们在
redis-cli
终端,看到的不都是字符串么,怎么这里是序列化成二进制数组呢?实际上,Redis Client 传递给 Redis Server 是传递的 KEY 和 VALUE 都是二进制值数组。好奇的胖友,可以打开 Jedis `Connection#sendCommand(final Command cmd, final byte[]… args)` 方法,传入的参数就是二进制数组,而cmd
命令也会被序列化成二进制数组。
RedisSerializer 的实现类,如下图:
主要分成四类:
JDK 序列化方式
String 序列化方式
JSON 序列化方式
XML 序列化方式
3.1.1 JDK 序列化方式
org.springframework.data.redis.serializer.JdkSerializationRedisSerializer
,默认情况下,RedisTemplate 使用该数据列化方式。具体的,可以看看 RedisTemplate#afterPropertiesSet()
方法,在 RedisTemplate 未设置序列化的情况下,使用 JdkSerializationRedisSerializer 作为序列化实现。在 Spring Boot 自动化配置 RedisTemplate Bean 对象时,就未设置。
绝大多数情况下,可能 99.9999% ,我们不会使用 JdkSerializationRedisSerializer 进行序列化。为什么呢?我们来看一个示例,代码如下:
// Test01.java
(SpringRunner.class)
public class Test01 {
private RedisTemplate redisTemplate;
public void testStringSetKey02() {
redisTemplate.opsForValue().set("yunai", "shuai");
}
}
我们先来执行下 #testStringSetKey02()
方法这个测试方法。注意,此处我们使用的是 RedisTemplate 而不是 StringRedisTemplate 。执行完成后,我们在控制台查询,看看是否真的执行成功了。
在 `redis-cli` 终端中
127.0.0.1:6379> scan 0
1) "0"
2) 1) "\xac\xed\x00\x05t\x00\x05yunai"
127.0.0.1:6379> get "\xac\xed\x00\x05t\x00\x05yunai"
"\xac\xed\x00\x05t\x00\x05shuai"
通过 Redis SCAN 命令,我们扫描出了一个奇怪的
"yunai"
KEY ,前面带着奇怪的 16 进制字符。而后,我们使用这个奇怪的 KEY 去获取对应的 VALUE ,结果前面也是一串奇怪的 16 进制字符。具体为什么是这样一串奇怪的 16 进制,胖友可以看看
ObjectOutputStream#writeString(String str, boolean unshared)
的代码,实际就是标志位 + 字符串长度 + 字符串内容。
对于 KEY 被序列化成这样,我们线上通过 KEY 去查询对应的 VALUE 势必会非常不方便,所以 KEY 肯定是不能被这样序列化的。
对于 VALUE 被序列化成这样,除了阅读可能困难一点,不支持跨语言外,实际上也没啥问题。不过,实际线上场景,还是使用 JSON 序列化居多。
3.1.2 String 序列化方式
① org.springframework.data.redis.serializer.StringRedisSerializer
,字符串和二进制数组的直接转换。代码如下:
// StringRedisSerializer.java
private final Charset charset;
public String deserialize(@Nullable byte[] bytes) {
return (bytes == null ? null : new String(bytes, charset));
}
public byte[] serialize( String string) {
return (string == null ? null : string.getBytes(charset));
}
是不是很直接简单。
绝大多数情况下,我们 KEY 和 VALUE 都会使用这种序列化方案。而 VALUE 的序列化和反序列化,自己在逻辑调用 JSON 方法去序列化。为什么呢?继续往下看。
② org.springframework.data.redis.serializer.GenericToStringSerializer<T>
,使用 Spring ConversionService 实现 <T>
对象和 String 的转换,从而 String 和二进制数组的转换。
例如说,序列化的过程,首先 <T>
对象通过 ConversionService 转换成 String ,然后 String 再序列化成二进制数组。反序列化的过程,胖友自己结合源码思考下 🤔 。
当然,GenericToStringSerializer 貌似基本不会去使用,所以不用去了解也问题不大,哈哈哈。
3.1.3 JSON 序列化方式
① org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer
,使用 Jackson 实现 JSON 的序列化方式,并且从 Generic 单词可以看出,是支持所有类。怎么体现呢?参见构造方法的代码:
// GenericJackson2JsonRedisSerializer.java
public GenericJackson2JsonRedisSerializer(@Nullable String classPropertyTypeName) {
this(new ObjectMapper());
// simply setting {@code mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)} does not help here since we need
// the type hint embedded for deserialization using the default typing feature.
mapper.registerModule(new SimpleModule().addSerializer(new NullValueSerializer(classPropertyTypeName)));
// <1>
if (StringUtils.hasText(classPropertyTypeName)) {
mapper.enableDefaultTypingAsProperty(DefaultTyping.NON_FINAL, classPropertyTypeName);
// <2>
} else {
mapper.enableDefaultTyping(DefaultTyping.NON_FINAL, As.PROPERTY);
}
}
<1>
处,如果传入了classPropertyTypeName
属性,就是使用使用传入对象的classPropertyTypeName
属性对应的值,作为默认类型(Default Typing)。<2>
处,如果未传入classPropertyTypeName
属性,则使用传入对象的类全名,作为默认类型(Default Typing)。
那么,胖友可能会问题,什么是默认类型(Default Typing)呢?我们来思考下,在将一个对象序列化成一个字符串,怎么保证字符串反序列化成对象的类型呢?Jackson 通过 Default Typing ,会在字符串多冗余一个类型,这样反序列化就知道具体的类型了。来举个例子,使用我们等会示例会用到的 UserCacheObject 类。
标准序列化的结果,如下:
{
"id": 1,
"name": "芋道源码",
"gender": 1
}使用 Jackson Default Typing 机制序列化的结果,如下:
{
"@class": "cn.iocoder.springboot.labs.lab10.springdatarediswithjedis.cacheobject.UserCacheObject",
"id": 1,
"name": "芋道源码",
"gender": 1
}看
@class
属性,反序列化的对象的类型不就有了么?
下面我们来看一个 GenericJackson2JsonRedisSerializer 的示例。在看之前,胖友先跳到 「3.2 配置序列化方式」 小节,来看看如何配置 GenericJackson2JsonRedisSerializer 作为 VALUE 的序列化方式。然后,马上调回到此处。
示例代码如下:
// Test01.java
private RedisTemplate redisTemplate;
public void testStringSetKeyUserCache() {
UserCacheObject object = new UserCacheObject()
.setId(1)
.setName("芋道源码")
.setGender(1); // 男
String key = String.format("user:%d", object.getId());
redisTemplate.opsForValue().set(key, object);
}
public void testStringGetKeyUserCache() {
String key = String.format("user:%d", 1);
Object value = redisTemplate.opsForValue().get(key);
System.out.println(value);
}
胖友分别执行 #testStringSetKeyUserCache()
和 #testStringGetKeyUserCache()
方法,然后对着 Redis 的结果看看,比较简单,就不多哔哔了。
我们在回过头来看看 @class
属性,它看似完美解决了反序列化后的对象类型,但是带来 JSON 字符串占用变大,所以实际项目中,我们也并不会采用 Jackson2JsonRedisSerializer 类。
② org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer<T>
,使用 Jackson 实现 JSON 的序列化方式,并且显示指定 <T>
类型。代码如下:
// Jackson2JsonRedisSerializer.java
public class Jackson2JsonRedisSerializer<T> implements RedisSerializer<T> {
// ... 省略不重要的代码
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
/**
* 指定类型,和 <T> 要一致。
*/
private final JavaType javaType;
private ObjectMapper objectMapper = new ObjectMapper();
}
因为 Jackson2JsonRedisSerializer 序列化类里已经声明了类型,所以序列化的 JSON 字符串,无需在存储一个 @class
属性,用于存储类型。
但是,我们抠脚一想,如果使用 Jackson2JsonRedisSerializer 作为序列化实现类,那么如果我们类型比较多,岂不是每个类型都要定义一个 RedisTemplate Bean 了?!所以实际场景下,我们也并不会使用 Jackson2JsonRedisSerializer 类。😈
③ com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer
,使用 FastJSON 实现 JSON 的序列化方式,和 GenericJackson2JsonRedisSerializer 一致,就不重复赘述。
注意,GenericFastJsonRedisSerializer 不是 Spring Data Redis 内置实现,而是由于 FastJSON 自己实现。
④ com.alibaba.fastjson.support.spring.FastJsonRedisSerializer<T>
,使用 FastJSON 实现 JSON 的序列化方式,和 Jackson2JsonRedisSerializer 一致,就不重复赘述。
注意,GenericFastJsonRedisSerializer 不是 Spring Data Redis 内置实现,而是由于 FastJSON 自己实现。
3.1.4 XML 序列化方式
org.springframework.data.redis.serializer.OxmSerializer
,使用 Spring OXM 实现将对象和 String 的转换,从而 String 和二进制数组的转换。
因为 XML 序列化方式,暂时没有这么干过,我自己也没有,所以就直接忽略它吧。😝
3.2 配置序列化方式
创建 RedisConfiguration 配置类,代码如下:
public class RedisConfiguration {
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
// 创建 RedisTemplate 对象
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 设置 RedisConnection 工厂。😈 它就是实现多种 Java Redis 客户端接入的秘密工厂。感兴趣的胖友,可以自己去撸下。
template.setConnectionFactory(factory);
// 使用 String 序列化方式,序列化 KEY 。
template.setKeySerializer(RedisSerializer.string());
// 使用 JSON 序列化方式(库是 Jackson ),序列化 VALUE 。
template.setValueSerializer(RedisSerializer.json());
return template;
}
}
RedisSerializer#string()
静态方法,返回的就是使用 UTF-8 编码的 StringRedisSerializer 对象。代码如下:// RedisSerializer.java
static RedisSerializer<String> string() {
return StringRedisSerializer.UTF_8;
}
// StringRedisSerializer.java
public static final StringRedisSerializer ISO_8859_1 = new StringRedisSerializer(StandardCharsets.ISO_8859_1);RedisSerializer#json()
静态方法,返回 GenericJackson2JsonRedisSerializer 对象。代码如下:// RedisSerializer.java
static RedisSerializer<Object> json() {
return new GenericJackson2JsonRedisSerializer();
}
3.3 自定义 RedisSerializer 实现类
我们直接以 GenericFastJsonRedisSerializer 举例子,直接莽源码。代码如下:
// GenericFastJsonRedisSerializer.java
public class GenericFastJsonRedisSerializer implements RedisSerializer<Object> {
private final static ParserConfig defaultRedisConfig = new ParserConfig();
static { defaultRedisConfig.setAutoTypeSupport(true);}
public byte[] serialize(Object object) throws SerializationException {
// 空,直接返回空数组
if (object == null) {
return new byte[0];
}
try {
// 使用 JSON 进行序列化成二进制数组,同时通过 SerializerFeature.WriteClassName 参数,声明写入类全名。
return JSON.toJSONBytes(object, SerializerFeature.WriteClassName);
} catch (Exception ex) {
throw new SerializationException("Could not serialize: " + ex.getMessage(), ex);
}
}
public Object deserialize(byte[] bytes) throws SerializationException {
// 如果为空,则返回空对象
if (bytes == null || bytes.length == 0) {
return null;
}
try {
// 使用 JSON 解析成对象。
return JSON.parseObject(new String(bytes, IOUtils.UTF8), Object.class, defaultRedisConfig);
} catch (Exception ex) {
throw new SerializationException("Could not deserialize: " + ex.getMessage(), ex);
}
}
}
完成自定义 RedisSerializer 配置类后,我们就可以参照 「3.2 配置序列化方式」 小节,将 VALUE 序列化的修改成我们的,哈哈哈。
欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢:
已在知识星球更新源码解析如下:
如果你喜欢这篇文章,喜欢,转发。
生活很美好,明天见(。・ω・。)ノ♡