Redis篇--常见问题篇8--缓存一致性3(注解式缓存Spring Cache)

1、概述

Spring Cache是Spring框架提供的一个缓存抽象层,旨在简化应用程序中的缓存管理。通过使用Spring Cache,开发者可以轻松地将缓存机制集成到业务逻辑中,而无需关心具体的缓存实现细节。

Spring Cache支持多种缓存提供者(如EhCache、Caffeine、Redis、Hazelcast等),并且提供了统一的API来操作缓存。

Spring Cache的主要目标是减少对后端数据源(如数据库、远程服务等)的访问次数,从而提高应用的性能和响应速度。它通过缓存计算结果或查询结果,避免重复执行相同的业务逻辑或数据库查询。

2、Spring Cache的核心概念

(1)、CacheManager

CacheManager是Spring Cache的核心接口之一,负责管理多个Cache实例。每个Cache实例对应一个缓存区域(cache region),用于存储特定类型的缓存数据。

CacheManager负责创建、获取和管理这些缓存区域。

常见的 CacheManager 实现包括:

  • ConcurrentMapCacheManager:基于内存的缓存,默认使用ConcurrentHashMap。
  • EhCacheCacheManager:基于EhCache的缓存。
  • CaffeineCacheManager:基于Caffeine的缓存。
  • RedisCacheManager:基于Redis的分布式缓存。
  • HazelcastCacheManager:基于Hazelcast的分布式缓存。

(2)、Cache

Cache接口表示一个具体的缓存区域,负责存储和检索缓存数据。每个Cache实例通常与一个命名空间相关联,例如user-cache或product-cache。

Cache提供了基本的缓存操作,如get()、put()和evict()。

(3)、@Cacheable

@Cacheable注解用于标记需要缓存的方法。当方法被调用时,Spring Cache会首先检查缓存中是否存在相同参数的结果。如果存在,则直接返回缓存中的结果,此时不会执行方法中的代码;如果不存在,则执行方法并将结果存入缓存。
示例:

java 复制代码
@Cacheable(value = "user-cache", key = "id")
public User getUserById(Long id) {
    // 查询数据库或远程服务
    return userRepository.findById(id).orElse(null);
}

解释:

  • value:指定缓存的名称,即缓存区域的名称。你可以为不同的业务逻辑配置不同的缓存区域。
  • key:指定缓存的键生成策略。默认情况下,Spring Cache会根据方法参数自动生成缓存键。你也可以使用SpEL(Spring Expression Language)来定义更复杂的键生成规则。
  • condition:指定缓存条件。只有当条件为true时,才会将结果存入缓存。
  • unless:指定不缓存的条件。即使方法执行成功,只有当条件为false时,才会将结果存入缓存。

@Cacheable 注意的点:
1、当方法中抛出异常时,不会缓存任何数据。
2、当方法中返回null时,默认情况下不会缓存null,但是可以通过配置cache-null-values属性实现缓存null。
例如:

java 复制代码
@Cacheable(value = "user-cache", key = "id", cache-null-values = true)
public User getUserById(Long id) {
    // 查询数据库或远程服务
    return userRepository.findById(id).orElse(null);
}

3、当方法中存在不确定因素时,不建议使用@Cacheable注解。

如:方法中你返回了对象,同时也打印数据信息到日志中。第一次请求会按照你的预期执行。但第二次请求会直接走缓存,不会走方法,从而造成无法打印日志。

所以在使用@Cacheable注解时,方法内建议使用最少且确定的查询操作。可以mapper文件中执行,如果有其他操作建议放在之前的service中去完成。

4、假设我们只想缓存非空的用户信息,使用unless示例。

注意unless是指定不缓存的条件,这里设置result==null,实际会缓存result!=null的数据。
示例:

java 复制代码
@Cacheable(value = "user-cache", key = "id", unless = "result == null")
public User getUserById(Long id) {
    // 查询数据库或远程服务
    return userRepository.findById(id).orElse(null);
}

5、带条件的缓存condition

假设我们只在用户年龄大于18岁时缓存用户信息
示例:

java 复制代码
@Cacheable(value = "user-cache", key = "id", condition = "id > 18")
public User getUserById(Long id) {
    // 查询数据库或远程服务
    return userRepository.findById(id).orElse(null);
}

6、指定缓存key的格式

可以使用SpEL表达式来组合多个参数,生成更复杂的缓存键
示例:

java 复制代码
@Cacheable(value = "user-cache", key = "id + '_' + username")
public User getUserByIdAndUsername(Long id, String username) {
    // 查询数据库或远程服务
    return userRepository.findByIdAndUsername(id, username).orElse(null);
}

(4)、@CachePut

@CachePut注解用于更新缓存中的数据。与@Cacheable不同,@CachePut总是会执行方法,并将结果存入缓存。它通常用于更新缓存中的数据,而不影响方法的正常执行。
示例:

