從零搭建Spring Boot腳手架(6):整合Redis作為快取

碼農小胖哥發表於2020-08-19

1. 前言

上一文我們整合了Mybatis Plus,今天我們會把快取也整合進來。快取是一個系統應用必備的一種功能,除了在減輕資料庫的壓力之外。還在儲存一些短時效的資料場景中發揮著重大作用,比如儲存使用者Token、簡訊驗證碼等等,目前快取的選型還是比較多的,EHCACHEHAZELCASTCAFFEINECOUCHBASE以及本文要整合的REDIS。接下來我們將會在kono腳手架專案中整合Spring Cache以及Redis

Gitee: https://gitee.com/felord/kono day05 分支

GitHub: https://github.com/NotFound403/kono day05 分支

2. 整合目標

使專案具有快取功能,同時將預設的JDK序列化修改為Jackson序列化以儲存一些物件,同時實現一些特定的個性化的快取空間以滿足不同場景下的不同快取TTL時間需求。

3. 依賴整合

目前只需要引入下面的依賴即可:

 <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>       

預設情況下spring-data-redis使用高效能的lettuce客戶端實現,當然你可以替換為老舊的jedis

4. 快取及Redis配置

快取以及Redis相關的配置項分別為spring.cachespring.redis開頭的配置,這裡比較簡單的配置為:

spring:
  redis:
    host: localhost
    port: 6379
  cache:
#   type: REDIS
    redis:
    # 全域性過期時間
      time-to-live: 120

5. RedisTemplate個性化

預設情況下會有兩個模板類被注入Spring IoC供我們使用,需要個性化配置來滿足實際的開發。

一個是RedisTemplate<Object, Object>,主要用於物件快取,其預設使用JDK序列化,我們需要更改其序列化方式解決一些問題,比如Java 8日期問題、JSON序列化問題。需要我們重寫一下。

/**
 * Redis的一些自定義配置.
 *
 * @author felord.cn
 * @since 2020 /8/17 20:39
 */
@ConditionalOnClass(ObjectMapper.class)
@Configuration(proxyBeanMethods = false)
public class RedisConfiguration {
    /**
     * Redis template redis template.
     *
     * @param redisConnectionFactory the redis connection factory
     * @return the redis template
     */
    @Bean("redisTemplate")
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        // 使用Jackson2JsonRedisSerialize 替換預設序列化
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = initJacksonSerializer();
        // 設定value的序列化規則和 key的序列化規則
        template.setValueSerializer(jackson2JsonRedisSerializer);
        template.setKeySerializer(new StringRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }

    /**
     * 處理redis序列化問題
     * @return Jackson2JsonRedisSerializer
     */
    private Jackson2JsonRedisSerializer<Object> initJacksonSerializer() {
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        //以下替代舊版本 om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        om.activateDefaultTyping(om.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL);
        //bugFix Jackson2反序列化資料處理LocalDateTime型別時出錯
        om.disable(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS);
        // java8 時間支援
        om.registerModule(new JavaTimeModule());
        jackson2JsonRedisSerializer.setObjectMapper(om);
        return jackson2JsonRedisSerializer;
    }

}

另一個是StringRedisTemplate,主要處理鍵值都是字串的快取,採用預設就好。

6. 快取個性化

使用Spring Cache做快取的時候,有針對不同的key設定不同過期時間的場景。比如Jwt Token我想設定為一週過期,而驗證碼我想設定為五分鐘過期。這個怎麼實現呢?需要我們個性化配置RedisCacheManager。首先我通過列舉來定義這些快取及其TTL時間。例如:

/**
 * 快取定義列舉
 *
 * @author felord.cn
 * @see cn.felord.kono.configuration.CacheConfiguration
 * @since 2020/8/17 21:40
 */

public enum CacheEnum {

    /**
     * 使用者jwt token 快取空間 ttl 7天
     */
    JWT_TOKEN_CACHE("usrTkn", 7 * 24 * 60 * 60),
    /**
     * 驗證碼快取 5分鐘ttl
     */
    SMS_CAPTCHA_CACHE("smsCode", 5 * 60);

