伴随信息量的爆炸式增长以及构建的应用系统越来越多样化、复杂化,特别是企业级应用互联网化的趋势,缓存(Cache)对应用程序性能的优化变的越来越重要。 将所需服务请求的数据放在缓存中,既可以提高应用程序的访问效率,又可以减少数据库服务器的压力,从而让用户获得更好的用户体验。
Spring 从 3.1 开始,以一贯的优雅风格提供了一种透明的缓存解决方案,这使得 Spring 可以在后台使用不同的缓存框架(如EhCache、GemFire、HazelCast 和 Guava)时保持编程的一致。
Spring 从 4.0 开始则全面支持 JSR-107 annotations 和自定义的缓存标签。
简介
Spring 提供可一种可以在方法级别进行缓存的缓存抽象。 通过使用 AOP 对方法机型织入,如果已经为特定方法入参执行过该方法,那么不必执行实际方法就可以返回被缓存的结果。
Spring Cache的好处
- 支持开箱即用(Out Of The Box),并提供基本的 Cache 抽象,方便切换各种底层 Cache
- 通过 Cache 注解即可实现缓存逻辑透明化,让开发者关注业务逻辑
- 当事务回滚时,缓存也会自动回滚
- 支持比较复杂的缓存逻辑
- 提供缓存编程的一致性抽象,方便代码维护。
Spring Cache的缺点
- Spring Cache 并不针对多进程的应用环境进行专门的处理。
- 另外 Spring Cache 抽象的操作中没有锁的概念,当多线程并发操作(更新或者删除)同一个缓存项时,有可能读取到过期的数据。
缓存管理器
缓存管理器封装了 Spring Cache 的执行逻辑,Spring 提供了多种 CacheManager:org.springframework.cache.support.SimpleCacheManager
、org.springframework.data.redis.cache.RedisCacheManager
等。
以 RedisCacheManager 为例,其继承链为 RedisCacheManager -> AbstractTransactionSupportingCacheManager -> AbstractCacheManager -> CacheManager
。
CacheManager 缓存管理器底层接口
CacheManager 为基本接口,定义了获取缓存器的方法。
public interface CacheManager {
Cache getCache(String name);
Collection<String> getCacheNames();
}
AbstractCacheManager 缓存管理器公共抽象
AbstractCacheManager 定义了缓存管理过程:初始化缓存管理器、新增缓存器、根据名称获取缓存器、装饰缓存器、Miss时补偿获取缓存器。
AbstractTransactionSupportingCacheManager 缓存管理器事务装饰
通过重载 decorateCache 装饰缓存器方法,创建 TransactionAwareCacheDecorator 代理 Cache,达到支持事务。
@Override
protected Cache decorateCache(Cache cache) {
return isTransactionAware() ?
new TransactionAwareCacheDecorator(cache) : cache;
}
以 put 操作为例,当事务开启时,会在事务成功提交后才执行。
@Override
public void put(final Object key, @Nullable final Object value) {
if (TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCommit() {
TransactionAwareCacheDecorator.this.targetCache.put(key, value);
}
}
);
} else {
this.targetCache.put(key, value);
}
}
RedisCacheManager 缓存管理器Redis支持
缓存写入器
RedisCacheWriter 封装了操作 Redis Client 的底层实现。
锁机制
RedisCacheWriter 内部提供了两种写入锁机制:有锁写入和无锁写入。无锁写入顾名思义,多线程写时不加锁。
RedisCacheWriter.lockingRedisCacheWriter(); // 有锁写入
RedisCacheWriter.nonLockingRedisCacheWriter(); // 无锁写入
重点说下有锁写入。
private <T> T execute(String name, Function<RedisConnection, T> callback) {
RedisConnection connection = connectionFactory.getConnection();
try {
// 循环通过 exist 检测是否在 redis 内有锁,每次循环间隔 sleepTime
checkAndPotentiallyWaitUntilUnlocked(name, connection);
// 如果检测到无锁,执行对应 redis 命令
return callback.apply(connection);
} finally {
connection.close();
}
}
callback 调用的是实际的 redis 方法,有锁写入仅支持 putIfAbsent 和 clear 两种方法,以 putIfAbsent 为例。
public byte[] putIfAbsent(String name, byte[] key, byte[] value, @Nullable Duration ttl) {
return execute(name, connection -> {
// 如果是有锁写入则给该 key 加锁
// 锁的 key 为缓存器名称 + ~lock
if (isLockingCacheWriter()) {
doLock(name, connection);
}
try {
connection.setNX(key, value);
return connection.get(key);
} finally {
// 如果是有锁写入则给该 key 释放
锁
if (isLockingCacheWriter()) {
doUnlock(name, connection);
}
}
}
BatchStrategy 批处理策略
需要注意的是在构造时有一个 BatchStrategy 批处理策略。
批处理策略有两种默认实现:keys 和 scan,分别对应 redis keys 和 scan 两个命令。当有需要调用 clean 时会根据这两种命令批量获取键。
keys 和 scan 命令区别
keys 是根据所给 pattern 遍历所有 key,复杂度是 O(n),当实例中数据量过大的时候,Redis 服务可能会卡顿,其余指令可能会延时甚至超时报错,生产环境建议屏蔽掉该指令。
scan 是 keys 命令的替代方案,其复杂度虽然也是 O(n),但有以下特性:
- 通过游标分步进行的,不会阻塞线程;
- 提供 limit 参数,可以控制每次返回结果的最大条数,limit 只是对增量式迭代命令的一种提示 (hint),返回的结果可多可少;
- 返回的结果可能会有重复,需要客户端去重复;
- 遍历的过程中如果有数据修改,改动后的数据能不能遍历到是不确定的;
- 单次返回的结果是空的并不意味着遍历结束,而要看返回的游标值是否为零。
缓存统计功能
可在 RedisCacheManagerBuilder 中开启。
RedisCacheManager.builder()
.cacheWriter(redisCacheWriter)
.cacheDefaults(defaultCacheConfig)
.withInitialCacheConfigurations(redisCacheConfigurationMap)
// .disableCreateOnMissingCache()
// 开启缓存统计
.enableStatistics()
.build();
也可在 RedisCacheWriter 中开启,推荐重写 CacheStatisticsCollector 用做业务监控,默认实现统计结果存储在本地,多机情况下还需要单独聚合,使用 Prometheus 更加方便。
// 初始化一个RedisCacheWriter
RedisCacheWriter redisCacheWriter = RedisCacheWriter
.nonLockingRedisCacheWriter(redisConnectionFactory)
// 设置缓存统计收集器 效果等同于 在Builder开启缓存统计
.withStatisticsCollector(CacheStatisticsCollector.create());
简单读取统计数据
@RestController
public class TestController {
@Resource
private RedisCacheManager manager;
@Resource
private RedisCacheWriter writer;
@GetMapping("/statistic")
public Mono<Map<String, CacheStatistics>> statistic() {
Map<String, CacheStatistics> statisticsMap = manager.getCacheNames()
.stream()
.collect(Collectors.toMap(k -> k, writer::getCacheStatistics));
return Mono.just(statisticsMap);
}
}
缓存配置
RedisCacheManager 还支持对每一个缓存器单独定义相关配置。
先配置一个默认的配置,即缺省配置。
/**
* 自定义 Redis 配置
* 配置自动化缓存使用的序列化方式以及过期时间
*/
@Bean
public RedisCacheConfiguration redisCacheConfiguration() {
return RedisCacheConfiguration.defaultCacheConfig()
// 配置value序列化方式为Jackson2JsonRedisSerializer,key序列化方式采用默认的StringRedisSerializer
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(getSerializer(Object.class)))
// 设置默认超过期时间是1天(短时间的已经做了其他的处理,不会采用注解形式加入缓存)
.entryTtl(Duration.ofDays(1))
// 设置缓存名称前缀,最好以项目区分
.prefixCacheNameWith("COMMON-")
// 不缓存 null 值
.disableCachingNullValues();
}
还可以通过 Map 的形式(Key 为缓存器名称,Value 为自定义配置)自定义缓存器的执行逻辑。
/**
* 缓存过期时间自定义配置
*/
@Bean
public CacheManager cacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration configuration) {
// 对于不同类型的Key信息进行单独的缓存配置
Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
// 对于名称为USER的缓存器,单独配置
configMap.put("USER", configuration.entryTtl(Duration.ofDays(10)).disableCachingNullValues().prefixCacheNameWith("USER-"));
// 初始化 RedisCacheManager
return RedisCacheManager.builder()
.cacheWriter(cacheWriter)
.cacheDefaults(configuration)
.withInitialCacheConfigurations(configMap)
.build();
}
如示例,共用 Key 前缀为 COMMON-
但是 USER 缓存器的 Key 前缀为 USER-
。
缓存解析器
根据缓存解析器(CacheResolver)的策略,通过 CacheManager 获取 Cache。
@Override
public Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context) {
Collection<String> cacheNames = getCacheNames(context);
if (cacheNames == null) {
return Collections.emptyList();
} Collection<Cache> result = new ArrayList<>(cacheNames.size());
for (String cacheName : cacheNames) {
Cache cache = getCacheManager().getCache(cacheName);
if (cache == null) {
throw new IllegalArgumentException("Cannot find cache named '" +
cacheName + "' for " + context.getOperation());
} result.add(cache);
} return result;
}
内置的比较简单,可以定制开发实现。将缓存实现逻辑完全隔离在业务逻辑外,且提升了复用性。例如:两级缓存,本地缓存 -> 远程缓存 -> DB、热点缓存自动升降级,根据数仓将热数据存入本地缓存,冷数据存入 DB 或是远程缓存。
缓存Key生成器
使用 target、method、params 三个参数,根据自定义策略生成键值。
@Bean("customKeyGenerator")
public KeyGenerator keyGenerator() {
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... params) {
return method.getName() + "[" + Arrays.asList(params) + "]";
}
};
}
注解
缓存方法级注解
通用参数
参数 | 解释 | 示例 |
---|---|---|
value | 缓存器的名称,在 spring 配置文件中定义,必须指定至少一个 | |
cacheNames | 含义同 value | |
key | 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 | @Cacheable(cacheNames=”cache”, key=”#userName”) |
keyGenerator | key 和 keyGenerator 不能同时设置 | @Cacheable(cacheNames = “cache”, keyGenerator = “myKeyGenerator”) |
cacheManager | 单独指定使用某个缓存管理器 | |
cacheResolver | 单独指定使用某个缓存解析器 | |
condition | 缓存操作执行的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才执行 | @Cacheable(cacheNames=”cache”, condition=”#userName.length()>2”) |
value 指定两个是什么逻辑
Cacheable
@Cacheable 的作用主要针对方法配置,能够根据方法的请求参数对其结果进行缓存。
略过通用参数介绍。
参数 | 解释 | 示例 |
---|---|---|
unless | 在方法执行结束后,根据方法的执行结果执行是否需要放入缓存 | 例如 unless = “#result != null”,表示仅当方法执行结果不为 null 时才放入缓存 |
sync | 是否需要同步调用,如果设置为true,具有相同key的多次调用串行执行 |
CachePut
略过通用参数介绍。
参数 | 解释 | 示例 |
---|---|---|
unless | 在方法执行结束后,根据方法的执行结果执行是否需要放入缓存 | 例如 unless = “#result != null”,表示仅当方法执行结果不为 null 时才放入缓存 |
CacheEvict
略过通用参数介绍。
参数 | 解释 | 示例 |
---|---|---|
allEntries | 是否删除缓存中所有的记录(当前指定的cacheNames下),如果设置为false,仅删除设定的key | |
beforeInvocation | 是否在方法调用前删除缓存,默认是false,仅当方法成功执行后才删除缓存,如果设定为true,则在调用前即删除缓存 |
Caching
为了一个方法操作多个缓存的情况做的扩展
public @interface Caching {
Cacheable[] cacheable() default {};
CachePut[] put() default {};
CacheEvict[] evict() default {};
}
缓存类级注解
CacheConfig
@CacheConfig
是一个类级别的注解,可以在类级别上配置 cacheNames、keyGenerator、cacheManager、cacheResolver 等。
源码概略
注解 AOP 解析入口
解析 AOP 的入口是 BeanFactoryCacheOperationSourceAdvisor,其继承了 AbstractBeanFactoryPointcutAdvisor 允许在 BeanFactory 进行织入。在初始化 Bean 后会在 Bean PostProcessor 的过程中调起 getPointcut()
方法。
private final CacheOperationSourcePointcut pointcut = new CacheOperationSourcePointcut() {
@Nullable
protected CacheOperationSource getCacheOperationSource() {
return BeanFactoryCacheOperationSourceAdvisor.this.cacheOperationSource;
}
};
public Pointcut getPointcut() {
return this.pointcut;
}
BeanFactoryCacheOperationSourceAdvisor 根据 CacheOperationSourcePointcut 这个抽象 Pointcut 类创建了一个内部类,并将 getPointcut()
方法绑定到该单例内部类中。
CacheOperationSourcePointcut 继承了 StaticMethodMatcherPointcut 抽象类,实现了匹配方法的规则。
@Override
public boolean matches(Method method, Class<?> targetClass) {
CacheOperationSource cas = getCacheOperationSource();
return (cas != null && !CollectionUtils.isEmpty(cas.getCacheOperations(method, targetClass)));
}
实际的匹配规则由 AnnotationCacheOperationSource 进行了实现。
注解 AOP 调用入口
调用 AOP 的入口是 CacheInterceptor,将原方法调用包装为 CacheOperationInvoker,通过 execute 方法触发附加操作。
public Object invoke(final MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
CacheOperationInvoker aopAllianceInvoker = () -> {
try {
return invocation.proceed();
} catch (Throwable ex) {
throw new CacheOperationInvoker.ThrowableWrapper(ex);
} };
Object target = invocation.getThis();
Assert.state(target != null, "Target must not be null");
try {
return execute(aopAllianceInvoker, target, method, invocation.getArguments());
} catch (CacheOperationInvoker.ThrowableWrapper th) {
throw th.getOriginal();
}
}
根据平时的业务逻辑,实现 Cache-Aside 策略
private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
// Process any early evictions
processCacheEvicts(contexts.get(CacheEvictOperation.class), true,
CacheOperationExpressionEvaluator.NO_RESULT);
// Check if we have a cached item matching the conditions
Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));
Object cacheValue;
Object returnValue;
if (cacheHit != null && !hasCachePut(contexts)) {
// If there are no put requests, just use the cache hit
cacheValue = cacheHit.get();
returnValue = wrapCacheValue(method, cacheValue);
} else {
// Invoke the method if we don't have a cache hit
returnValue = invokeOperation(invoker);
cacheValue = unwrapReturnValue(returnValue);
}
// Collect any explicit @CachePuts
collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);
// Process any collected put requests, either from @CachePut or a @Cacheable miss
for (CachePutRequest cachePutRequest : cachePutRequests) {
cachePutRequest.apply(cacheValue);
}
// Process any late evictions
processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);
return returnValue;
}
示例程序
Maven 配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
spring 配置
spring.cache.type=redis
spring.redis.database=0
spring.redis.host=localhost
spring.redis.port=6379
spring.cache.redis.time-to-live=50000ms
Redis 配置类
import java.lang.reflect.Method;
import java.time.Duration;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.BatchStrategies;
import org.springframework.data.redis.cache.CacheStatisticsCollector;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
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.StringRedisSerializer;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* 缓存Redis配置
*/
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
/**
* 使用Jackson2JsonRedisSerialize 替换默认序列化
*/
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
// 使用Jackson2JsonRedisSerialize 替换默认序列化
Jackson2JsonRedisSerializer<Object> serializer = getSerializer(Object.class);
// 设置value的序列化规则和 key的序列化规则
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(serializer);
// hash参数序列化方式
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(serializer);
// 缓存支持回滚(事务管理)
redisTemplate.setEnableTransactionSupport(true);
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
/**
* 配置事务管理器
*/
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
/**
* 自定义 Redis 配置
* 配置自动化缓存使用的序列化方式以及过期时间
*/
@Bean
public RedisCacheConfiguration redisCacheConfiguration() {
return RedisCacheConfiguration.defaultCacheConfig()
// 配置value序列化方式为Jackson2JsonRedisSerializer,key序列化方式采用默认的StringRedisSerializer
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(getSerializer(Object.class)))
// 设置默认超过期时间是1天(短时间的已经做了其他的处理,不会采用注解形式加入缓存)
.entryTtl(Duration.ofDays(1))
// 设置缓存名称前缀,最好以项目区分
.prefixCacheNameWith("COMMON-")
// 不缓存 null 值
.disableCachingNullValues();
}
/**
* 自定义 RedisWriter 配置
*
* @param redisConnectionFactory redis连接工厂
* @return RedisCacheWriter
*/
@Bean
public RedisCacheWriter cacheWriter(RedisConnectionFactory redisConnectionFactory, CacheStatisticsCollector statisticsCollector) {
return RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory, BatchStrategies.scan(10))
.withStatisticsCollector(statisticsCollector);
}
/**
* 自定义缓存收集器配置
* @return CacheStatisticsCollector
*/ @Bean
public CacheStatisticsCollector statisticsCollector() {
return CacheStatisticsCollector.create();
}
/**
* 缓存过期时间自定义配置
*/
@Bean
public CacheManager cacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration configuration) {
// 对于不同类型的Key信息进行单独的缓存配置
Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
// 对于名称为USER的缓存器,单独配置
configMap.put("USER", configuration.entryTtl(Duration.ofDays(10)).disableCachingNullValues().prefixCacheNameWith("USER-"));
// 初始化 RedisCacheManager return RedisCacheManager.builder()
.cacheWriter(cacheWriter)
.cacheDefaults(configuration)
.withInitialCacheConfigurations(configMap)
.build();
}
/**
* 自定义 Key 生成器
* @return KeyGenerator
*/
@Bean("customKeyGenerator")
public KeyGenerator keyGenerator() {
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... params) {
return method.getName() + "[" + Arrays.asList(params) + "]";
} }; }
private <T> Jackson2JsonRedisSerializer<T> getSerializer(Class<T> clazz) {
Jackson2JsonRedisSerializer<T> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(clazz);
jackson2JsonRedisSerializer.setObjectMapper(getObjectMapper());
return jackson2JsonRedisSerializer;
}
private ObjectMapper getObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
return objectMapper;
}
}
缓存代理类
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;
@Component
public class TestCacheProxy {
@Cacheable(value = "USER", key = "#name + ':' + #tag")
public String getUser(String name, String tag) {
return String.format("user: %s, tag: %s", name, tag);
}
@Cacheable(value = "ORDER", key = "#id")
public String getOrder(String id) {
return String.format("order: %s", id);
}
@Cacheable(value = "ORDERINFO", keyGenerator = "customKeyGenerator")
public String getOrderInfo(String id) {
return String.format("order: %s", id);
}
}
测试类
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import lombok.AllArgsConstructor;
import reactor.core.publisher.Mono;
@RestController
@AllArgsConstructor
public class TestController {
private TestCacheProxy proxy;
@GetMapping("/user")
public Mono<String> user(String name, String tag) {
return Mono.just(proxy.getUser(name, tag));
}
@GetMapping("/order/{id}")
public Mono<String> order(@PathVariable("id") String id) {
return Mono.just(proxy.getOrder(id));
}
@GetMapping("/order-info/{id}")
public Mono<String> orderInfo(@PathVariable("id") String id) {
return Mono.just(proxy.getOrderInfo(id));
}
}