前言
在上一篇文章中,我們完成了SpringBoot整合Redis進行數據緩存管理的工作,但緩存管理的實體類數據使用的是JDK序列化方式(如下圖所示),不便於使用可視化管理工具進行查看和管理。
接下來分別針對基於API的Redis緩存實現和基於注解的Redis緩存實現中的數據序列化機制進行介紹,並自定義JSON格式的數據序列化方式進行數據緩存管理。
基於API的Redis緩存實現——自定義RedisTemplate
1、Redis API默認序列化方式源碼解析
基於API的Redis緩存實現是使用RedisTemplate模板進行數據緩存操作的,查看RedisTemplate的源碼信息:
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; private @Nullable ClassLoader classLoader; // 聲明了key、value的各種序列化方式,初始值為空 @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; ... /* * 進行默認序列化方式設置,設置為JDK序列化方式 * (non-Javadoc) * @see org.springframework.data.redis.core.RedisAccessor#afterPropertiesSet() */ @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; } ... }
從上述RedisTemplate核心源碼可以看出,在RedisTemplate內部聲明了緩存數據key、value的各種序列化方式,各種初始值都為空;在afterPropertiesSet()方法中,判斷如果默認序列化參數defaultSerializer為空,則將數據的默認序列化方式設置為JdkSerializationRedisSerializer。
根據上述源碼信息可得出以下兩個重要結論:
(1)使用RedisTemplate進行Redis數據緩存操作時,內部默認使用的是JdkSerializationRedisSerializer序列化方式,所以進行數據緩存的實體類必須實現JDK自帶的序列化接口(例如Serializable);
(2)使用RedisTemplate進行Redis數據緩存操作時,如果自定義了緩存序列化方式defaultSerializer,那么將使用自定義的序列化方式。
另外,在RedisTemplate類的源碼中,看到的緩存數據key、value的各種序列化類型都是RedisSerializer。進入RedisSerializer查看RedisSerializer支持的序列化方式:
可以看到,RedisSerializer是一個Redis序列化接口,默認有6個實現類,這6個實現類代表了6種不同的數據序列化方式。其中,JdkSerializationRedisSerializer是JDK自帶的,也是RedisTemplate內部默認使用的序列化方式,開發者可以根據需要選擇其他支持的序列化方式(例如JSON方式)。
2、自定義RedisTemplate序列化機制
在項目中引入Redis依賴后,SpringBoot提供的RedisAutoConfiguration自動配置會生效。打開RedisAutoConfiguration類,查看內部源碼中關於RedisTemplate的定義方式:
package org.springframework.boot.autoconfigure.data.redis; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisOperations; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate; /** * {@link EnableAutoConfiguration Auto-configuration} for Spring Data's Redis support. * * @author Dave Syer * @author Andy Wilkinson * @author Christian Dupuis * @author Christoph Strobl * @author Phillip Webb * @author Eddú Meléndez * @author Stephane Nicoll * @author Marco Aust * @author Mark Paluch * @since 1.0.0 */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass(RedisOperations.class) @EnableConfigurationProperties(RedisProperties.class) @Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class }) public class RedisAutoConfiguration { @Bean @ConditionalOnMissingBean(name = "redisTemplate") @ConditionalOnSingleCandidate(RedisConnectionFactory.class) public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<Object, Object> template = new RedisTemplate<>(); template.setConnectionFactory(redisConnectionFactory); return template; } ... }
從上述RedisAutoConfiguration核心源碼中可以看出,在Redis自動配置類中,通過Redis連接工廠RedisConnectionFactory初始化了一個RedisTemplate;在該方法上方添加了一個@ConditionalOnMissingBean注解(顧名思義,當某個Bean不存在時生效),用來表明如果開發者自定義了一個名為redisTemplate的Bean,那么該默認初始化的RedisTemplate就不會生效。
如果要使用自定義序列化方式的RedisTemplate進行數據緩存操作,可以參考上述核心代碼創建一個名為redisTemplate的Bean組件,並在該組件中設置對應的序列化方式即可。
接下來,在項目中創建名為com.hardy.springbootdatacache.config的包,在該包下創建一個Redis自定義配置類RedisConfig,並按照上述思路自定義名為redisTemplate的Bean組件:
package com.hardy.springbootdatacache.config; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; 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 org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; /** * @Author: HardyYao * @Date: 2021/6/24 */ @Configuration public class RedisConfig { @Bean public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<Object, Object> template = new RedisTemplate(); template.setConnectionFactory(redisConnectionFactory); // ֵ使用JSON格式序列化對象,對緩存數據key和value進行轉換 Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer(Object.class); // 解決查詢緩存轉換異常的問題 ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jacksonSeial.setObjectMapper(om); // 設置RedisTemplate模板API的序列化方式為JSON template.setDefaultSerializer(jacksonSeial); return template; } }
上述代碼通過@Configuration注解定義了一個RedisConfig配置類,並使用@Bean注解注入了一個默認名稱為方法名的redisTemplate的Bean組件(注意:該Bean組件名稱必須是redisTemplate)。在定義的Bean組件中,自定義了一個RedisTemplate,使用自定義的Jackson2JsonRedisSerializer數據序列化方式;在定制序列化方式中,定義了一個ObjectMapper用於進行數據轉換設置。
3、效果測試
啟動項目,通過瀏覽器訪問:http://localhost:8080/api/findCommentById?id=2(連續訪問三次),查看網頁返回信息及控制台消息:
根據控制台打印消息可知,執行findById()方法正確查詢出了用戶評論信息Comment,重復進行同樣的查詢操作,數據庫也不會重復執行SQL語句,這表明定制的Redis緩存生效了。
使用Redis客戶端可視化管理工具Redis Desktop Manager查看緩存數據:
執行findById()方法查詢到的用戶評論信息Comment正確存儲到了Redis緩存庫中,且緩存到Redis服務的數據已經使用了JSON格式的數據存儲展示,查看和管理也十分方便,這說明自定義的Redis API模板工具RedisTemplate生效了。
基於注解的Redis緩存實現——自定義RedisCacheManager
剛剛針對基於API方式的RedisTemplate進行了自定義序列化方式的改進,從而實現了JSON序列化方式緩存數據,但是這種自定義的RedisTemplate對於基於注解的Redis緩存來說,是沒有作用的。
接下來,針對基於注解的Redis緩存機制和自定義序列化方式進行講解。
1、Redis注解默認序列化機制
打開SpringBoot整合Redis組件提供的緩存自動配置類RedisCacheConfiguration(org.springframework.boot.autoconfigure.cache包下的),查看該類的源碼信息,其核心代碼如下:
package org.springframework.boot.autoconfigure.cache; import java.util.LinkedHashSet; import java.util.List; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.cache.CacheProperties.Redis; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.cache.CacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ResourceLoader; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.cache.RedisCacheManager.RedisCacheManagerBuilder; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair; @Configuration(proxyBeanMethods = false) @ConditionalOnClass(RedisConnectionFactory.class) @AutoConfigureAfter(RedisAutoConfiguration.class) @ConditionalOnBean(RedisConnectionFactory.class) @ConditionalOnMissingBean(CacheManager.class) @Conditional(CacheCondition.class) class RedisCacheConfiguration { @Bean RedisCacheManager cacheManager(CacheProperties cacheProperties, CacheManagerCustomizers cacheManagerCustomizers, ObjectProvider<org.springframework.data.redis.cache.RedisCacheConfiguration> redisCacheConfiguration, ObjectProvider<RedisCacheManagerBuilderCustomizer> redisCacheManagerBuilderCustomizers, RedisConnectionFactory redisConnectionFactory, ResourceLoader resourceLoader) { RedisCacheManagerBuilder builder = RedisCacheManager.builder(redisConnectionFactory).cacheDefaults( determineConfiguration(cacheProperties, redisCacheConfiguration, resourceLoader.getClassLoader())); List<String> cacheNames = cacheProperties.getCacheNames(); if (!cacheNames.isEmpty()) { builder.initialCacheNames(new LinkedHashSet<>(cacheNames)); } if (cacheProperties.getRedis().isEnableStatistics()) { builder.enableStatistics(); } redisCacheManagerBuilderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); return cacheManagerCustomizers.customize(builder.build()); } private org.springframework.data.redis.cache.RedisCacheConfiguration determineConfiguration( CacheProperties cacheProperties, ObjectProvider<org.springframework.data.redis.cache.RedisCacheConfiguration> redisCacheConfiguration, ClassLoader classLoader) { return redisCacheConfiguration.getIfAvailable(() -> createConfiguration(cacheProperties, classLoader)); } private org.springframework.data.redis.cache.RedisCacheConfiguration createConfiguration( CacheProperties cacheProperties, ClassLoader classLoader) { Redis redisProperties = cacheProperties.getRedis(); org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration .defaultCacheConfig(); // 默認也是使用JdkSerializationRedisSerializer作為序列化方式 config = config.serializeValuesWith( SerializationPair.fromSerializer(new JdkSerializationRedisSerializer(classLoader))); if (redisProperties.getTimeToLive() != null) { config = config.entryTtl(redisProperties.getTimeToLive()); } if (redisProperties.getKeyPrefix() != null) { config = config.prefixCacheNameWith(redisProperties.getKeyPrefix()); } if (!redisProperties.isCacheNullValues()) { config = config.disableCachingNullValues(); } if (!redisProperties.isUseKeyPrefix()) { config = config.disableKeyPrefix(); } return config; } }
從上述核心源碼可看出,同RedisAutoConfiguration源碼(其中定義的RedisTemplate)類似,RedisCacheConfiguration內部同樣通過Redis連接工廠RedisConnectionFactory定義了一個緩存管理器RedisCacheManager;同時定制RedisCacheManager時,也默認使用了JdkSerializationRedisSerializer序列化方式。
如果想要使用自定義序列化方式的RedisCacheManager進行數據緩存操作,可以參考上述核心源碼創建一個名為cacheManager的Bean組件,並在該組件中設置對應的序列化方式即可。
注意:在SpringBoot 2.X版本中,RedisCacheManager是單獨進行構建的。因此,在SpringBoot 2.X版本中,對RedisTemplate進行自定義序列化機制構建后,仍然無法對RedisCacheManager內部默認序列化機制進行覆蓋(這也就解釋了基於注解的Redis緩存實現仍然會使用JDK默認序列化機制的原因),想要基於注解的Redis緩存實現也是用自定義序列化機制。想要自定義RedisCacheManager。
2、自定義RedisCacheManager
在項目的Redis配置類RedisConfig,按照上一步分析的定制方法自定義名為cacheManager的Bean組件:
package com.hardy.springbootdatacache.config; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import java.time.Duration; /** * @Author: HardyYao * @Date: 2021/6/24 */ @Configuration public class RedisConfig { ... @Bean public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) { // 分別創建String和JSON格式序列化對象,對緩存數據key和value進行轉換 RedisSerializer<String> strSerializer = new StringRedisSerializer(); Jackson2JsonRedisSerializer jacksonSerial = new Jackson2JsonRedisSerializer(Object.class); // 解決查詢緩存轉換異常問題 ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jacksonSerial.setObjectMapper(om); // 定制緩存數據序列化方式及時效 RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofDays(1)) .serializeKeysWith(RedisSerializationContext.SerializationPair .fromSerializer(strSerializer)) .serializeValuesWith(RedisSerializationContext.SerializationPair .fromSerializer(jacksonSerial)) .disableCachingNullValues(); RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(config).build(); return cacheManager; } }
上述代碼中,在RedisConfig配置類中使用@Bean注解注入了一個默認名稱為方法名的cacheManager組件。在定義的Bean組件中,通過RedisCacheConfiguration對緩存數據的key和value分別進行了序列化方式的定制,其中緩存數據的key定制為StringRedisSerializer(即String格式),而value定制了Jackson2JsonRedisSerializer(即JSON格式),同時還是用entryTtl(Duration.ofDays(1))方式將緩存數據有效期設置為1天。
完成基於注解的Redis緩存管理器RedisCacheManager定制后,可以對該緩存管理器的效果進行測試。(記得要開啟SpringBoot基於注解的緩存管理支持,即在啟動類上添加@EnableCaching注解。另外,使用自定義序列化機制的RedisCacheManager測試時,實體類可以不用實現序列化接口)。
啟動項目,通過瀏覽器訪問:http://localhost:8080/findCommentById?id=2(連續訪問三次),查看網頁返回信息及控制台消息:
根據控制台打印消息可知,執行findById()方法正確查詢出了用戶評論信息Comment,重復進行同樣的查詢操作,數據庫也不會重復執行SQL語句,這表明定制的Redis緩存生效了。
使用Redis客戶端可視化管理工具Redis Desktop Manager查看緩存數據:
可以看到用戶評論信息Comment正確存儲到了Redis緩存庫中,且緩存到Redis服務的數據已經使用了JSON格式的數據存儲展示,這說明自定義的基於注解的Redis緩存管理器RedisCacheManager生效了。