SpringBoot Cache 实现二级缓存
mhr18 2024-12-06 16:05 21 浏览 0 评论
二级缓存介绍
- 二级缓存分为本地缓存和远程缓存,也可称为内存缓存和网络缓存
- 常见的流行缓存框架
- 本地缓存:Caffeine,Guava Cache
- 远程缓存:Redis,MemCache
- 二级缓存的访问流程
- 二级缓存的优势与问题
- 优势:二级缓存优先使用本地缓存,访问数据非常快,有效减少和远程缓存之间的数据交换,节约网络开销
- 问题:分布式环境下本地缓存存在一致性问题,本地缓存变更后需要通知其他节点刷新本地缓存,这对一致性要求高的场景可能不能很好的适应
SpringBoot Cache 组件
- SpringBoot Cache 组件提供了一套缓存管理的接口以及声明式使用的缓存的注解
- 引入 SpringBoot Cache
- xml复制代码
- <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency>
- 如何集成第三方缓存框架到 Cache 组件
- 实现 Cache 接口,适配第三方缓存框架的操作,实现 CacheManager 接口,提供缓存管理器的 Bean
- SpringBoot Cache 默认提供了 Caffeine、ehcache 等常见缓存框架的管理器,引入相关依赖后即可使用
- 引入 Caffeine
- xml复制代码
- <dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> </dependency>
- SpringBoot Redis 提供了 Redis 缓存的实现及管理器
- 引入 Redis 缓存、RedisTemplate
- xml复制代码
- <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
- SpringBoot Cache 声明式缓存注解
- @Cacheable:执行方法前,先从缓存中获取,没有获取到才执行方法,并将其结果更新到缓存
- @CachePut:执行方法后,将其结果更新到缓存
- @CacheEvict:执行方法后,清除缓存
- @Caching:组合前三个注解
- @Cacheable 注解的常用属性:
- vbnet复制代码
- cacheNames/value:缓存名称 key:缓存数据的 key,默认使用方法参数值,支持 SpEL keyGenerator:指定 key 的生成器,和 key 属性二选一 cacheManager:指定使用的缓存管理器。 condition:在方法执行开始前检查,在符合 condition 时,进行缓存操作 unless:在方法执行完成后检查,在符合 unless 时,不进行缓存操作 sync:是否使用同步模式,同步模式下,多个线程同时未命中一个 key 的数据,将阻塞竞争执行方法
- SpEL 支持的表达式
本地缓存 Caffeine
Caffeine 介绍
- Caffeine 是继 Guava Cache 之后,在 SpringBoot 2.x 中默认集成的缓存框架
- Caffeine 使用了 Window TinyLFU 淘汰策略,缓存命中率极佳,被称为现代高性能缓存库之王
- 创建一个 Caffeine Cache
- java复制代码
- Cache<String, Object> cache = Caffeine.newBuilder().build();
Caffeine 内存淘汰策略
- FIFO:先进先出,命中率低
- LRU:最近最久未使用,不能应对冷门突发流量,会导致热点数据被淘汰
- LFU:最近最少使用,需要维护使用频率,占用内存空间,
- W-TinyLFU:LFU 的变种,综合了 LRU LFU 的长处,高命中率,低内存占用
Caffeine 缓存失效策略
- 基于容量大小
- 根据最大容量
- java复制代码
- Cache<String, Object> cache = Caffeine.newBuilder() .maximumSize(10000) .build();
- 根据权重
- java复制代码
- Cache<String, Object> cache = Caffeine.newBuilder() .maximumWeight(10000) .weigher((Weigher<String, Object>) (s, o) -> { // 根据不同对象计算权重 return 0; }) .build();
- 基于引用类型
- 基于弱引用,当不存在强引用时淘汰
- java复制代码
- Cache<String, Object> cache = Caffeine.newBuilder() .weakKeys() .weakValues() .build();
- 基于软引用,当不存在强引用且内存不足时淘汰
- java复制代码
- Cache<String, Object> cache = Caffeine.newBuilder() .softValues() .build();
- 基于过期时间
- expireAfterWrite,写入后一定时间后过期
- java复制代码
- Cache<String, Object> cache = Caffeine.newBuilder() .expireAfterWrite(5, TimeUnit.SECONDS) .build();
- expireAfterAccess(long, TimeUnit),访问后一定时间后过期,一直访问则一直不过期
- expireAfter(Expiry),自定义时间的计算方式
Caffeine 线程池
- Caffeine 默认使用 ForkJoinPool.commonPool()
- Caffeine 线程池可通过 executor 方法设置
Caffeine 指标统计
- Caffeine 通过配置 recordStats 方法开启指标统计,通过缓存的 stats 方法获取信息
- Caffeine 指标统计的内容有:命中率,加载数据耗时,缓存数量相关等
Caffeine Cache 的种类
- 普通 Cache
- java复制代码
- Cache<String, Object> cache = Caffeine.newBuilder() .expireAfterWrite(5, TimeUnit.SECONDS) .build(); // 存入 cache.put("key1", "123"); // 取出 Object key1Obj = cache.getIfPresent("key1"); // 清除 cache.invalidate("key1"); // 清除全部 cache.invalidateAll();
- 异步 Cache
- 响应结果通过 CompletableFuture 包装,利用线程池异步执行
- java复制代码
- AsyncCache<String, Object> asyncCache = Caffeine.newBuilder() .expireAfterWrite(5, TimeUnit.SECONDS) .buildAsync(); // 存入 asyncCache.put("key1", CompletableFuture.supplyAsync(() -> "123")); // 取出 CompletableFuture<Object> key1Future = asyncCache.getIfPresent("key1"); try { Object key1Obj = key1Future.get(); } catch (InterruptedException | ExecutionException e) { // } // 清除 asyncCache.synchronous().invalidate("key1"); // 清除全部 asyncCache.synchronous().invalidateAll();
- Loading Cache
- 和普通缓存使用方式一致
- 在缓存未命中时,自动加载数据到缓存,需要设置加载数据的回调,比如从数据库查询数据
- java复制代码
- LoadingCache<String, Object> cache = Caffeine.newBuilder() .expireAfterWrite(5, TimeUnit.SECONDS) .build(key -> { // 获取业务数据 return "Data From DB"; });
- 异步 Loading Cache
- 和异步缓存使用方式一致
- 在缓存未命中时,自动加载数据到缓存,与 Loading Cache 不同的是,加载数据是异步的
- java复制代码
- // 使用 AsyncCache 的线程池异步加载 AsyncLoadingCache<String, Object> asyncCache0 = Caffeine.newBuilder() .expireAfterWrite(5, TimeUnit.SECONDS) .buildAsync(key -> { // 获取业务数据 return "Data From DB"; }); // 指定加载使用的线程池 AsyncLoadingCache<String, Object> asyncCache1 = Caffeine.newBuilder() .expireAfterWrite(5, TimeUnit.SECONDS) .buildAsync((key, executor) -> CompletableFuture.supplyAsync(() -> { // 异步获取业务数据 return "Data From DB"; }, otherExecutor));
- 注意:AsyncLoadingCache 不支持弱引用和软引用相关淘汰策略
Caffeine 自动刷新机制
- Caffeine 可通过 refreshAfterWrite 设置定时刷新
- 必须是指定了 CacheLoader 的缓存,即 LoadingCache 和 AsyncLoadingCache
- java复制代码
- LoadingCache<String, Object> cache = Caffeine.newBuilder() .expireAfterWrite(5, TimeUnit.SECONDS) .refreshAfterWrite(3, TimeUnit.SECONDS) .build(key -> { // 获取业务数据 return "Data From DB"; });
- refreshAfterWrite 是一种定时刷新,key 过期时并不一定会立即刷新
实现二级缓存
配置类 DLCacheProperties
java复制代码@Data
@ConfigurationProperties(prefix = "uni-boot.cache.dl")
public class DLCacheProperties {
/**
* 是否存储 null 值
*/
private boolean allowNullValues = true;
/**
* 过期时间,为 0 表示不过期,默认 30 分钟
* 单位:毫秒
*/
private long defaultExpiration = 30 * 60 * 1000;
/**
* 针对 cacheName 设置过期时间,为 0 表示不过期
* 单位:毫秒
*/
private Map<String, Long> cacheExpirationMap;
/**
* 本地缓存 caffeine 配置
*/
private LocalConfig local = new LocalConfig();
/**
* 远程缓存 redis 配置
*/
private RemoteConfig remote = new RemoteConfig();
@Data
public static class LocalConfig {
/**
* 初始化大小,为 0 表示默认
*/
private int initialCapacity;
/**
* 最大缓存个数,为 0 表示默认
* 默认最多 5 万条
*/
private long maximumSize = 10000L;
}
@Data
public static class RemoteConfig {
/**
* Redis pub/sub 缓存刷新通知主题
*/
private String syncTopic = "cache:dl:refresh:topic";
}
}
缓存实现 DLCache
本地缓存基于 Caffeine,远程缓存使用 Redis
实现 SpringBoot Cache 的抽象类,AbstractValueAdaptingCache
java复制代码@Slf4j
@Getter
public class DLCache extends AbstractValueAdaptingCache {
private final String name;
private final long expiration;
private final DLCacheProperties cacheProperties;
private final Cache<String, Object> caffeineCache;
private final RedisTemplate<String, Object> redisTemplate;
public DLCache(String name, long expiration, DLCacheProperties cacheProperties,
Cache<String, Object> caffeineCache, RedisTemplate<String, Object> redisTemplate) {
super(cacheProperties.isAllowNullValues());
this.name = name;
this.expiration = expiration;
this.cacheProperties = cacheProperties;
this.caffeineCache = caffeineCache;
this.redisTemplate = redisTemplate;
}
@Override
public String getName() {
return name;
}
@Override
public Object getNativeCache() {
return this;
}
@Override
protected Object lookup(Object key) {
String redisKey = getRedisKey(key);
Object val;
val = caffeineCache.getIfPresent(key);
// val 是 toStoreValue 包装过的值,为 null 则 key 不存在
// 因为存储的 null 值被包装成了 DLCacheNullVal.INSTANCE
if (ObjectUtil.isNotNull(val)) {
log.debug("DLCache local get cache, key:{}, value:{}", key, val);
return val;
}
val = redisTemplate.opsForValue().get(redisKey);
if (ObjectUtil.isNotNull(val)) {
log.debug("DLCache remote get cache, key:{}, value:{}", key, val);
caffeineCache.put(key.toString(), val);
return val;
}
return val;
}
@SuppressWarnings("unchecked")
@Override
public <T> T get(Object key, Callable<T> valueLoader) {
T val;
val = (T) lookup(key);
if (ObjectUtil.isNotNull(val)) {
return val;
}
// 双检锁
synchronized (key.toString().intern()) {
val = (T) lookup(key);
if (ObjectUtil.isNotNull(val)) {
return val;
}
try {
// 拦截的业务方法
val = valueLoader.call();
// 加入缓存
put(key, val);
} catch (Exception e) {
throw new DLCacheException("DLCache valueLoader fail", e);
}
return val;
}
}
@Override
public void put(Object key, Object value) {
putRemote(key, value);
sendSyncMsg(key);
putLocal(key, value);
}
@Override
public void evict(Object key) {
// 先清理 redis 再清理 caffeine
clearRemote(key);
sendSyncMsg(key);
clearLocal(key);
}
@Override
public void clear() {
// 先清理 redis 再清理 caffeine
clearRemote(null);
sendSyncMsg(null);
clearLocal(null);
}
private void sendSyncMsg(Object key) {
String syncTopic = cacheProperties.getRemote().getSyncTopic();
DLCacheRefreshMsg refreshMsg = new DLCacheRefreshMsg(name, key);
// 加入 SELF_MSG_MAP 防止自身节点重复处理
DLCacheRefreshListener.SELF_MSG_MAP.add(refreshMsg);
redisTemplate.convertAndSend(syncTopic, refreshMsg);
}
private void putLocal(Object key, Object value) {
// toStoreValue 包装 null 值
caffeineCache.put(key.toString(), toStoreValue(value));
}
private void putRemote(Object key, Object value) {
if (expiration > 0) {
// toStoreValue 包装 null 值
redisTemplate.opsForValue().set(getRedisKey(key), toStoreValue(value), expiration, TimeUnit.MILLISECONDS);
return;
}
redisTemplate.opsForValue().set(getRedisKey(key), toStoreValue(value));
}
public void clearRemote(Object key) {
if (ObjectUtil.isNull(key)) {
Set<String> keys = redisTemplate.keys(getRedisKey("*"));
if (ObjectUtil.isNotEmpty(keys)) {
keys.forEach(redisTemplate::delete);
}
return;
}
redisTemplate.delete(getRedisKey(key));
}
public void clearLocal(Object key) {
if (ObjectUtil.isNull(key)) {
caffeineCache.invalidateAll();
return;
}
caffeineCache.invalidate(key);
}
/**
* 检查是否允许缓存 null
*
* @param value 缓存值
* @return 不为空则 true,为空但允许则 false,否则异常
*/
private boolean checkValNotNull(Object value) {
if (ObjectUtil.isNotNull(value)) {
return true;
}
if (isAllowNullValues() && ObjectUtil.isNull(value)) {
return false;
}
// val 不能为空,但传了空
throw new DLCacheException("Check null val is not allowed");
}
@Override
protected Object fromStoreValue(Object storeValue) {
if (isAllowNullValues() && DLCacheNullVal.INSTANCE.equals(storeValue)) {
return null;
}
return storeValue;
}
@Override
protected Object toStoreValue(Object userValue) {
if (!checkValNotNull(userValue)) {
return DLCacheNullVal.INSTANCE;
}
return userValue;
}
/**
* 获取 redis 完整 key
*/
private String getRedisKey(Object key) {
// 双冒号,与 spring cache 默认一致
return this.name.concat("::").concat(key.toString());
}
/**
* 在缓存时代替 null 值,以区分是 key 不存在还是 val 为 null
*/
@Data
public static class DLCacheNullVal {
public static final DLCacheNullVal INSTANCE = new DLCacheNullVal();
private String desc = "nullVal";
}
}
注意:需要区分缓存 get 到 null 值和 key 不存在,因此使用了 DLCacheNullVal 来代替 null 值
缓存管理器 DLCacheManager
缓存管理器
实现 SpringBoot Cache 的 CacheManager 接口
java复制代码@Slf4j
@RequiredArgsConstructor
public class DLCacheManager implements CacheManager {
private final ConcurrentHashMap<String, DLCache> cacheMap = new ConcurrentHashMap<>();
private final DLCacheProperties cacheProperties;
private final RedisTemplate<String, Object> redisTemplate;
@Override
public DLCache getCache(String name) {
return cacheMap.computeIfAbsent(name, (o) -> {
DLCache dlCache = buildCache(o);
log.debug("Create DLCache instance, name:{}", o);
return dlCache;
});
}
private DLCache buildCache(String name) {
Caffeine<Object, Object> caffeine = Caffeine.newBuilder();
// 设置过期时间 expireAfterWrite
long expiration = 0;
// 获取针对 cache name 设置的过期时间
Map<String, Long> cacheExpirationMap = cacheProperties.getCacheExpirationMap();
if (ObjectUtil.isNotEmpty(cacheExpirationMap) && cacheExpirationMap.get(name) > 0) {
expiration = cacheExpirationMap.get(name);
} else if (cacheProperties.getDefaultExpiration() > 0) {
expiration = cacheProperties.getDefaultExpiration();
}
if (expiration > 0) {
caffeine.expireAfterWrite(expiration, TimeUnit.MILLISECONDS);
}
// 设置参数
LocalConfig localConfig = cacheProperties.getLocal();
if (ObjectUtil.isNotNull(localConfig.getInitialCapacity()) && localConfig.getInitialCapacity() > 0) {
caffeine.initialCapacity(localConfig.getInitialCapacity());
}
if (ObjectUtil.isNotNull(localConfig.getMaximumSize()) && localConfig.getMaximumSize() > 0) {
caffeine.maximumSize(localConfig.getMaximumSize());
}
return new DLCache(name, expiration, cacheProperties, caffeine.build(), redisTemplate);
}
@Override
public Collection<String> getCacheNames() {
return Collections.unmodifiableSet(cacheMap.keySet());
}
}
缓存刷新监听器
缓存消息
java复制代码@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class DLCacheRefreshMsg {
private String cacheName;
private Object key;
}
缓存刷新消息监听
java复制代码@Slf4j
@RequiredArgsConstructor
@Component
public class DLCacheRefreshListener implements MessageListener, InitializingBean {
public static final ConcurrentHashSet<DLCacheRefreshMsg> SELF_MSG_MAP = new ConcurrentHashSet<>();
private final DLCacheManager dlCacheManager;
private final DLCacheProperties cacheProperties;
private final RedisMessageListenerContainer listenerContainer;
@Override
public void onMessage(Message message, byte[] pattern) {
// 序列化出刷新消息
DLCacheRefreshMsg refreshMsg = (DLCacheRefreshMsg) RedisUtil.getTemplate().getValueSerializer().deserialize(message.getBody());
if (ObjectUtil.isNull(refreshMsg)) {
return;
}
// 判断是不是自身节点发出
if (SELF_MSG_MAP.contains(refreshMsg)) {
SELF_MSG_MAP.remove(refreshMsg);
return;
}
log.debug("DLCache refresh local, cache name:{}, key:{}", refreshMsg.getCacheName(), refreshMsg.getKey());
// 清理本地缓存
dlCacheManager.getCache(refreshMsg.getCacheName()).clearLocal(refreshMsg.getKey());
}
@Override
public void afterPropertiesSet() {
// 注册到 RedisMessageListenerContainer
listenerContainer.addMessageListener(this, new ChannelTopic(cacheProperties.getRemote().getSyncTopic()));
}
}
使用二级缓存
注入 DLCacheManager
java复制代码@Bean(name = "dlCacheManager")
public DLCacheManager dlCacheManager(DLCacheProperties cacheProperties, RedisTemplate<String, Object> redisTemplate) {
return new DLCacheManager(cacheProperties, redisTemplate);
}
使用 @Cacheable 配合 DLCacheManager
java复制代码@ApiOperation("测试 @Cacheable")
@Cacheable(cacheNames = "test", key = "'dl'", cacheManager = "dlCacheManager")
@PostMapping("test_cacheable")
public String testCacheable() {
log.info("testCacheable 执行");
return "Cacheable";
}
@ApiOperation("测试 @Cacheable null 值")
@Cacheable(cacheNames = "test", key = "'dl'", cacheManager = "dlCacheManager")
@PostMapping("test_cacheable_null")
public String testCacheableNull() {
log.info("testCacheableNull 执行");
return null;
}
@ApiOperation("测试 @CachePut")
@CachePut(cacheNames = "test", key = "'dl'", cacheManager = "dlCacheManager")
@PostMapping("test_put")
public String testPut() {
return "Put";
}
@ApiOperation("测试 @CacheEvict")
@CacheEvict(cacheNames = "test", key = "'dl'", cacheManager = "dlCacheManager")
@PostMapping("test_evict")
public String testEvict() {
return "Evict";
}
相关推荐
- 【推荐】一个开源免费、AI 驱动的智能数据管理系统,支持多数据库
-
如果您对源码&技术感兴趣,请点赞+收藏+转发+关注,大家的支持是我分享最大的动力!!!.前言在当今数据驱动的时代,高效、智能地管理数据已成为企业和个人不可或缺的能力。为了满足这一需求,我们推出了这款开...
- Pure Storage推出统一数据管理云平台及新闪存阵列
-
PureStorage公司今日推出企业数据云(EnterpriseDataCloud),称其为组织在混合环境中存储、管理和使用数据方式的全面架构升级。该公司表示,EDC使组织能够在本地、云端和混...
- 对Java学习的10条建议(对java课程的建议)
-
不少Java的初学者一开始都是信心满满准备迎接挑战,但是经过一段时间的学习之后,多少都会碰到各种挫败,以下北风网就总结一些对于初学者非常有用的建议,希望能够给他们解决现实中的问题。Java编程的准备:...
- SQLShift 重大更新:Oracle→PostgreSQL 存储过程转换功能上线!
-
官网:https://sqlshift.cn/6月,SQLShift迎来重大版本更新!作为国内首个支持Oracle->OceanBase存储过程智能转换的工具,SQLShift在过去一...
- JDK21有没有什么稳定、简单又强势的特性?
-
佳未阿里云开发者2025年03月05日08:30浙江阿里妹导读这篇文章主要介绍了Java虚拟线程的发展及其在AJDK中的实现和优化。阅前声明:本文介绍的内容基于AJDK21.0.5[1]以及以上...
- 「松勤软件测试」网站总出现404 bug?总结8个原因,不信解决不了
-
在进行网站测试的时候,有没有碰到过网站崩溃,打不开,出现404错误等各种现象,如果你碰到了,那么恭喜你,你的网站出问题了,是什么原因导致网站出问题呢,根据松勤软件测试的总结如下:01数据库中的表空间不...
- Java面试题及答案最全总结(2025版)
-
大家好,我是Java面试陪考员最近很多小伙伴在忙着找工作,给大家整理了一份非常全面的Java面试题及答案。涉及的内容非常全面,包含:Spring、MySQL、JVM、Redis、Linux、Sprin...
- 数据库日常运维工作内容(数据库日常运维 工作内容)
-
#数据库日常运维工作包括哪些内容?#数据库日常运维工作是一个涵盖多个层面的综合性任务,以下是详细的分类和内容说明:一、数据库运维核心工作监控与告警性能监控:实时监控CPU、内存、I/O、连接数、锁等待...
- 分布式之系统底层原理(上)(底层分布式技术)
-
作者:allanpan,腾讯IEG高级后台工程师导言分布式事务是分布式系统必不可少的组成部分,基本上只要实现一个分布式系统就逃不开对分布式事务的支持。本文从分布式事务这个概念切入,尝试对分布式事务...
- oracle 死锁了怎么办?kill 进程 直接上干货
-
1、查看死锁是否存在selectusername,lockwait,status,machine,programfromv$sessionwheresidin(selectsession...
- SpringBoot 各种分页查询方式详解(全网最全)
-
一、分页查询基础概念与原理1.1什么是分页查询分页查询是指将大量数据分割成多个小块(页)进行展示的技术,它是现代Web应用中必不可少的功能。想象一下你去图书馆找书,如果所有书都堆在一张桌子上,你很难...
- 《战场兄弟》全事件攻略 一般事件合同事件红装及隐藏职业攻略
-
《战场兄弟》全事件攻略,一般事件合同事件红装及隐藏职业攻略。《战场兄弟》事件奖励,事件条件。《战场兄弟》是OverhypeStudios制作发行的一款由xcom和桌游为灵感来源,以中世纪、低魔奇幻为...
- LoadRunner(loadrunner录制不到脚本)
-
一、核心组件与工作流程LoadRunner性能测试工具-并发测试-正版软件下载-使用教程-价格-官方代理商的架构围绕三大核心组件构建,形成完整测试闭环:VirtualUserGenerator(...
- Redis数据类型介绍(redis 数据类型)
-
介绍Redis支持五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)及Zset(sortedset:有序集合)。1、字符串类型概述1.1、数据类型Redis支持...
- RMAN备份监控及优化总结(rman备份原理)
-
今天主要介绍一下如何对RMAN备份监控及优化,这里就不讲rman备份的一些原理了,仅供参考。一、监控RMAN备份1、确定备份源与备份设备的最大速度从磁盘读的速度和磁带写的带度、备份的速度不可能超出这两...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- oracle位图索引 (63)
- oracle批量插入数据 (62)
- oracle事务隔离级别 (53)
- oracle 空为0 (50)
- oracle主从同步 (55)
- oracle 乐观锁 (51)
- redis 命令 (78)
- php redis (88)
- redis 存储 (66)
- redis 锁 (69)
- 启动 redis (66)
- redis 时间 (56)
- redis 删除 (67)
- redis内存 (57)
- redis并发 (52)
- redis 主从 (69)
- redis 订阅 (51)
- redis 登录 (54)
- redis 面试 (58)
- 阿里 redis (59)
- redis 搭建 (53)
- redis的缓存 (55)
- lua redis (58)
- redis 连接池 (61)
- redis 限流 (51)