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()
内部逻辑,剩下两次没有,可见剩下两次走了缓存。我们也可以看看这次缓存的过期时间:
可见缓存时间也是生效了的。
三. 总结
利用spring cache
来使用缓存,可以使我们避免与某种缓存框架硬耦合,避免编写固定的模板代码,使代码更专注于业务实现。