实战之Spring Cache与Caffeine的整合优化高并发场景下的接口请求性能Spring Cache与Caffe
引言
最近现场总是cpu飙高,公司的大佬们通力合作发现啊,有大量数据一次性下发(削峰处理,多线程处理),商品策略计算时,商品与货位的笛卡尔积过大(减少商品计算数量),因为项目历史原因,一次性计算需要2W多条不同sql计算,导致mybatis缓存过多(不要mybatis 缓存),还有就是分到我头上接口请求过多且参数一样结果也一样,故此大佬们希望缓存住相同参数获取到的结果,故此基于现有项目和希望,采取Spring Cache + Caffeine的整合。
概述
在现代的应用开发中,缓存是一项至关重要的技术,它能显著提高应用性能,减少数据库压力,提升用户体验。 Spring Framework 为Java开发者提供了一个强大的缓存抽象层——Spring Cache,它让缓存的使用变得简单而统一。 而Caffeine,作为一个高性能的Java缓存库,因其轻量级和快速的特性,被广泛应用于各种场景。 本文将详细介绍如何将Spring Cache与Caffeine整合,实现一个高效的缓存系统。
Spring Cache的简介
Spring Cache是Spring框架中的一部分,它提供了一个声明式的缓存抽象层,允许开发者通过简单的注解来控制方法的缓存行为。
常用的注解包括@Cacheable
、@CachePut
和@CacheEvict
。
通过这些注解,开发者可以轻松地实现方法结果的缓存、更新以及缓存的清除。
Caffeine缓存库的特点
Caffeine是一个由Ben Manes开发的Java缓存库,它是Guava Cache的一个优秀替代品,特别设计用于提供高性能的本地缓存。Caffeine的主要特点有:
- 高性能:Caffeine使用了高效的算法和数据结构,如窗口TinyLFU策略,以提供最佳的命中率。
- 轻量级:Caffeine的API简洁,依赖少,易于集成。
- 丰富的特性:支持大小限制、过期策略、引用回收等。
正文
在Spring框架中整合Caffeine缓存,通常需要以下几个步骤:
- 添加依赖
- 配置缓存管理器
- 使用缓存注解
配置pom
首先,在项目的pom.xml
文件中添加了Caffeine相关的依赖。
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.9.3</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.17</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>5.3.17</version>
</dependency>
配置类CacheConfig
然后,配置Spring的缓存管理器以使用Caffeine。在Spring配置类中添加以下配置:
@Configuration
@EnableCaching
public class StrategyCacheConfig {
private CacheManager conditionalCacheManager(CaffeineCacheManager delegate) {
if ("true".equalsIgnoreCase(SysConfigUtils.getValue("cache_conditional", "true"))) {
return delegate;
} else {
return new NoOpCacheManager();
}
}
@Bean
public CacheManager strategyCacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.registerCustomCache("siteStrategyCache",
Caffeine.newBuilder().expireAfterWrite(30, TimeUnit.HOURS).maximumSize(200).build());
cacheManager.registerCustomCache("operationPointCache",
Caffeine.newBuilder().expireAfterWrite(30, TimeUnit.HOURS).maximumSize(200).build());
cacheManager.registerCustomCache("seedingWallSideCache",
Caffeine.newBuilder().expireAfterWrite(30, TimeUnit.HOURS).maximumSize(200).build());
cacheManager.registerCustomCache("strategyWaveDetailControl",
Caffeine.newBuilder().expireAfterWrite(30, TimeUnit.HOURS).maximumSize(200).build());
cacheManager.registerCustomCache("strategyElectronicLabel",
Caffeine.newBuilder().expireAfterWrite(30, TimeUnit.HOURS).maximumSize(200).build());
disableCacheManagerDynamic(cacheManager);
// return conditionalCacheManager(cacheManager);
return cacheManager;
}
@Bean
@Primary
public CacheManager baseManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.registerCustomCache("station",
Caffeine.newBuilder().expireAfterWrite(30, TimeUnit.SECONDS).maximumSize(200).build());
return cacheManager;
}
/**
* 解决 CaffeineCacheManager.setDynamic(false) 效果,让 getCache("notExists")返回null
* 确保只有预定义的缓存名称可被使用,请求不存在的缓存会抛出异常
* 如果不设置,则如果使用了不存在的缓存名时,会自动创建新的缓存空间,没有大小和过期限制
*
* @param cacheManager CaffeineCacheManager
*/
public static void disableCacheManagerDynamic(CaffeineCacheManager cacheManager) {
try {
Field field = cacheManager.getClass().getDeclaredField("dynamic");
field.setAccessible(true);
field.set(cacheManager, false);
} catch (Exception ignore) {
System.out.println("disableCacheManagerDynamic error:" + ignore.getMessage());
}
}
}
因为项目是多模块开发,故此尽量多个模块使用一个CacheConfig 配置,如果实在不行可以根据不同包建立自己的CacheConfig,但是,整个项目中 @Primary 生成的bean 有且有仅有一个CacheManager。
故此我这边做了一个实验:
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.cache.interceptor.KeyGenerator;
import java.lang.reflect.Field;
import java.util.concurrent.TimeUnit;
@Configuration
@EnableCaching
public class ToteRCacheConfig {
@Bean
public CacheManager tRCacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.registerCustomCache("rCForBoxCode",
Caffeine.newBuilder().expireAfterWrite(10, TimeUnit.SECONDS).maximumSize(200).build());
//disableCacheManagerDynamic(cacheManager);
return cacheManager;
}
@Bean("customKeyGenerator")
public KeyGenerator keyGenerator() {
return new CustomKeyGenerator();
}
}
配置Caffeine和Redis混合
这边完一个稍微复杂的,因为有的地方想用Caffeine做缓存,有些地方使用Redis,故此也可以实现Caffeine和Redis混合配置。
具体代码实现如下:
import com.github.ben-manes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
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.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import java.util.concurrent.TimeUnit;
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager caffeineCacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(1000));
return cacheManager;
}
@Bean
public CacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(60)) // 设置缓存有效期一小时
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(cacheConfiguration)
.build();
}
}
在这个配置类中,定义了caffeineCacheManager
和redisCacheManager
两个bean。caffeineCacheManager
使用CaffeineCacheManager
,它配置了Caffeine缓存的过期时间和最大大小。redisCacheManager
使用RedisCacheManager
,配置了Redis缓存的有效期和序列化机制。
现在,当需要在服务中使用缓存时,可以指定使用哪个CacheManager
:
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class DataService {
// 使用基于Caffeine的缓存管理器
@Cacheable(cacheNames = "localCache", cacheManager = "caffeineCacheManager", key = "#id")
public MyData getDataFromLocalCache(Long id) {
// 方法实现,比如从数据库加载数据
}
// 使用基于Redis的缓存管理器
@Cacheable(cacheNames = "distributedCache", cacheManager = "redisCacheManager", key = "#id")
public MyData getDataFromDistributedCache(Long id) {
// 方法实现,比如从数据库加载数据
}
}
在上面的服务类中,getDataFromLocalCache
方法使用了基于Caffeine的缓存管理器,而getDataFromDistributedCache
方法使用了基于Redis的缓存管理器。这样,就可以根据不同的业务需求选择不同的缓存策略。
具体使用
然后,使用Spring的缓存注解在服务层中添加缓存逻辑。例如:
@Slf4j
@Service
public class TReBasePersistenceServiceImpl implements TRBasePersistenceService {
@Autowired
TRBaseMapper tRBaseMapper;
@Override
@Cacheable(cacheManager = "tRCacheManager", cacheNames = "rCForBoxCode",keyGenerator = "customKeyGenerator", sync = true)
public List<String> getRCForBoxCode(Integer deliverType, List<String> shelfCodes) {
List<String> all = Lists.newArrayList();
if (CollectionUtils.isNotEmpty(shelfCodes)) {
List<List<String>> splitList = com.google.common.collect.Lists.partition(shelfCodes, 500);
for (List<String> singleList : splitList) {
all.addAll(tRBaseMapper.getRCForBoxCode(deliverType, singleList));
}
return all.stream().distinct().collect(Collectors.toList());
}
return all;
}
@Cacheable(cacheManager = "tRCacheManager", cacheNames = "rCForBoxCode",
key = "T(com.***.***.***.tr.strategy.base.DigestUtils).generateKey(#deliverType, #shelfCodes)", sync = true)
public List<String>getRCForBoxCode1(Integer deliverType, List<String> shelfCodes) {
List<String> all = Lists.newArrayList();
if (CollectionUtils.isNotEmpty(shelfCodes)) {
List<List<String>> splitList = com.google.common.collect.Lists.partition(shelfCodes, 500);
for (List<String> singleList : splitList) {
all.addAll(tRBaseMapper.getRCForBoxCode(deliverType, singleList));
}
return all.stream().distinct().collect(Collectors.toList());
}
return all;
}
}
在上述代码中,其实写了两种key 的生成规则。简单的key就不过多介绍网上查查就有,这边直接进入生成key的详细论述。
因为项目中不可能都是那种简单参数如就一个字符串,key = #aaa ,这边会出集合啊,对象等大对象,如果直接拿他们做key,则缓存中的key 就会很大,拿他们做缓存就没有啥意义。故此需要计算出小的且唯一值进行存储。
这边采取了两种方式,
解决key值过大之-使用keyGenerator
要使用keyGenerator
生成缓存键,因为shelfCodes
列表非常大,将整个列表内容作为缓存键的一部分可能会导致生成的键非常长,这可能会影响性能并增加内存消耗。为了避免这个问题,可以考虑使用列表的哈希值来代替列表内容本身。这样可以确保缓存键的长度保持恒定,同时仍然基于shelfCodes
列表的内容生成唯一的键。下面是一个优化后的KeyGenerator
实现,它使用deliverType
和shelfCodes
列表的哈希值来生成缓存键
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.List;
@Component("customKeyGenerator")
public class CustomKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
Integer deliverType =(Integer) params[0];
@SuppressWarnings("unchecked")
List<String> shelfCodes = (List<String>) params[1];
// 如果shelfCodes为空或者为空列表,使用一个特定的字符串代替
String shelfCodesKeyPart = (shelfCodes == null || shelfCodes.isEmpty())
? "emptyList"
: String.valueOf(shelfCodes.hashCode());
// 生成缓存键
String key = "DeliverType_" + deliverType + "_ShelfCodes_" + shelfCodesKeyPart;
return key;
}
}
可以在@Cacheable
注解中使用keyGenerator
属性引用自定义KeyGenerator
:
import org.springframework.cache.annotation.Cacheable;
public class TReBasePersistenceServiceImpl implements TRBasePersistenceService {
@Cacheable(cacheManager = "tRCacheManager", cacheNames = "rCForBoxCode",keyGenerator = "customKeyGenerator", sync = true)
public List<String> getRCForBoxCode(Integer deliverType, List<String> shelfCodes) {
// 方法实现
}
}
这样 使用了keyGenerator = "customKeyGenerator"
来告诉Spring使用我们自定义的KeyGenerator
生成缓存键。这样,每次调用getRCForBoxCode
方法时,都会根据deliverType
和shelfCodes
的值生成一个唯一的缓存键。
生成的缓存键应该是唯一的,以确保不同的参数组合不会导致缓存键冲突。此外,如果shelfCodes
列表非常大,生成的键可能会非常长,这可能不是一个好的做法,因为它会增加内存使用,并且可能会影响缓存存储的性能。在这种情况下,可能需要考虑使用更复杂的键生成策略
解决key值过大之-使用参数摘要
对于大型对象或多个参数,可以考虑使用摘要算法(如MD5、SHA-256)来生成一个固定长度的缓存键
参数摘要方案是指使用某种摘要算法(如MD5、SHA-1、SHA-256等)来生成一个固定长度的字符串,作为缓存键。这种方法特别适用于方法参数较多或参数对象较大的情况,因为它可以将复杂的参数组合转换为一个简洁的标识符。以下是如何实现参数摘要方案的步骤和代码示例:
步骤1:创建工具类方法生成摘要键
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
public class DigestUtils {
// 使用SHA-256摘要算法生成缓存键
public static String generateKey(Object... params) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
for (Object param : params) {
// 检查参数是否为空
if (param != null) {
if (param instanceof Collection<?>) {
// 对于列表,计算列表元素的组合哈希值
Collection<?> listParam = (Collection<?>) param;
int listHash = listParam.hashCode();
digest.update(Integer.toString(listHash).getBytes(StandardCharsets.UTF_8));
} else {
// 将参数转换为字符串表示(这里需要自定义转换逻辑,例如调用toString或使用JSON序列化)
String paramString = String.valueOf(param);
// 更新摘要信息
digest.update(paramString.getBytes(StandardCharsets.UTF_8));
}
} else {
// 对于null参数,使用一个特定的字符串代替,例如"null"
digest.update("null".getBytes(StandardCharsets.UTF_8));
}
}
// 生成摘要字节
byte[] hash = digest.digest();
// 将字节转换为十六进制字符串
return bytesToHex(hash);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Could not generate hash", e);
}
}
// 将字节转换为十六进制字符串
private static String bytesToHex(byte[] hash) {
StringBuilder hexString = new StringBuilder(2 * hash.length);
for (byte b : hash) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
}
public static void main(String[] args) {
List<String> c1 = new ArrayList<>();
List<String> c3 = new ArrayList<>();
List<String> c4 = new ArrayList<>();
for (int i = 0; i <= 9000; i++) {
c1.add(String.valueOf(i));
c3.add(String.valueOf(i));
}
for (int i = 9000; i >=0; i--) {
c4.add(String.valueOf(i));
}
Long s = System.currentTimeMillis();
System.out.println("**************" + generateKey("123", 321321L, 321321333, c1));
System.out.println("**************" + (System.currentTimeMillis() - s));
System.out.println("**************" + generateKey("123", 321321L, 321321333, c3));
c4.sort(Comparator.comparingInt(Integer::parseInt));
System.out.println("**************" + generateKey("123", 321321L, 321321333, c4));
System.out.println("**************"+ c1.size()+"**************"+ c4.size());
List<String> c2 = null;
System.out.println("**************" + generateKey("123", 321321L, 321321333, c2));
}
static class C {
private String name;
private String age;
public C(String name, String age) {
this.name = name;
this.age = age;
}
}
}
步骤2:在@Cacheable
注解中使用工具类方法
@Cacheable(cacheManager = "tRCacheManager", cacheNames = "rCForBoxCode",
key = "T(com.***.base.DigestUtils).generateKey(#deliverType, #shelfCodes)", sync = true)
public List<String> getRCForBoxCode1(Integer deliverType, List<String> shelfCodes) {
//代码实现
}
在这个示例中,定义了一个DigestUtils
工具类,它包含一个generateKey
方法,该方法使用SHA-256算法来生成参数的摘要。在@Cacheable
注解的key
属性中,我们使用了Spring Expression Language(SpEL)来调用DigestUtils.generateKey
方法,并传入方法参数来生成缓存键。
这种方法的优点是无论参数有多复杂或多大,生成的缓存键都是一个固定长度的字符串,这有助于保持缓存的一致性和性能。
请注意,在生成参数字符串表示时,需要确保参数的转换是一致的,以便相同的参数组合总是产生相同的缓存键。在本例中,我们使用了String.valueOf(param)
,但在实际应用中,可能需要更复杂的转换逻辑,例如使用JSON序列化器将对象转换为JSON字符串,以确保对象的每个属性都被考虑在内。
遇到超坑问题-跨JVM
在开发测试中发现,明明调用CacheEvict
@CacheEvict(cacheManager = "strategyCacheManager", cacheNames = "siteStrategyCache", allEntries = true)
摸索解决问题
进行删除缓存,但是就删除不掉.后来自我实现一套了删除缓存的策略如下:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Derek_Smart
* @company
* @project
* @date 2024/8/20 7:42
* @since 1.0.0
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomCacheEvict {
String[] cacheNames();
String key() default "";
String cacheManager() default "";
//String keyGenerator() default "";
String condition() default "";
boolean allEntries() default false;
}
和切面
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.CacheManager;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* @author derek_smart
* @company
* @project
* @date 2024/8/20 8:32
* @since 1.0.0
*/
@Aspect
@Component
public class CustomCacheEvictAspect implements ApplicationContextAware {
@Autowired
private CacheManager cacheManager; // 默认的缓存管理器
private ApplicationContext applicationContext;
private ExpressionParser expressionParser = new SpelExpressionParser();
private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
// 省略其他代码...
@Around("@annotation(customCacheEvict)")
public Object cacheEvictAdvice(ProceedingJoinPoint joinPoint, CustomCacheEvict customCacheEvict) throws Throwable {
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
Object[] args = joinPoint.getArgs();
Object target = joinPoint.getTarget();
// 创建评估上下文,使用 DefaultParameterNameDiscoverer
MethodBasedEvaluationContext evaluationContext = new MethodBasedEvaluationContext(
target, method, args, parameterNameDiscoverer);
// 计算 condition 表达式的值
boolean conditionPasses = true;
if (!customCacheEvict.condition().isEmpty()) {
Expression expression = expressionParser.parseExpression(customCacheEvict.condition());
conditionPasses = expression.getValue(evaluationContext, Boolean.class);
}
// 执行目标方法
Object result = joinPoint.proceed();
// 如果 condition 表达式为 true,则清除缓存
if (conditionPasses) {
for (String cacheName : customCacheEvict.cacheNames()) {
CacheManager managerToUse = this.cacheManager;
if (!customCacheEvict.cacheManager().isEmpty()) {
// 如果注解中指定了缓存管理器,则获取相应的缓存管理器
managerToUse = getCacheManagerByName(customCacheEvict.cacheManager());
}
if (customCacheEvict.allEntries()) {
// 清除指定缓存中的所有条目
managerToUse.getCache(cacheName).clear();
} else {
// 如果指定了 keyGenerator,则生成 key 并清除对应条目
// 需要实现 keyGenerator 相关逻辑
managerToUse.getCache(cacheName).evict(customCacheEvict.key());
}
}
}
return result;
}
private CacheManager getCacheManagerByName(String cacheManagerName) {
// 从应用上下文中按名称获取缓存管理器
return ApplicationContextUtils.loadBean(cacheManagerName);
// return applicationContext.getBean(cacheManagerName, CacheManager.class);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
}
然后再调用方法上实验
@CustomCacheEvict(cacheManager = "strategyCacheManager", cacheNames = "siteStrategyCache", allEntries = true)
一样删除不掉,但是通过自己写的切面 可以很清除的看到strategyCacheManager对象中的cache 就是为空,无论如何去删就是删除不掉。然后通过
log.info("地址:Service instance: " + System.identityHashCode(siteStrategyService));
打印整个虚拟机生命周期内hashCode,发现 生成缓存的地方和删除缓存地方 居然不一样。突然想到我们项目部署是两个war,生成和删除刚好分别在两个war 中,故此其对应spring 上下文不一样,获取的本地缓存也不一样。
解决方案
故此我这边采取新的方式进行解决多war下缓存不一致,解决CacheEvict失效问题:创建一个可以跨 WAR 文件共享的缓存机制,需要使用一个外部的缓存服务器,这样不同的 Spring 上下文可以访问同一个缓存实例。下面是一个使用 Redis 作为外部缓存服务器的示例。Redis 是一个开源的、支持网络、基于内存、键值存储数据库,它具有高性能和灵活性,非常适合用作分布式缓存解决方案 实现代码如下:
public class StrategyCacheConfig {
@Bean
public CacheManager strategyCacheManager(RedissonClient redissonClient) {
// 创建缓存名称到缓存配置的映射
// 创建缓存名称到缓存配置的映射
Map<String, CacheConfig> configMap = new HashMap<>();
configMap.put("siteStrategyCache", new CacheConfig(TimeUnit.SECONDS.toMillis(30), 0)); // 30小时
configMap.put("operationPointCache", new CacheConfig(TimeUnit.HOURS.toMillis(30), 0)); // 30天
configMap.put("seedingWallSideCache", new CacheConfig(TimeUnit.HOURS.toMillis(30), 0)); // 30秒
configMap.put("strategyWaveDetailControl", new CacheConfig(TimeUnit.HOURS.toMillis(30), 0)); // 30秒
configMap.put("strategyElectronicLabel", new CacheConfig(TimeUnit.HOURS.toMillis(30), 0)); // 30秒
// 创建 RedissonSpringCacheManager 并返回
return new RedissonSpringCacheManager(redissonClient, configMap);
}
这样现在,RedisCacheManager
应该已经配置好了,并且可以跨 WAR 文件共享缓存。
当然在本项目中,依赖redis地方太多,且数据量过大,I/O耗时,从redis 获取数据和从数据库获取数据有些地方时间相差不大,故此该方案只是目前可选方案之一。
总结
通过整合Spring Cache和Caffeine,能够以最小的开销实现一个功能强大而高效的缓存系统。Spring Cache的抽象层让缓存的使用变得简单,而Caffeine的高性能确保了我们的应用能够快速响应用户的请求。这种组合为构建可伸缩的Java应用提供了坚实的基础,无论是在云环境还是传统的部署环境中。
缓存是现代应用架构中不可或缺的一部分,而Spring Cache与Caffeine的整合使得缓存的实现和维护变得既简单又高效。通过这篇文章的介绍,希望能够在自己的项目中成功应用这些技术,为用户带来更快、更稳定的服务体验。
本文皆为个人原创,请尊重创作,未经许可不得转载。
转载自:https://juejin.cn/post/7410654079842697225