整合Redis以及Redis的序列化

整合Redis以及Redis的序列化

最近做一个项目,考虑到性能问题决定整合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,所以一会在自动注入的时候千万不要写成RedisTemplate,否则SpringBoot 会报错!!!

接下来写个测试

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的第一个泛型是key的类型,是Object类型的,所以我们没办法使用String类型的键来获取对应的值,怎么办?
此时就是自动配置没法满足个人需求,好,那么就写个配置类来弄一个符合我们使用需求的RedisTemplate
该怎么写呢?
自然是仿照RedisAutoConfiguration类来写了

/*
一个比较简单的配置类
*/
@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个需要配置的序列化器,分别是:

  1. keySerializer 键序列化器
  2. valueSerializer 值序列化器
  3. hashKeySerializer 哈希键序列化器
  4. hashValueSerializer 哈希值序列化器

网上很多同学会把序列化器逐个设置
就像这样

	@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"

哦可,大功告成!

你可能感兴趣的:(#,Spring,Boot,#,Maven,JavaEE)