最近做一个项目,考虑到性能问题决定整合Redis
Spring Boot 2.2 目前最新版,尝个鲜;
首先是用Spring Initializr 向导,如果只使用redis那就只选redis的模块,最多再多选一个web模块,不要在向导里选多余的模块,否则会有更多的配置,不配置就有可能给你报错,为了简单起见,我这里只选了redis模块,这是Maven依赖部分
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
dependencies>
然后是application.yml配置文件
spring:
redis:
database: 0
host: 127.0.0.1
password: ''
port: 6379
非常简单的配置,没有什么大问题
Spring Boot 最大的好处就是自动配置功能,需要配置的功能写在配置文件或者配置类里,不需要写的功能就使用默认配置,那么Redis也一定有个自动配置类,我们来看看Redis的自动配置类都为我们配置了些什么
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
加了@Configuration,一个经典的配置类
这个配置类里为容器注入了两个Bean, RedisTemplate 与StringRedisTemplate,查看StringRedisTemplate源码发现StringRedisTemplate只是继承了RedisTemplate
注意!
SpringBoot 自动注入的泛型泛型是RedisTemplate
接下来写个测试
public class TestObject{
private String name;
private Integer code;
private String phone;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
}
测试类
//因为这里有些不同,所以贴一下导入的语句
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
//Spring Boot 2.2 的测试类不需要加 @RunWith(SpringRunner.class)
@SpringBootTest
class SpringbootRedisApplicationTests {,
@Autowired
RedisTemplate<Object, Object> redisTemplate;
@Autowired
StringRedisTemplate stringRedisTemplate;
@Test
void testStringRedisTemplate() {
stringRedisTemplate.opsForValue().append("string-object","value1");
}
}
结果报了一组错误,说是DefaultSerializer不能序列化TestObject的对象,什么鬼?立马打断点调试
org.springframework.data.redis.serializer.SerializationException: Cannot serialize; nested exception is org.springframework.core.serializer.support.SerializationFailedException: Failed to serialize object using DefaultSerializer; nested exception is java.lang.IllegalArgumentException: DefaultSerializer requires a Serializable payload but received an object of type [cn.edu.imau.zy.studentproduction.TestObject]
其余报错跳过...
搞了半天居然是TestObject类没有实现Serializable接口…不由得老脸一红
public class DefaultSerializer implements Serializer<Object> {
/**
* Writes the source object to an output stream using Java serialization.
* The source object must implement {@link Serializable}.
* @see ObjectOutputStream#writeObject(Object)
*/
@Override
public void serialize(Object object, OutputStream outputStream) throws IOException {
//就是这里的问题...
if (!(object instanceof Serializable)) {
throw new IllegalArgumentException(getClass().getSimpleName() + " requires a Serializable payload " +
"but received an object of type [" + object.getClass().getName() + "]");
}
ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
objectOutputStream.writeObject(object);
objectOutputStream.flush();
}
}
运行成功,然后使用redis-cli工具查看Redis
127.0.0.1:6379> FLUSHALL
OK
127.0.0.1:6379> keys *
1) "string-key"
127.0.0.1:6379> get string-key
"value"
127.0.0.1:6379>
好这样的话我们字符串操作就成功了!
接下来是操作Object类型,再写个测试方法
@Test
void testRedisTemplate(){
TestObject object = new TestObject();
object.setName("Object");
object.setPhone("233333333");
object.setCode(23333);
redisTemplate.opsForValue().set("object", object);
}
这样运行必定失败,报错信息
org.springframework.data.redis.RedisSystemException: Error in execution; nested exception is io.lettuce.core.RedisCommandExecutionException: WRONGTYPE Operation against a key holding the wrong kind of value
...
意思就是键的类型错误
RedisTemplate
/*
一个比较简单的配置类
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
}
再运行测试,成功!然后查看redis
127.0.0.1:6379> keys *
1) "\xac\xed\x00\x05t\x00\x06object"
2) "string-key"
127.0.0.1:6379> get "\xac\xed\x00\x05t\x00\x06object"
"\xac\xed\x00\x05sr\x00)cn.edu.imau.zy.springbootredis.TestObject\x7f\t;\xda\xd6\x96\xf5\xe8\x02\x00\x03L\x00\x04codet\x00\x13Ljava/lang/Integer;L\x00\x04namet\x00\x12Ljava/lang/String;L\x00\x05phoneq\x00~\x00\x02xpsr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00[%t\x00\x06Objectt\x00\t233333333"
虽然没问题了,但是这个这个序列化emmm…为啥把二进制数据都序列化成了unicode?
有点小毛病,经过查看RedisTemplate源码发现,如果不手动设置一个序列化器,RedisTemplate会设置一个默认序列化器
关键源码
/*很明显,这个方法是在我们所有的配置完成之后,剩下的那些没配置的,就由这里进行配置,
如果我们没有在Redis配置类中设置这些东西,那么这里就帮我们把RedisTemplate剩余的配置都设一个默认值*/
@Override
public void afterPropertiesSet() {
super.afterPropertiesSet();
boolean defaultUsed = false;
if (defaultSerializer == null) {
//就是这里
defaultSerializer = new JdkSerializationRedisSerializer(
classLoader != null ? classLoader : this.getClass().getClassLoader());
}
if (enableDefaultSerializer) {
if (keySerializer == null) {
keySerializer = defaultSerializer;
defaultUsed = true;
}
if (valueSerializer == null) {
valueSerializer = defaultSerializer;
defaultUsed = true;
}
if (hashKeySerializer == null) {
hashKeySerializer = defaultSerializer;
defaultUsed = true;
}
if (hashValueSerializer == null) {
hashValueSerializer = defaultSerializer;
defaultUsed = true;
}
}
if (enableDefaultSerializer && defaultUsed) {
Assert.notNull(defaultSerializer, "default serializer null and not all serializers initialized");
}
if (scriptExecutor == null) {
this.scriptExecutor = new DefaultScriptExecutor<>(this);
}
initialized = true;
}
问题找到了,但是用什么序列化器比较好呢?自带的肯定是不行的了,不过一说序列化,我们立马就会想到web中用到的Jackson,那么Jackson有没有实现redis序列化的接口呢?当然!
redis有4个需要配置的序列化器,分别是:
网上很多同学会把序列化器逐个设置
就像这样
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class)
// 逐个设置序列化器,比较麻烦
redisTemplate.setKeySerializer(jackson2JsonRedisSerializer);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
return redisTemplate;
}
但是我们之前看到RedisTemplate的afterPropertiesSet方法里有这样一段代码
if (enableDefaultSerializer) {
if (keySerializer == null) {
keySerializer = defaultSerializer;
defaultUsed = true;
}
if (valueSerializer == null) {
valueSerializer = defaultSerializer;
defaultUsed = true;
}
if (hashKeySerializer == null) {
hashKeySerializer = defaultSerializer;
defaultUsed = true;
}
if (hashValueSerializer == null) {
hashValueSerializer = defaultSerializer;
defaultUsed = true;
}
}
如果enableDefaultSerializer是在开启状态,那么defaultSerializer就可以同时用在4个序列化器上,
查看源代码,发现默认是开启状态,而且RedisTemplate也有让用户设置defautlSerializer的方法
public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperations<K, V>, BeanClassLoaderAware {
private boolean enableTransactionSupport = false;
private boolean exposeConnection = false;
private boolean initialized = false;
/*这里*/
private boolean enableDefaultSerializer = true;
private @Nullable RedisSerializer<?> defaultSerializer;
...
/*这里*/
public void setDefaultSerializer(RedisSerializer<?> serializer) {
this.defaultSerializer = serializer;
}
...
}
所以,我们的配置类现在变成了这样
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class)
redisTemplate.setDefaultSerializer(jackson2JsonRedisSerializer);
return redisTemplate;
}
}
当然,有些博客也这样写了
@Bean
public RedisTemplate<String, Object> jacksonRedisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//这里-{
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
//}
redisTemplate.setDefaultSerializer(new JdkSerializationRedisSerializer());
return redisTemplate;
}
一般情况下,使用默认的就可以了,如果有特殊需求,则再进行配置也没什么问题
再次运行测试,报错
Error:(19, 75) java: 无法访问com.fasterxml.jackson.databind.JavaType
找不到com.fasterxml.jackson.databind.JavaType的类文件
没有导入jackson,方便起见在maven引入导入spring-boot web模块 (Ps:web模块里已经包含了jackson的依赖)
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
然后,运行测试,成功。
查看redis
127.0.0.1:6379> keys *
1) "\"object\""
2) "\xac\xed\x00\x05t\x00\x06object"
3) "string-key"
127.0.0.1:6379> get "\"object\""
"{\"name\":\"Object\",\"code\":23333,\"phone\":\"233333333\"}"
成功!别急,还没完!用代码查看一下
@Test
void testRedisTemplate(){
TestObject object = new TestObject();
object.setName("Object");
object.setPhone("233333333");
object.setCode(23333);
redisTemplate.opsForValue().set("object", object);
System.out.println(redisTemplate.opsForValue().get("object"));
}
运行之…
{name=Object, code=23333, phone=233333333}
OK,但是…这年头用Jackson的人还多吗?都是换阿里的Fastjson了吧!
还是引入依赖
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.62version>
dependency>
哈哈哈,又是最新版的
所以我又尝试替换掉Jackson,使用阿里的Fastjson,但是据说集成Fastjson需要自己实现一个序列化器,
有兴趣的可以看看这篇文章 Redis使用FastJson序列化/FastJson2JsonRedisSerializer
我在集成的时候发现Fastjson从1.2.36就已经有了自己的Redis序列化器…并且在《Redis使用FastJson序列化/FastJson2JsonRedisSerializer》这篇文章的一些问题也已被修复,所以现在的配置类就变成了这样
import com.alibaba.fastjson.support.spring.FastJsonRedisSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import java.io.Serializable;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
FastJsonRedisSerializer fastJsonRedisSerializer = new FastJsonRedisSerializer(Object.class);
//这里-{ 这里是可选项
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setSerializerFeatures(SerializerFeature.WriteClassName);
fastJsonRedisSerializer.setFastJsonConfig(fastJsonConfig);
//}
redisTemplate.setDefaultSerializer(fastJsonRedisSerializer);
return redisTemplate;
}
}
修改测试类的方法
@Test
void testRedisTemplate(){
TestObject object = new TestObject();
object.setName("母鸡啊");
object.setPhone("233333333");
object.setCode(23333);
redisTemplate.opsForValue().set("good", object);
System.out.println(redisTemplate.opsForValue().get("good"));
}
运行之…成功
输出
{"code":23333,"phone":"233333333","name":"母鸡啊"}
查看redis
127.0.0.1:6379> keys *
1) "\"object\""
2) "\xac\xed\x00\x05t\x00\x06object"
3) "string-key"
哦可,大功告成!