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支持多种缓存提供者,开发者可以根据实际需求选择合适的缓存方案。

相关推荐
Warren981 小时前
Spring Boot 整合网易163邮箱发送邮件实现找回密码功能
数据库·vue.js·spring boot·redis·后端·python·spring
小花鱼20257 小时前
redis在Spring中应用相关
redis·spring
郭京京7 小时前
redis基本操作
redis·go
似水流年流不尽思念7 小时前
Redis 分布式锁和 Zookeeper 进行比对的优缺点?
redis·后端
郭京京7 小时前
go操作redis
redis·后端·go
杨杨杨大侠9 小时前
Spring AI 系列(一):Spring AI 基础概念与架构入门
人工智能·spring·架构
Warren989 小时前
Spring Boot 拦截器返回中文乱码的解决方案(附全局优化思路)
java·网络·spring boot·redis·后端·junit·lua
XXD啊9 小时前
Redis 从入门到实践:Python操作指南与核心概念解析
数据库·redis·python
A尘埃15 小时前
Spring Event 企业级应用
java·spring·event
一叶飘零_sweeeet19 小时前
SPI 机制深度剖析:Java、Spring、Dubbo 的服务发现哲学与实战指南
java·spring·dubbo