SpringBoot缓存的使用:@Cacheable等缓存注解源码级剖析(22)

SpringBoot缓存的使用:@Cacheable等缓存注解源码级剖析

一、SpringBoot缓存抽象体系架构

1.1 缓存抽象的设计目标与核心接口

SpringBoot的缓存抽象旨在屏蔽不同缓存实现(如Redis、Caffeine、Guava)的底层差异,提供统一的编程模型。其核心接口如下:

  • Cache:代表单个缓存,定义了getputevict等基本操作。
  • CacheManager:管理多个Cache实例,负责创建、获取和销毁缓存。
  • CacheResolver:根据方法参数解析对应的Cache,用于支持多缓存策略。

spring-context模块中,AbstractCacheManagerCacheManager的抽象实现,提供了缓存实例的懒加载机制。例如:

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方法由具体实现类(如CaffeineCacheManagerRedisCacheManager)负责实现,体现了模板方法设计模式。

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引入的caffeineredis库),自动选择对应的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 缓存键的生成策略

@Cacheablekey属性用于指定缓存键,其值可以是SpEL表达式,默认使用SimpleKeyGenerator生成键。SimpleKeyGeneratorgenerateKey方法逻辑如下:

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方法调用时,会按以下流程处理:

  1. 解析缓存操作元数据:通过CacheOperationSource获取方法上的@Cacheable注解属性,包括缓存名称、键表达式、条件等。
  2. 生成缓存键:根据键生成策略和方法参数计算缓存键。
  3. 查询缓存:通过CacheManager获取对应的Cache实例,调用get(key)方法查询缓存值。
  4. 处理缓存命中/未命中
    • 命中:直接返回缓存值,不执行目标方法。
    • 未命中:执行目标方法,将结果存入缓存(通过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类。RedisCacheget方法通过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)的解析

@Cacheablecondition属性用于指定缓存生效的条件,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 调整执行顺序的实现原理

通过设置@Cacheablesync = 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 缓存更新策略

  1. Cache-Aside(旁路缓存)
    • 读操作:先查询缓存,若未命中则查询数据库,然后将结果写入缓存。
    • 写操作:先更新数据库,再删除缓存(而不是更新缓存)。下次读取时会重新从数据库加载最新数据。@CacheEvict注解常用于实现该策略:
    @Transactional
    @CacheEvict(value = "users", key = "#user.id")
    public void updateUser(User user) {
        userRepository.save(user);
    }
    
  2. 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();
        }
    }
    
  3. Write-Through(写穿透)
    • 写操作:先更新缓存,再更新数据库,确保缓存与数据库的数据一致。这种方式实现复杂,且对数据库性能要求较高。
  4. 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环境下的缓存相比,响应式缓存有以下特点:

  • 基于流和发布者:数据以FluxMono形式返回,缓存操作需要处理这些响应式类型。
  • 背压处理:在缓存数据的读取和写入过程中,需要考虑背压问题,避免数据堆积。

11.2 响应式缓存的实现

Spring WebFlux提供了ResponseCache接口来实现响应式缓存,其核心方法包括cachelookup

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);
                })
            );
    }
}

通过这种方式,在后续相同请求时可以直接从缓存中获取数据,提升系统性能和响应速度。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Android 小码蜂

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值