SpringBoot快取管理(三) 自定義Redis快取序列化機制

blayn發表於2021-07-20

前言

在上一篇文章中,我們完成了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生效了。

相關文章