java 复制代码
@CachePut(value = "user-cache", key = "user.id")
public User updateUser(User user) {
    // 更新数据库或远程服务
    userRepository.save(user);
    return user;
}

解释:

  • value:指定缓存的名称。
  • key:指定缓存的键生成策略。
  • condition:指定缓存条件。

(5)、@CacheEvict

@CacheEvict注解用于清除缓存中的数据。它可以用于删除单个缓存条目,也可以清除整个缓存区域。它通常用于在数据发生变更时清理过期的缓存数据。
示例:

java 复制代码
@CacheEvict(value = "user-cache", key = "id")
public void deleteUser(Long id) {
    // 删除数据库或远程服务中的数据
    userRepository.deleteById(id);
}

解释:

  • value:指定缓存的名称。
  • key:指定要清除的缓存条目的键。
  • allEntries:如果设置为true,则清除整个缓存区域中的所有条目。
  • beforeInvocation:如果设置为 true,则在方法执行之前清除缓存;否则,在方法执行之后清除缓存。

(6)、@Caching

@Caching注解允许你组合多个缓存注解(如@Cacheable、@CachePut和@CacheEvict),以便在同一方法上应用多个缓存策略。
示例1:

保存user-cache中的键,同时删除user-stats-cache中的键

java 复制代码
@Caching(
    cacheable = @Cacheable(value = "user-cache", key = "id"),
    evict = @CacheEvict(value = "user-stats-cache", key = "id")
)
public User getUserById(Long id) {
    // 查询数据库或远程服务
    return userRepository.findById(id).orElse(null);
}

示例2:

一条信息保存到多个缓存区域。

java 复制代码
@Caching(
        cacheable = {
            @Cacheable(value = "user-cache", key = "id"),  // 缓存到 user-cache
            @Cacheable(value = "admin-cache", key = "id", condition = "user.isAdmin()")}
    )
    public User getUserById(Long id) {
        return userRepository.findById(id).orElse(null);
    }

3、Spring Cache的工作原理

1、拦截方法调用:当调用带有缓存注解的方法时,Spring AOP会拦截该方法的执行。

2、检查缓存:Spring Cache会根据注解中的配置(如@Cacheable)检查缓存中是否存在相同参数的结果。

  • 如果存在缓存结果,则直接返回缓存中的数据,跳过方法的实际执行。
  • 如果不存在缓存结果,则继续执行方法。
    3、执行方法:如果缓存中没有找到结果,Spring Cache会执行方法,并将结果存入缓存。
    4、更新或清除缓存:根据注解的配置(如@CachePut或@CacheEvict),Spring Cache可能会在方法执行前后更新或清除缓存。

4、集成Spring Cache

(1)、启用缓存支持

要在Spring Boot项目中启用缓存支持,首先需要在主类或配置类上添加@EnableCaching注解。
示例:

java 复制代码
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching
public class CacheApplication {
    public static void main(String[] args) {
        SpringApplication.run(CacheApplication.class, args);
    }
}

(2)、选择缓存提供者

Spring Cache支持多种缓存提供者。你可以根据需求选择合适的缓存实现。这里以Redis为例。

首先,添加 Redis 的依赖:

java 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

然后,配置Redis缓存管理器(application.yml):

java 复制代码
spring:
  cache:
    type: redis    // 指定spring Cache的方法为Redis(重点)
  redis:
    host: localhost
    port: 6379
    timeout: 5000   连接超时时间
    lettuce:
      pool:
        max-active: 8
        max-idle: 8
        min-idle: 0
  redis:
    cache:
      time-to-live: 60000   默认缓存过期时间为 60 秒(毫秒)

(3)、编写配置类

这里配置类主要是为了设置合理的序列化,以及针对不同域的key设置不同的过期时间。

java 复制代码
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.CacheErrorHandler;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.*;

import java.lang.reflect.Method;
import java.nio.charset.Charset;
import java.time.Duration;

@Slf4j
@Configuration
@EnableCaching
@EnableConfigurationProperties(RedisProperties.class)
public class CacheConfig extends CachingConfigurerSupport {
    private static final FastJsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);


    //缓存管理器。可以管理多个缓存
    //只有CacheManger才能扫描到cacheable注解
    //spring提供了缓存支持Cache接口,实现了很多个缓存类,其中包括RedisCache。但是我们需要对其进行配置,这里就是配置RedisCache

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        RedisCacheManager cacheManager = RedisCacheManager.RedisCacheManagerBuilder
                //Redis链接工厂
                .fromConnectionFactory(connectionFactory)
                //缓存配置 通用配置  默认存储一小时
                .cacheDefaults(getCacheConfigurationWithTtl(Duration.ofHours(1)))
                //配置同步修改或删除  put/evict
                .transactionAware()
                //对于不同的cacheName我们可以设置不同的过期时间