    /**
     * 快取名稱
     */
    private final String cacheName;
    /**
     * 快取過期秒數
     */
    private final int ttlSecond;

    CacheEnum(String cacheName, int ttlSecond) {
        this.cacheName = cacheName;
        this.ttlSecond = ttlSecond;
    }
    
    public String cacheName() {
        return this.cacheName;
    }


    public int ttlSecond() {
        return this.ttlSecond;
    }
}

這樣就能很清楚地描述個性化的快取了。

然後我們通過向Spring IoC分別注入RedisCacheConfigurationRedisCacheManagerBuilderCustomizer 來個性化配置,你可以留意CacheEnum是如何工作的。如果你有其它的個性化需要也可以對這兩個配置類進行定製化。

import cn.felord.kono.enumeration.CacheEnum;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.autoconfigure.cache.RedisCacheManagerBuilderCustomizer;
import org.springframework.cache.annotation.EnableCaching;
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.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializationContext;

import java.time.Duration;
import java.util.EnumSet;
import java.util.stream.Collectors;

/**
 * redis 快取配置.
 *
 * @author felord.cn
 * @since 2020 /8/17 20:14
 */
@EnableCaching
@Configuration
public class CacheConfiguration {


    /**
     * Redis cache configuration.
     *
     * @param redisTemplate the redis template
     * @return the redis cache configuration
     */
    @Bean
    public RedisCacheConfiguration redisCacheConfiguration(RedisTemplate<Object, Object> redisTemplate, CacheProperties cacheProperties) {
         // 參見 spring.cache.redis
        CacheProperties.Redis redisProperties = cacheProperties.getRedis();

        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                // 快取的序列化問題
                .serializeValuesWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(redisTemplate.getValueSerializer()));

        if (redisProperties.getTimeToLive() != null) {
            // 全域性 TTL 時間
            redisCacheConfiguration = redisCacheConfiguration.entryTtl(redisProperties.getTimeToLive());
        }
        if (redisProperties.getKeyPrefix() != null) {
            // key 字首值
            redisCacheConfiguration = redisCacheConfiguration.prefixCacheNameWith(redisProperties.getKeyPrefix());
        }
        if (!redisProperties.isCacheNullValues()) {
            // 預設快取null值 可以防止快取穿透
            redisCacheConfiguration = redisCacheConfiguration.disableCachingNullValues();
        }
        if (!redisProperties.isUseKeyPrefix()) {
            // 不使用key字首
            redisCacheConfiguration = redisCacheConfiguration.disableKeyPrefix();
        }
        return redisCacheConfiguration;
    }


    /**
     * Redis cache manager 個性化配置快取過期時間.
     * @see RedisCacheManager,CacheEnum
     * @return the redis cache manager builder customizer
     */
    @Bean
    public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer(RedisCacheConfiguration redisCacheConfiguration) {

        return builder -> builder.cacheDefaults(redisCacheConfiguration)
                // 自定義的一些快取配置初始化 主要是特定快取及其ttl時間
                .withInitialCacheConfigurations(EnumSet.allOf(CacheEnum.class).stream()
                        .collect(Collectors.toMap(CacheEnum::cacheName,
                                cacheEnum -> redisCacheConfiguration.entryTtl(Duration.ofSeconds(cacheEnum.ttlSecond())))));
    }

}

個性化的同時我們可以通過註解@EnableCaching開啟Spring Cache快取支援。關於Spring Cache的細節可以通過文章Spring Cache詳解來了解。

驗證Spring Cache Redis快取個性化

請注意,只有通過Spring Cache操作快取才會達到上圖的效果。命令列操作需要顯式的宣告指令。

7. 總結

最近事情比較多,所以難得抽出時間來搞一搞。如果你在實際開發中遇到需要整合的功能也可以告訴我,同時如果你發現整合中的一些缺陷或者Bug請提交ISSUE。多多關注:碼農小胖哥,跟我一起整合開發腳手架。

關注公眾號:Felordcn 獲取更多資訊

個人部落格:https://felord.cn

相關文章