spring boot + spring cache + redis 缓存使用方式


spring boot + spring cache + redis 缓存使用方式

对于缓存的使用,我们并不陌生。这里我们不讨论缓存击穿,缓存穿透和雪崩等问题。主要是讲怎么将spring boot + spring cache + redis优雅的结合起来。

一. spring cache

对于缓存,可以使用的框架太多,如reids,caffeine,ehcache等等,各有各自的优势。如果我们要想使用缓存,就得与这些框架耦合,为了避免这种情况,spring cache就利用AOP,实现基于注解的缓存功能,并进行合理的抽象,使业务代码不用担心底层使用了什么缓存框架。

二. 编码

1. gradle配置
plugins {
    id 'org.springframework.boot' version '2.4.1'
    id 'io.spring.dependency-management' version '1.0.10.RELEASE'
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenLocal()
    maven { url 'https://repo.spring.io/milestone' }
    maven { url "http://maven.aliyun.com/nexus/content/groups/public/" }
    mavenCentral()
    jcenter()
}

ext {
    set('springCloudVersion', "2020.0.0-M6")
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springframework.cloud:spring-cloud-starter-sleuth'
    compileOnly 'org.projectlombok:lombok'
    testCompileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
    annotationProcessor 'org.projectlombok:lombok'
    testAnnotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
 
    // spring cache starter
    implementation 'org.springframework.boot:spring-boot-starter-cache'
    // redis starter
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
}

dependencyManagement {
    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
    }
}

test {
    useJUnitPlatform()
}
2. application.yml
server:
  port: 8080
  shutdown: graceful
spring:
  application:
    name: demo
  lifecycle:
    timeout-per-shutdown-phase: 30s
  # 缓存设置
  cache:
    type: redis
    redis:
      time-to-live: 1d
  # redis 配置
  redis:
    host: 127.0.0.1
    port: 6379
    database: 0
    password: *******
3. 程序入口
@SpringBootApplication
public class Demo20201212Application {

    public static void main(String[] args) {
        SpringApplication.run(Demo20201212Application.class, args);
    }

}
4. redis配置
package com.example.demo20201212.config;

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ResourceLoader;
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.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;

import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;

@Slf4j
@Configuration
@EnableCaching
@EnableConfigurationProperties(CacheProperties.class)
public class RedisCacheConfig {

  	// 缓存名称
    public static final String DECODE_PREFIX = "demo:test1";

    // 这里是为了对不同的缓存进行不同的过期时间配置,如果有需求对不同的数据应用不同的缓存时间,可以在这边增加一个对应的枚举
    @Getter
    @AllArgsConstructor
    public enum CacheNameEnum {
        DECODE_PREFIX_CACHE(DECODE_PREFIX, Duration.ofMinutes(30));

        private final String name;
        private final Duration timeToLive;
    }

    // 覆盖默认的redis缓存管理器,目的是为了自定义缓存过期时间
    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory, CacheProperties cacheProperties, ResourceLoader resourceLoader) {
        // 设置jackson配置 
        ObjectMapper om = new ObjectMapper();
        om.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        om.registerModules(new JavaTimeModule(), new SimpleModule(), new ParameterNamesModule(), new Jdk8Module());
        om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);

        RedisCacheManager.RedisCacheManagerBuilder builder = RedisCacheManager.builder(redisConnectionFactory)
                .cacheDefaults(determineConfiguration(cacheProperties, resourceLoader.getClassLoader(), om));
        List<String> cacheNames = cacheProperties.getCacheNames();

        if (!cacheNames.isEmpty()) {
            builder.initialCacheNames(new LinkedHashSet<>(cacheNames));
        }

        // 自定义过期时间
        customize(builder, cacheProperties, resourceLoader.getClassLoader(), om);

        return builder.build();
    }

    private void customize(RedisCacheManager.RedisCacheManagerBuilder builder, CacheProperties cacheProperties, ClassLoader classLoader, ObjectMapper objectMapper) {
        Map<String, RedisCacheConfiguration> cacheConfigMap = new LinkedHashMap<>();

        for (CacheNameEnum value : CacheNameEnum.values()) {
            cacheConfigMap.put(value.getName(), determineConfiguration(cacheProperties, classLoader, objectMapper).entryTtl(value.timeToLive));
        }
        builder.withInitialCacheConfigurations(cacheConfigMap);
    }

    private RedisCacheConfiguration determineConfiguration(CacheProperties cacheProperties, ClassLoader classLoader, ObjectMapper objectMapper) {
        CacheProperties.Redis redisProperties = cacheProperties.getRedis();
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        // 使用这种jackson的序列化会导致反序列化到对应实体失败,出现类型转换异常
        //        config = config.serializeValuesWith(
//                RedisSerializationContext.SerializationPair.fromSerializer(new Jackson2JsonRedisSerializer<>(Object.class)));

        // 可以使用jdk自带的序列化方式,这种就是在redis查看时,看不到具体的缓存信息
//        config = config.serializeValuesWith(
//                RedisSerializationContext.SerializationPair.fromSerializer(new JdkSerializationRedisSerializer(classLoader)));

        config = config.serializeValuesWith(
                RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer(objectMapper)));
        if (redisProperties.getTimeToLive() != null) {
            config = config.entryTtl(redisProperties.getTimeToLive());
        }

        if (!redisProperties.isCacheNullValues()) {
            config = config.disableCachingNullValues();
        }

        return config;
    }
}
5. 测试
@SpringBootTest
class CacheTests {