//                .withCacheConfiguration("app:",getCacheConfigurationWithTtl(Duration.ofHours(5)))
                .withCacheConfiguration("user:",getCacheConfigurationWithTtl(Duration.ofHours(2)))
                .build();
        return cacheManager;
    }
    //缓存的基本配置对象
    private   RedisCacheConfiguration getCacheConfigurationWithTtl(Duration duration) {
        return RedisCacheConfiguration
                .defaultCacheConfig()
                //设置key value的序列化方式
                // 设置key为String
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                // 设置value 为自动转Json的Object
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(fastJsonRedisSerializer))
                // 不缓存null
                .disableCachingNullValues()
                // 设置缓存的过期时间
                .entryTtl(duration);
    }

    //缓存的异常处理
    @Bean
    @Override
    public CacheErrorHandler errorHandler() {
        // 异常处理,当Redis发生异常时,打印日志,但是程序正常走
        log.info("初始化 -> [{}]", "Redis CacheErrorHandler");
        return new CacheErrorHandler() {
            @Override
            public void handleCacheGetError(RuntimeException e, Cache cache, Object key) {
                log.error("Redis occur handleCacheGetError:key -> [{}]", key, e);
            }
            @Override
            public void handleCachePutError(RuntimeException e, Cache cache, Object key, Object value) {
                log.error("Redis occur handleCachePutError:key -> [{}];value -> [{}]", key, value, e);
            }

            @Override
            public void handleCacheEvictError(RuntimeException e, Cache cache, Object key) {
                log.error("Redis occur handleCacheEvictError:key -> [{}]", key, e);
            }

            @Override
            public void handleCacheClearError(RuntimeException e, Cache cache) {
                log.error("Redis occur handleCacheClearError:", e);
            }
        };
    }

   /** @Override
    @Bean("myKeyGenerator")   // 自定义key的生成策略
    public KeyGenerator keyGenerator() {
        return new KeyGenerator() {
            @Override
            public Object generate(Object target, Method method, Object... params) {
                StringBuffer sb = new StringBuffer();
                sb.append(target.getClass().getName());
                sb.append(method.getName());
                for (Object obj : params) {
                    sb.append(obj.toString());
                }
                return sb.toString();
            }
        };
} 
**/
}

(4)、编写业务,使用注解

在业务需要的mapper文件中定义方法,参考上诉中的注解即使用示例就可以了。

5、Spring Cache实践建议

- 合理选择缓存提供者 :根据应用场景选择合适的缓存提供者。对于单机应用,可以选择内存缓存(如Caffeine);对于分布式应用,可以选择分布式缓存(如Redis)。
- 避免缓存雪崩 :缓存雪崩是指大量缓存条目同时失效,导致短时间内大量请求直接打到后端数据源。可以通过设置不同的TTL或使用缓存预热机制来避免缓存雪崩。
- 避免缓存穿透 :缓存穿透是指查询的缓存键不存在,且后端数据源也没有对应的数据。可以通过设置默认值或使用布隆过滤器来避免缓存穿透。
- 避免缓存击穿:缓存击穿是指某个热点数据恰好在缓存失效时,大量请求同时访问该数据,导致后端数据源压力过大。可以通过加锁或使用互斥锁机制来避免缓存击穿。

  • 定期清理无效缓存:定期清理不再使用的缓存条目,避免缓存占用过多内存资源。

6、总结

Spring Cache是一个强大且灵活的缓存框架,能够显著提升应用的性能和响应速度。通过使用@Cacheable、@CachePut和@CacheEvict等注解,开发者可以轻松地将缓存机制集成到业务逻辑中,而无需关心具体的缓存实现细节。Spring Cache支持多种缓存提供者,开发者可以根据实际需求选择合适的缓存方案。

相关推荐
编码浪子1 小时前
Springboot3国际化
java·spring·mybatis
HUNAG-DA-PAO1 小时前
Spring AOP是什么
java·jvm·spring
带刺的坐椅1 小时前
Solon v3.0.5 发布!(Spring 生态可以退休了吗?)
java·spring·solon
怒码ing1 小时前
Java包装类型的缓存
java·开发语言·缓存
凡人的AI工具箱1 小时前
每天40分玩转Django:Django缓存
数据库·人工智能·后端·python·缓存·django
安然望川海1 小时前
springboot 使用注解设置缓存时效
spring boot·后端·缓存
大G哥2 小时前
k8s创建单例redis设置密码
数据库·redis·云原生·容器·kubernetes
A22742 小时前
Redis——双写一致性
java·redis·缓存
小林想被监督学习3 小时前
Spring Boot 整合 RabbitMQ(在Spring项目中使用RabbitMQ)
spring boot·spring·java-rabbitmq
小沈同学呀3 小时前
Redis KEYS查询大批量数据替代方案(推荐SCAN 命令)
redis