SpringBoot缓存的使用:@Cacheable等缓存注解源码级剖析
一、SpringBoot缓存抽象体系架构
1.1 缓存抽象的设计目标与核心接口
SpringBoot的缓存抽象旨在屏蔽不同缓存实现(如Redis、Caffeine、Guava)的底层差异,提供统一的编程模型。其核心接口如下:
Cache
:代表单个缓存,定义了get
、put
、evict
等基本操作。CacheManager
:管理多个Cache
实例,负责创建、获取和销毁缓存。CacheResolver
:根据方法参数解析对应的Cache
,用于支持多缓存策略。
在spring-context
模块中,AbstractCacheManager
是CacheManager
的抽象实现,提供了缓存实例的懒加载机制。例如:
public abstract class AbstractCacheManager implements CacheManager {
private final Map<String, Cache> cacheMap = new ConcurrentHashMap<>();
@Override
public Cache getCache(String name) {
// 优先从缓存Map中获取
Cache cache = this.cacheMap.get(name);
if (cache == null) {
// 不存在则创建新实例
cache = createCache(name);
if (cache != null) {
this.cacheMap.put(name, cache);
}
}
return cache;
}
protected abstract Cache createCache(String name);
}
createCache
方法由具体实现类(如CaffeineCacheManager
、RedisCacheManager
)负责实现,体现了模板方法设计模式。
1.2 缓存自动配置的核心逻辑
SpringBoot通过CacheAutoConfiguration
实现缓存的自动配置,关键逻辑如下:
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(CacheManager.class)
public class CacheAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public CacheManagerCustomizers cacheManagerCustomizers() {
return new CacheManagerCustomizers();
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnClass(CacheManager.class)
public CacheManager cacheManager(CacheManagerCustomizers customizers) {
// 查找所有CacheManager类型的Bean
CacheManager cacheManager = getCacheManager();
// 应用自定义配置
customizers.customize(cacheManager);
return cacheManager;
}
private CacheManager getCacheManager() {
// 优先查找用户自定义的CacheManager
// 否则根据类路径依赖自动配置(如Caffeine、Redis等)
}
}
自动配置会根据类路径中的依赖(如spring-boot-starter-cache
引入的caffeine
或redis
库),自动选择对应的CacheManager
实现类。例如,若存在caffeine
依赖,则自动配置CaffeineCacheManager
;若存在redis
依赖,则配置RedisCacheManager
。
二、@Cacheable注解的底层实现原理
2.1 注解解析与AOP代理生成
@Cacheable
注解通过AOP实现方法缓存。当Spring容器启动时,CacheAnnotationBeanPostProcessor
会扫描所有Bean,对标注了@Cacheable
等注解的方法创建代理。
CacheAnnotationBeanPostProcessor
继承自BeanPostProcessor
,其postProcessAfterInitialization
方法会为目标Bean创建CacheInterceptor
拦截器:
public Object postProcessAfterInitialization(Object bean, String beanName) {
if (this.cacheOperationSource != null) {
// 创建Cache拦截器
CacheInterceptor interceptor = this.cacheInterceptor;
if (interceptor == null) {
interceptor = new CacheInterceptor();
interceptor.setCacheOperationSource(this.cacheOperationSource);
// 配置缓存管理器等属性
}
// 使用代理模式包装目标Bean
return ProxyFactory.getProxy(bean.getClass(), new CacheOperationSourceAdvisor(interceptor, this.cacheOperationSource));
}
return bean;
}
CacheOperationSourceAdvisor
是一个Advisor
,包含CacheInterceptor
作为通知(Advice),用于拦截目标方法的调用。
2.2 缓存键的生成策略
@Cacheable
的key
属性用于指定缓存键,其值可以是SpEL表达式,默认使用SimpleKeyGenerator
生成键。SimpleKeyGenerator
的generateKey
方法逻辑如下:
public Object generateKey(Object... params) {
if (params.length == 0) {
return SimpleKey.EMPTY;
}
if (params.length == 1) {
Object param = params[0];
if (param != null && !param.getClass().isArray()) {
return param;
}
}
return new SimpleKey(params);
}
- 无参数时,键为
SimpleKey.EMPTY
。 - 单个非数组参数时,直接使用该参数作为键。
- 多个参数或数组参数时,使用
SimpleKey
对象封装参数列表。
若需要自定义键生成策略,可实现KeyGenerator
接口并通过@Cacheable(keyGenerator = "customKeyGenerator")
指定。
2.3 缓存查询与更新流程
当CacheInterceptor
拦截到@Cacheable
方法调用时,会按以下流程处理:
- 解析缓存操作元数据:通过
CacheOperationSource
获取方法上的@Cacheable
注解属性,包括缓存名称、键表达式、条件等。 - 生成缓存键:根据键生成策略和方法参数计算缓存键。
- 查询缓存:通过
CacheManager
获取对应的Cache
实例,调用get(key)
方法查询缓存值。 - 处理缓存命中/未命中:
- 命中:直接返回缓存值,不执行目标方法。
- 未命中:执行目标方法,将结果存入缓存(通过
Cache.put(key, value)
),然后返回结果。
public class CacheInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
// 解析@Cacheable注解
CacheableOperation operation = (CacheableOperation) getCacheOperation(invocation.getMethod());
Cache cache = cacheManager.getCache(operation.getName());
Object key = generateKey(invocation.getArguments());
// 查询缓存
ValueWrapper valueWrapper = cache.get(key);
if (valueWrapper != null) {
return valueWrapper.get();
}
// 未命中,执行方法
Object result = invocation.proceed();
// 存入缓存
cache.put(key, result);
return result;
}
}
三、@CachePut与@CacheEvict注解的实现差异
3.1 @CachePut:更新缓存值
@CachePut
的核心逻辑是先执行方法,再更新缓存,适用于需要保证方法执行后缓存与数据库一致的场景。其处理流程与@Cacheable
的主要区别在于:
- 无论缓存是否命中,都会执行目标方法。
- 方法执行后,根据结果更新缓存,不会影响方法返回值。
public class CacheInterceptor implements MethodInterceptor {
private void processCachePut(CachePutOperation operation, MethodInvocation invocation) {
Object result = invocation.proceed(); // 先执行方法
Object key = generateKey(invocation.getArguments());
Cache cache = cacheManager.getCache(operation.getName());
cache.put(key, result); // 再更新缓存
}
}
3.2 @CacheEvict:删除缓存项
@CacheEvict
用于从缓存中删除指定项,支持以下场景:
- 单缓存项删除:通过
key
属性指定具体键。 - 清空所有缓存:设置
allEntries = true
,删除缓存中的所有项。 - 级联删除:结合
beforeInvocation
属性,在方法执行前或后删除缓存。
public class CacheInterceptor implements MethodInterceptor {
private void processCacheEvict(CacheEvictOperation operation, MethodInvocation invocation) {
Cache cache = cacheManager.getCache(operation.getName());
if (operation.isAllEntries()) {
cache.clear(); // 清空所有缓存
return;
}
Object key = generateKey(invocation.getArguments());
if (operation.isBeforeInvocation()) {
// 方法执行前删除缓存(避免方法抛出异常导致缓存不一致)
cache.evict(key);
} else {
try {
invocation.proceed();
} finally {
cache.evict(key); // 方法执行后删除缓存
}
}
}
}
beforeInvocation = true
的场景下,即使方法执行抛出异常,缓存也会被删除,确保数据一致性。
四、@Caching与@CacheConfig的组合使用
4.1 @Caching:批量缓存操作
@Caching
注解允许在一个方法上组合多个缓存操作(如同时使用@Cacheable
和@CacheEvict
)。其底层实现是解析注解中的多个操作元数据,并按顺序执行:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Caching {
Cacheable[] cacheable() default {};
CachePut[] put() default {};
CacheEvict[] evict() default {};
}
当CacheInterceptor
处理@Caching
注解时,会遍历所有缓存操作并依次执行:
public Object invoke(MethodInvocation invocation) throws Throwable {
List<CacheOperation> operations = getCacheOperations(invocation.getMethod());
for (CacheOperation operation : operations) {
if (operation instanceof CacheableOperation) {
// 处理@Cacheable
} else if (operation instanceof CachePutOperation) {
// 处理@CachePut
} else if (operation instanceof CacheEvictOperation) {
// 处理@CacheEvict
}
}
// ...
}
4.2 @CacheConfig:类级缓存配置
@CacheConfig
用于在类级别定义共享的缓存配置,避免重复代码。例如:
@CacheConfig(cacheNames = "users", keyGenerator = "customKeyGenerator")
public class UserService {
@Cacheable
public User getUser(Long id) { ... }
@CacheEvict
public void deleteUser(Long id) { ... }
}
CacheInterceptor
会将类级配置与方法级注解合并,优先级为:方法级配置 > 类级配置。在解析注解时,通过AnnotationAttributes
合并策略实现:
private AnnotationAttributes mergeCacheConfig(Method method, Class<?> targetClass) {
AnnotationAttributes classLevel = AnnotationConfigUtils.getAnnotationAttributes(targetClass, CacheConfig.class);
AnnotationAttributes methodLevel = AnnotationConfigUtils.getAnnotationAttributes(method, Cacheable.class);
// 合并逻辑:methodLevel覆盖classLevel
return methodLevel != null ? methodLevel : classLevel;
}
五、缓存管理器(CacheManager)的实现与切换
5.1 基于Caffeine的本地缓存实现
CaffeineCacheManager
是SpringBoot默认的本地缓存实现,基于Caffeine库。其createCache
方法逻辑如下:
public class CaffeineCacheManager extends AbstractCachingCacheManager {
private com.github.benmanes.caffeine.cache.Cache<Object, Object> createCaffeineCache(String name) {
return Caffeine.newBuilder()
.expireAfterAccess(this.properties.getCacheProperties().getExpireAfterAccess())
.maximumSize(this.properties.getCacheProperties().getMaximumSize())
.build();
}
@Override
protected Cache createCache(String name) {
return new CaffeineCache(name, createCaffeineCache(name), this.statsCollector);
}
}
expireAfterAccess
:指定缓存项在最后一次访问后多久过期。maximumSize
:限制缓存的最大条目数,超过时按淘汰策略(默认LRU)移除旧条目。
5.2 基于Redis的分布式缓存实现
RedisCacheManager
通过 lettuce 或 jedis 客户端与Redis交互,其核心是RedisCache
类。RedisCache
的get
方法通过RedisTemplate
执行查询:
public class RedisCache implements Cache {
@Override
public ValueWrapper get(Object key) {
byte[] value = this.redisTemplate.execute(
connection -> connection.get(this.keySerializer.serialize(key))
);
if (value == null) {
return null;
}
return new SimpleValueWrapper(this.valueSerializer.deserialize(value));
}
@Override
public void put(Object key, Object value) {
this.redisTemplate.execute((RedisCallback<Void>) connection -> {
connection.set(this.keySerializer.serialize(key), this.valueSerializer.serialize(value));
return null;
});
}
}
RedisCacheManager
支持缓存声明周期管理,如通过CacheManager.RedisCacheManagerBuilder
配置缓存过期时间:
RedisCacheManager.builder(redisConnectionFactory)
.withCacheConfiguration("users",
CacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.disableCachingNullValues());
六、缓存条件表达式与异步处理
6.1 条件表达式(condition、unless)的解析
@Cacheable
的condition
属性用于指定缓存生效的条件,unless
属性用于排除某些结果。两者均支持SpEL表达式,通过SpelExpressionParser
解析:
public class CacheOperationExpressionEvaluator {
private final ExpressionParser parser = new SpelExpressionParser();
public boolean checkCondition(String condition, Method method, Object[] args) {
if (condition == null || condition.isEmpty()) {
return true;
}
StandardEvaluationContext context = createEvaluationContext(method, args);
Expression expression = parser.parseExpression(condition);
return expression.getValue(context, Boolean.class);
}
private StandardEvaluationContext createEvaluationContext(Method method, Object[] args) {
// 添加方法参数、目标对象等到上下文
StandardEvaluationContext context = new StandardEvaluationContext();
context.setVariable("args", args);
context.setVariable("target", method.getDeclaringClass());
return context;
}
}
例如,@Cacheable(condition = "#id > 0")
表示仅当id
参数大于0时才缓存结果;@Cacheable(unless = "#result == null")
表示当结果为null
时不缓存。
6.2 异步方法的缓存支持
对于标注@Async
的方法,缓存注解的处理需考虑线程上下文。Spring的CacheAsyncConfiguration
会配置AsyncExecutionInterceptor
,结合CacheInterceptor
实现异步场景下的缓存操作:
public class AsyncCacheConfiguration {
@Bean
public AsyncExecutionInterceptor asyncExecutionInterceptor(CacheAspectSupport cacheAspectSupport) {
AsyncExecutionInterceptor interceptor = new AsyncExecutionInterceptor();
// 将Cache拦截器的逻辑包装到异步执行链中
interceptor.setTaskExecutor(cacheAspectSupport.getCacheManager().getCache("asyncCache").getNativeCache());
return interceptor;
}
}
异步方法的缓存键生成和缓存操作会在异步线程中执行,确保缓存操作与方法执行在同一线程上下文内。
七、缓存与Spring事务的交互顺序
7.1 事务与缓存的执行顺序问题
当方法同时被@Transactional
和缓存注解标注时,需注意执行顺序:
- 默认顺序:缓存操作(如
@Cacheable
)在事务之前执行,可能导致缓存读取到未提交的数据。 - 期望顺序:先执行事务,再更新缓存,确保缓存与数据库一致。
7.2 调整执行顺序的实现原理
通过设置@Cacheable
的sync = true
属性,可将缓存操作改为在事务提交后执行。其核心是使用TransactionAwareCacheDecorator
装饰Cache
实例:
public class TransactionAwareCacheDecorator implements Cache {
private final Cache targetCache;
private final TransactionSynchronizationManager synchronizationManager;
@Override
public void put(Object key, Object value) {
if (synchronizationManager.isSynchronizationActive()) {
// 注册事务后处理器,在事务提交时执行缓存更新
synchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
targetCache.put(key, value);
}
});
} else {
targetCache.put(key, value);
}
}
}
sync = true
时,CacheInterceptor
会通过TransactionAwareCacheManager
获取装饰后的Cache
实例,确保缓存操作在事务提交后执行。
八、缓存抽象的扩展与自定义
8.1 自定义CacheManager
通过实现CacheManager
接口并注册为Bean,可集成自定义缓存实现(如Memcached):
@Configuration
public class CustomCacheManagerConfig {
@Bean
public CacheManager customCacheManager() {
return new CustomCacheManager();
}
private static class CustomCacheManager implements CacheManager {
@Override
public Cache getCache(String name) {
return new CustomCache(name);
}
private static class CustomCache implements Cache {
// 自定义缓存的get、put等实现
}
}
}
8.2 缓存解析器(CacheResolver)的应用
CacheResolver
接口允许开发者根据方法调用的上下文动态选择Cache
实例,这在需要灵活切换缓存策略的场景中十分有用。SimpleCacheResolver
是Spring提供的默认实现,它根据@Cacheable
等注解中指定的缓存名称直接获取Cache
实例:
public class SimpleCacheResolver implements CacheResolver {
private final CacheManager cacheManager;
public SimpleCacheResolver(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
@Override
public Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context) {
// 从缓存操作元数据中获取缓存名称
String[] cacheNames = context.getOperation().getCacheNames();
return Arrays.stream(cacheNames)
.map(this.cacheManager::getCache)
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
}
若需自定义解析逻辑,可实现CacheResolver
接口。例如,根据请求的来源或用户角色选择不同的缓存:
public class DynamicCacheResolver implements CacheResolver {
private final CacheManager cacheManager;
public DynamicCacheResolver(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
@Override
public Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String userRole = request.getHeader("User-Role");
if ("admin".equals(userRole)) {
return Collections.singletonList(cacheManager.getCache("admin-cache"));
}
return Collections.singletonList(cacheManager.getCache("default-cache"));
}
}
然后在配置类中通过@Bean
注册自定义的CacheResolver
,并在@Cacheable
等注解中指定使用该解析器:
@Configuration
public class CacheConfig {
@Bean
public CacheResolver dynamicCacheResolver(CacheManager cacheManager) {
return new DynamicCacheResolver(cacheManager);
}
}
@Cacheable(cacheResolver = "dynamicCacheResolver")
public Object getData() {
// ...
}
8.3 缓存切面(CacheAspect)的定制
Spring的缓存注解通过AOP切面实现,开发者可以通过继承CacheAspectSupport
类来自定义缓存切面逻辑。CacheAspectSupport
类包含了缓存操作的核心方法,如execute
方法用于执行缓存操作:
public abstract class CacheAspectSupport extends AbstractAspectJAdvice {
protected Object execute(CacheOperationInvocationContext<?> context) {
// 解析缓存操作元数据
CacheOperation operation = context.getOperation();
// 生成缓存键
Object key = generateKey(context);
// 获取Cache实例
Cache cache = getCache(operation, context);
// 处理缓存操作(@Cacheable、@CachePut、@CacheEvict)
if (operation instanceof CacheableOperation) {
return handleCacheable(context, cache, key);
} else if (operation instanceof CachePutOperation) {
return handleCachePut(context, cache, key);
} else if (operation instanceof CacheEvictOperation) {
handleCacheEvict(context, cache, key);
return null;
}
throw new IllegalArgumentException("Unsupported cache operation type");
}
}
例如,要在每次缓存操作前后添加日志记录,可以创建自定义切面类:
public class CustomCacheAspect extends CacheAspectSupport {
@Override
protected Object execute(CacheOperationInvocationContext<?> context) {
log.info("Before cache operation: {}", context.getOperation());
try {
return super.execute(context);
} finally {
log.info("After cache operation: {}", context.getOperation());
}
}
}
接着通过配置将自定义切面应用到缓存操作中,需要重新配置CacheInterceptor
并指定新的切面:
@Configuration
public class CacheAspectConfig {
@Bean
public CacheInterceptor customCacheInterceptor(CacheOperationSource cacheOperationSource) {
CustomCacheAspect customCacheAspect = new CustomCacheAspect();
customCacheAspect.setCacheOperationSource(cacheOperationSource);
CacheInterceptor interceptor = new CacheInterceptor();
interceptor.setCacheAspectSupport(customCacheAspect);
return interceptor;
}
}
九、缓存的监控与统计
9.1 缓存统计信息的采集
Spring的CacheStatistics
接口定义了缓存的统计指标,包括命中次数、未命中次数、缓存写入次数等。不同的CacheManager
实现会根据自身特点采集这些信息。以CaffeineCache
为例,它利用Caffeine库自身的统计功能:
public class CaffeineCache implements Cache {
private final com.github.benmanes.caffeine.cache.Cache<Object, Object> nativeCache;
@Override
public CacheStatistics getStatistics() {
com.github.benmanes.caffeine.cache.stats.CacheStats stats = nativeCache.stats();
return new SimpleCacheStatistics(
stats.hitCount(),
stats.missCount(),
stats.loadSuccessCount(),
stats.loadFailureCount(),
stats.evictionCount()
);
}
}
SimpleCacheStatistics
类实现了CacheStatistics
接口,将Caffeine的统计数据转换为Spring的标准格式。
9.2 暴露缓存统计信息
SpringBoot Actuator提供了对缓存统计信息的暴露支持。CacheStatisticsEndpoint
类负责将缓存统计信息以REST接口的形式暴露出来:
@Endpoint(id = "cacheStatistics")
public class CacheStatisticsEndpoint {
private final CacheManager cacheManager;
public CacheStatisticsEndpoint(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
@ReadOperation
public Map<String, CacheStatistics> getCacheStatistics() {
return cacheManager.getCacheNames().stream()
.collect(Collectors.toMap(
name -> name,
name -> cacheManager.getCache(name).getStatistics()
));
}
}
当应用集成了SpringBoot Actuator后,通过访问/actuator/cacheStatistics
接口,可以获取所有缓存的统计信息,结果以JSON格式返回,例如:
{
"users-cache": {
"cacheHits": 100,
"cacheMisses": 20,
"cacheEvictions": 5,
"cachePuts": 30
},
"products-cache": {
"cacheHits": 80,
"cacheMisses": 15,
"cacheEvictions": 3,
"cachePuts": 25
}
}
9.3 自定义监控指标
开发者还可以通过Micrometer框架自定义缓存监控指标。首先引入Micrometer相关依赖,然后在缓存操作中手动记录指标。例如,要记录自定义的缓存查询耗时指标:
public class CustomCache implements Cache {
private final Timer cacheQueryTimer;
public CustomCache(Timer cacheQueryTimer) {
this.cacheQueryTimer = cacheQueryTimer;
}
@Override
public ValueWrapper get(Object key) {
try (Timer.Sample sample = Timer.start(cacheQueryTimer)) {
// 实际的缓存查询逻辑
return null;
}
}
}
在配置类中注册Timer
bean:
@Configuration
public class CacheMetricsConfig {
@Bean
public Timer cacheQueryTimer(MeterRegistry meterRegistry) {
return Timer.builder("cache.query.time")
.description("Time taken to query cache")
.tag("cache", "custom-cache")
.register(meterRegistry);
}
}
这样,通过Micrometer集成的监控系统(如Prometheus、Grafana),就可以直观地查看自定义的缓存监控指标。
十、缓存一致性问题与解决方案
10.1 缓存不一致的产生原因
缓存不一致通常发生在数据更新场景,主要原因包括:
- 缓存更新顺序问题:如果先更新缓存再更新数据库,在数据库更新失败时,缓存中的数据就与数据库不一致;反之,先更新数据库再更新缓存,在缓存更新失败时也会导致不一致。
- 并发访问问题:多个线程同时对缓存和数据库进行读写操作,可能出现脏读、不可重复读等情况。例如,线程A读取缓存未命中后从数据库读取数据,此时线程B更新了数据库,线程A再将旧数据写入缓存,导致缓存数据过时。
10.2 缓存更新策略
- Cache-Aside(旁路缓存):
- 读操作:先查询缓存,若未命中则查询数据库,然后将结果写入缓存。
- 写操作:先更新数据库,再删除缓存(而不是更新缓存)。下次读取时会重新从数据库加载最新数据。
@CacheEvict
注解常用于实现该策略:
@Transactional @CacheEvict(value = "users", key = "#user.id") public void updateUser(User user) { userRepository.save(user); }
- Read-Through(读穿透):
- 读操作:查询缓存,若未命中则由缓存加载器自动从数据库加载数据并写入缓存。在Spring中,可以通过自定义
CacheLoader
实现,例如:
然后在配置public class CustomCacheLoader extends CacheLoader<Object, Object> { private final DataSource dataSource; public CustomCacheLoader(DataSource dataSource) { this.dataSource = dataSource; } @Override public Object load(Object key) throws Exception { // 从数据库查询数据的逻辑 return null; } }
CacheManager
时设置该加载器:public class CustomCacheManager implements CacheManager { @Override public Cache getCache(String name) { return Caffeine.newBuilder() .loader(new CustomCacheLoader(dataSource)) .build(); } }
- 读操作:查询缓存,若未命中则由缓存加载器自动从数据库加载数据并写入缓存。在Spring中,可以通过自定义
- Write-Through(写穿透):
- 写操作:先更新缓存,再更新数据库,确保缓存与数据库的数据一致。这种方式实现复杂,且对数据库性能要求较高。
- Write-Behind(写回):
- 写操作:先更新缓存,然后异步更新数据库。该方式性能较高,但可能出现数据丢失(如缓存更新后系统崩溃,未及时更新数据库)。
10.3 分布式环境下的缓存一致性
在分布式系统中,缓存一致性问题更为复杂,因为多个节点可能同时访问和修改缓存。常见解决方案包括:
- 分布式锁:在更新缓存和数据库时,通过分布式锁(如Redis锁、Zookeeper锁)保证同一时刻只有一个节点进行操作。
- 消息队列:将缓存更新操作发送到消息队列,由消费者按顺序处理,确保操作的一致性。例如,使用Kafka或RabbitMQ,在数据更新后发送一条消息,消费者接收到消息后执行缓存删除或更新操作:
@Service
public class UserService {
private final KafkaTemplate<String, String> kafkaTemplate;
@Transactional
public void updateUser(User user) {
userRepository.save(user);
kafkaTemplate.send("cache-update-topic", "users:" + user.getId());
}
}
消费者端监听该主题,并根据消息内容执行缓存操作:
@KafkaListener(topics = "cache-update-topic", groupId = "cache-consumer-group")
public void handleCacheUpdate(String message) {
// 解析消息并执行缓存删除或更新逻辑
}
十一、缓存与Spring WebFlux的集成
11.1 响应式缓存的特点
在Spring WebFlux响应式编程模型中,缓存操作需要适应异步和非阻塞的特性。与传统Servlet环境下的缓存相比,响应式缓存有以下特点:
- 基于流和发布者:数据以
Flux
或Mono
形式返回,缓存操作需要处理这些响应式类型。 - 背压处理:在缓存数据的读取和写入过程中,需要考虑背压问题,避免数据堆积。
11.2 响应式缓存的实现
Spring WebFlux提供了ResponseCache
接口来实现响应式缓存,其核心方法包括cache
和lookup
:
public interface ResponseCache {
Mono<Void> cache(ServerWebExchange exchange, Publisher<DataBuffer> body);
Mono<ResponseCache.Key> lookup(ServerWebExchange exchange);
}
cache
方法用于将响应体缓存起来,lookup
方法用于根据请求查找缓存的响应。DefaultResponseCache
是Spring提供的默认实现,它使用CacheManager
来管理缓存数据:
public class DefaultResponseCache implements ResponseCache {
private final CacheManager cacheManager;
@Override
public Mono<Void> cache(ServerWebExchange exchange, Publisher<DataBuffer> body) {
// 将响应体转换为字节数组并缓存
return DataBufferUtils.join(body)
.flatMap(dataBuffer -> {
byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
Cache cache = cacheManager.getCache("webflux-cache");
return Mono.fromRunnable(() -> cache.put(getCacheKey(exchange), bytes));
});
}
@Override
public Mono<ResponseCache.Key> lookup(ServerWebExchange exchange) {
Cache cache = cacheManager.getCache("webflux-cache");
Object cachedValue = cache.get(getCacheKey(exchange));
if (cachedValue != null) {
return Mono.just(new DefaultKey((byte[]) cachedValue));
}
return Mono.empty();
}
}
在WebFlux的配置类中,需要注册ResponseCache
bean:
@Configuration
public class WebFluxCacheConfig {
@Bean
public ResponseCache responseCache(CacheManager cacheManager) {
return new DefaultResponseCache(cacheManager);
}
}
11.3 响应式缓存的应用场景
响应式缓存适用于高并发、低延迟的场景,如实时数据查询、API网关等。例如,在一个查询用户信息的WebFlux接口中使用响应式缓存:
@RestController
public class UserController {
private final UserRepository userRepository;
private final ResponseCache responseCache;
@GetMapping("/users/{id}")
public Mono<User> getUser(@PathVariable Long id, ServerWebExchange exchange) {
return responseCache.lookup(exchange)
.flatMap(key -> {
byte[] bytes = key.getBody();
return Mono.just(objectMapper.readValue(bytes, User.class));
})
.switchIfEmpty(userRepository.findById(id)
.flatMap(user -> {
// 将查询结果缓存起来
return responseCache.cache(exchange, Mono.just(BufferFactory.buffer(objectMapper.writeValueAsBytes(user))))
.thenReturn(user);
})
);
}
}
通过这种方式,在后续相同请求时可以直接从缓存中获取数据,提升系统性能和响应速度。