摘要: 原创出处 http://www.iocoder.cn/Spring-Boot/Redis/ 「芋道源码」欢迎转载,保留摘要,谢谢!
在快速入门 Spring Boot 整合 Redis 之前,我们先来做个简单的了解。在 Spring 的生态中,我们使用 Spring Data Redis 来实现对 Redis 的数据访问。
可能这个时候,会有胖友会有疑惑,市面上已经有 Redis、Redisson、Lettuce 等优秀的 Java Redis 工具库,为什么还要有 Spring Data Redis 呢?学不动了,头都要秃了!不要慌,我们先来看一张图:
Spring Data Redis 调用
OK ,哔哔结束,我们先来快速上手下 Spring Data Redis 的使用。
示例代码对应仓库: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 的对比讨论。
在 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>
具体每个依赖的作用,胖友自己认真看下艿艿添加的所有注释噢。
在 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 ,表示不限制。
具体每个参数的作用,胖友自己认真看下艿艿添加的所有注释噢。
创建 Test01 测试类,我们来测试一下简单的 SET 指令。代码如下:
@RunWith(SpringRunner.class)
@SpringBootTest
public class Test01 {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
public void testStringSetKey() {
stringRedisTemplate.opsForValue().set("yunai", "shuai");
}
}
通过 StringRedisTemplate 类,我们进行了一次 Redis SET 指令的执行。关于 StringRedisTemplate 是什么,我们先卖个关子,在 「2.4 RedisTemplate」 中来介绍。
我们先来执行下 #testStringSetKey()
方法这个测试方法。执行完成后,我们在控制台查询,看看是否真的执行成功了。
$ redis-cli get yunai
"shuai"
"yunai"
的,哈哈哈哈。org.springframework.data.redis.core.RedisTemplate<K, V>
类,从类名上,我们就明明白白知道,提供 Redis 操作模板 API 。核心属性如下:
// RedisTemplate.java
// 艿艿省略了一些不重要的属性。
// <1> 序列化相关属性
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer keySerializer = null;
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer valueSerializer = null;
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer hashKeySerializer = null;
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer hashValueSerializer = null;
private RedisSerializer<String> stringSerializer = RedisSerializer.string();
// <2> Lua 脚本执行器
private @Nullable ScriptExecutor<K> scriptExecutor;
// <3> 常见数据结构操作类
// cache singleton objects (where possible)
private @Nullable ValueOperations<K, V> valueOps;
private @Nullable ListOperations<K, V> listOps;
private @Nullable SetOperations<K, V> setOps;
private @Nullable ZSetOperations<K, V> zSetOps;
private @Nullable GeoOperations<K, V> geoOps;
private @Nullable HyperLogLogOperations<K, V> hllOps;
<1>
处,看到了四个序列化相关的属性,用于 KEY 和 VALUE 的序列化。<2>
处,Lua 脚本执行器,提供 Redis scripting API 操作。<3>
处,Redis 常见数据结构操作类。那么 Pub/Sub、Transaction、Pipeline、Keys、Cluster、Connection 等相关的 API 操作呢?它在 RedisTemplate 自身提供,因为它们不属于具体每一种数据结构,所以没有封装在对应的 Operations 类中。哈哈哈,胖友打开 RedisTemplate 类,去瞅瞅,妥妥的明白。
艿艿:为了尽量把序列化说的清楚一些,所以本小节内容会略长。 因为有些地方,直接撸源码,比吓哔哔一段话更易懂,所以会有一些源码,保持淡定。
org.springframework.data.redis.serializer.RedisSerializer
接口,Redis 序列化接口,用于 Redis KEY 和 VALUE 的序列化。简化代码如下:
// RedisSerializer.java
public interface RedisSerializer<T> {
@Nullable
byte[] serialize(@Nullable T t) throws SerializationException;
@Nullable
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 的实现类,如下图:
Spring Data Redis 调用
主要分成四类:
org.springframework.data.redis.serializer.JdkSerializationRedisSerializer
,默认情况下,RedisTemplate 使用该数据列化方式。具体的,可以看看 RedisTemplate#afterPropertiesSet()
方法,在 RedisTemplate 未设置序列化的情况下,使用 JdkSerializationRedisSerializer 作为序列化实现。在 Spring Boot 自动化配置 RedisTemplate Bean 对象时,就未设置。
绝大多数情况下,可能 99.9999% ,我们不会使用 JdkSerializationRedisSerializer 进行序列化。为什么呢?我们来看一个示例,代码如下:
// Test01.java
@RunWith(SpringRunner.class)
@SpringBootTest
public class Test01 {
@Autowired
private RedisTemplate redisTemplate;
@Test
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"
"yunai"
KEY ,前面带着奇怪的 16 进制字符。而后,我们使用这个奇怪的 KEY 去获取对应的 VALUE ,结果前面也是一串奇怪的 16 进制字符。
具体为什么是这样一串奇怪的 16 进制,胖友可以看看 ObjectOutputStream#writeString(String str, boolean unshared)
的代码,实际就是标志位 + 字符串长度 + 字符串内容。对于 KEY 被序列化成这样,我们线上通过 KEY 去查询对应的 VALUE 势必会非常不方便,所以 KEY 肯定是不能被这样序列化的。
对于 VALUE 被序列化成这样,除了阅读可能困难一点,不支持跨语言外,实际上也没啥问题。不过,实际线上场景,还是使用 JSON 序列化居多。
① org.springframework.data.redis.serializer.StringRedisSerializer
,字符串和二进制数组的直接转换。代码如下:
// StringRedisSerializer.java
private final Charset charset;
@Override
public String deserialize(@Nullable byte[] bytes) {
return (bytes == null ? null : new String(bytes, charset));
}
@Override
public byte[] serialize(@Nullable 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 貌似基本不会去使用,所以不用去了解也问题不大,哈哈哈。
① 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 类。
@class
属性,反序列化的对象的类型不就有了么?下面我们来看一个 GenericJackson2JsonRedisSerializer 的示例。在看之前,胖友先跳到 「3.2 配置序列化方式」 小节,来看看如何配置 GenericJackson2JsonRedisSerializer 作为 VALUE 的序列化方式。然后,马上调回到此处。
示例代码如下:
// Test01.java
@Autowired
private RedisTemplate redisTemplate;
@Test
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);
}
@Test
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 自己实现。
org.springframework.data.redis.serializer.OxmSerializer
,使用 Spring OXM 实现将对象和 String 的转换,从而 String 和二进制数组的转换。
因为 XML 序列化方式,暂时没有这么干过,我自己也没有,所以就直接忽略它吧。?
创建 RedisConfiguration 配置类,代码如下:
@Configuration
public class RedisConfiguration {
@Bean
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(); }我们直接以 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);
}
}
}