    @Autowired
    private CommonCacheService commonCacheService;


    @Test
    void test1() {
        final List<NameAndValue> sex = commonCacheService.getSex();
        log.info("first === {}", sex);

        final List<NameAndValue> sex1 = commonCacheService.getSex();
        log.info("second === {}", sex1);

        final List<NameAndValue> sex2 = commonCacheService.getSex();
        log.info("third === {}", sex2);
    }

}


@Slf4j
@Service
public class CommonCacheService {

    /**
     * 这里利用注解来进行缓存,没有涉及到业务代码的书写,也无需关系底层的缓存实现是什么
     * 可以看到这个缓存的配置的过期时间室30分钟
     */
    @Cacheable(cacheNames = RedisCacheConfig.DECODE_PREFIX)
    public List<NameAndValue> getSex() {
        log.info("====== 获取性别字典 ======");
        NameAndValue nameAndValue1 = new NameAndValue();
        nameAndValue1.setValue("1");
        nameAndValue1.setName("男");
        nameAndValue1.setCreateTime(LocalDateTime.now());

        NameAndValue nameAndValue2 = new NameAndValue();
        nameAndValue2.setValue("2");
        nameAndValue2.setName("女");

        List<NameAndValue> result = new ArrayList<>();
        result.add(nameAndValue1);
        result.add(nameAndValue2);

        return result;
    }
}

结果:

...
2021-02-10 22:55:09.045  INFO [demo,,] 40666 --- [           main] c.e.d.service.CommonCacheService         : ====== 获取性别字典 ======
2021-02-10 22:55:09.153  INFO [demo,,] 40666 --- [           main] com.example.demo20201212.CacheTests      : first === [NameAndValue(value=1, name=男, createTime=2021-02-10T22:55:09.045), NameAndValue(value=2, name=女, createTime=null)]
2021-02-10 22:55:09.256  INFO [demo,,] 40666 --- [           main] com.example.demo20201212.CacheTests      : second === [NameAndValue(value=1, name=男, createTime=2021-02-10T22:55:09.045), NameAndValue(value=2, name=女, createTime=null)]
2021-02-10 22:55:09.300  INFO [demo,,] 40666 --- [           main] com.example.demo20201212.CacheTests      : third === [NameAndValue(value=1, name=男, createTime=2021-02-10T22:55:09.045), NameAndValue(value=2, name=女, createTime=null)]
...

可以看到”====== 获取性别字典 ======”日志只打印了一次,第一次走了getSex()内部逻辑,剩下两次没有,可见剩下两次走了缓存。我们也可以看看这次缓存的过期时间:

image-20210210225905768

可见缓存时间也是生效了的。

三. 总结

利用spring cache来使用缓存,可以使我们避免与某种缓存框架硬耦合,避免编写固定的模板代码,使代码更专注于业务实现。


文章作者: shiv
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 shiv !
评论
  目录