【黑马点评优化】之使用Caffeine+Redis实现应用级二层缓存
- 1 缓存雪崩定义及解决方案
- 2 为什么要使用多级缓存
- 3 Redis+Caffeine实现应用层二级缓存原理
- 4 利用Caffeine+Redis解决Redis突然宕机导致的缓存雪崩问题
- 4.1 pom.xml文件引入相关依赖
- 4.2 本地缓存配置类
- 4.3 修改ShopServiceImpl中的queryById方法
- 5 测试
在这里修改一下黑马点评2商户查询的方法。使用Redis+Caffeine实现应用层二级缓存来解决缓存雪崩 的问题。
添加Caffeine的过程参考博客如下:
SpringBoot 集成 Caffeine、Redis实现双重缓存方式(-)caffeine redis-CSDN博客
1 缓存雪崩定义及解决方案
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
解决方案:
● 给不同的Key的TTL添加随机值 (同一时段,所以给不同key设置不同的TTL)
● 利用Redis集群提高服务的可用性
● 给缓存业务添加降级限流策略 (微服务)
● 给业务添加多级缓存
2 为什么要使用多级缓存
如果只使用redis来做缓存我们会有大量的请求到redis,但是每次请求的数据都是一样的,假如这一部分数据就放在应用服务器本地,那么就省去了请求redis的网络开销,请求速度就会快很多;
如果只使用Caffeine来做本地缓存,我们的应用服务器的内存是有限,并且单独为了缓存去扩展应用服务器是非常不划算。所以,只使用本地缓存也是有很大局限性的;
因此在项目中,我们可以将热点数据放本地缓存,作为一级缓存,将非热点数据放redis缓存,作为二级缓存,减少Redis的查询压力。
使用流程大致如下:
- 首先从一级缓存(caffeine-本地应用内)中查找数据;
- 如果没有的话,则从二级缓存(redis-内存)中查找数据;
- 如果还是没有的话,再从数据库(数据库-磁盘)中查找数据;
3 Redis+Caffeine实现应用层二级缓存原理
Redis 作为分布式缓存:
- Redis 具有高性能、丰富的数据结构和可扩展性,适合作为分布式缓存存储大量的数据。它可以在多服务器环境下共享缓存数据,提高系统的整体性能。
- 可以根据数据的特点选择合适的数据结构来存储数据,如使用哈希表存储对象、使用有序集合进行排行榜等操作。
- 配置 Redis 的持久化机制,以防止数据丢失。同时,考虑使用 Redis 的集群或主从复制来提高可用性和可扩展性。
Caffeine 作为本地缓存:
- Caffeine 是一个高效的本地缓存库,可以在应用程序内部实现缓存,减少对外部缓存服务的依赖,提高缓存的访问速度。
- Caffeine 支持自动过期功能,可以根据设定的时间自动清除过期的缓存数据,减少内存占用。
- 可以根据数据的访问频率和大小来调整 Caffeine 的缓存配置,如缓存的大小、过期时间等。
实现二级缓存架构
数据存储流程:
- 当应用程序需要访问数据时,首先从 Caffeine 本地缓存中查找数据。如果数据在 Caffeine 中存在,则直接返回数据,无需进一步访问 Redis 或数据库
- 如果数据不在 Caffeine 中,则从 Redis 分布式缓存中查找数据。如果数据在 Redis 中存在,则将数据加载到 Caffeine 中,并返回数据给应用程序。
- 如果数据不在 Redis 中,则从数据库中读取数据,并将数据同时存储到 Redis 和 Caffeine 中,然后返回数据给应用程序。
数据更新流程:
- 当数据在数据库中被更新时,需要同时更新 Redis 和 Caffeine 中的缓存数据,以保证数据的一致性
- 可以采用先更新数据库,然后删除 Redis 中的对应数据,让后续的访问从数据库中重新读取数据并更新到 Redis 和 Caffeine 中的方式来实现数据的更新。这种方式被称为 Cache Aside 模式。
缓存过期策略:
- 对于 Caffeine 本地缓存,可以设置自动过期时间,根据数据的变化频率和访问频率来调整过期时间,以避免内存占用过高。
- 对于 Redis 分布式缓存,可以根据业务需求设置合理的过期时间,或者采用主动更新的方式来保证缓存数据的有效性。
4 利用Caffeine+Redis解决Redis突然宕机导致的缓存雪崩问题
需求:修改根据id查询商铺的业务,基于二级缓存方式来解决缓存雪崩问题。
思路分析:当用户开始查询时,先查询本地缓存Caffeine,判断是否命中,如果没有命中则查询Redis,命中则直接返回。
4.1 pom.xml文件引入相关依赖
<!--引入本地缓存Caffine-->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
4.2 本地缓存配置类
Config目录下新建本地缓存配置类,LocalCacheConfiguration
package com.hmdp.config;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
/**
* 本地缓存Caffeine配置类
*/
@Configuration
public class LocalCacheConfiguration {
@Bean("localCacheManager")
public Cache<String, Object> localCacheManager() {
return Caffeine.newBuilder()
//写入或者更新5s后,缓存过期并失效, 实际项目中肯定不会那么短时间就过期,根据具体情况设置即可
.expireAfterWrite(120, TimeUnit.SECONDS)
// 初始的缓存空间大小
.initialCapacity(50)
// 缓存的最大条数,通过 Window TinyLfu算法控制整个缓存大小
.maximumSize(500)
//打开数据收集功能
.recordStats()
.build();
}
}
4.3 修改ShopServiceImpl中的queryById方法
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Resource
private Cache<String,Object> caffeineCache;
// @Cacheable(value = "shop",key = "#id")/
public Result queryById(Long id){
//1.从Caffeine中查询数据
Object o = caffeineCache.getIfPresent(CACHE_SHOP_KEY + id);
if(Objects.nonNull(o)){
log.info("从Caffeine中查询到数据...");
return Result.ok( o);
}
//缓存穿透
Shop shop = cacheClient.queryWithPassThrough(CACHE_SHOP_KEY,id,Shop.class,this::getById,CACHE_SHOP_TTL,TimeUnit.MINUTES);
if(shop != null){
log.info("从Redis中查到数据");
caffeineCache.put(CACHE_SHOP_KEY+id,shop);
}
if(shop == null){
return Result.fail("店铺不存在!");
}
//7.返回数据
return Result.ok(shop);
}
}
- caffeineCache.put(user.getId(), user):保存本地缓存;
- caffeineCache.invalidate(id):移除指定的本地缓存;
- caffeineCache.getIfPresent(id): 从本地缓存中获取值,如果缓存中不存指定的值,则方法将返回 null;
- caffeineCache.get(id, Function<>): 从本地缓存中获取值,该方法还支持将一个参数为 key 的 Function 作为参数传入。如果缓存中不存在该 key,则该函数将用于提供默认值,该值在计算后插入缓存中,如果缓存的元素无法生成或者在生成的过程中抛出异常而导致生成元素失败,则返回null。
5 测试
运行启动类,使用前后端联调来测试查询商铺信息功能。运行结果如下,首次查询Caffeine中没有数据,所以输出从Redis中查询,第二次查询相同店铺时,从Caffeine中查询。