# multi level cache **Repository Path**: bjfengwen/multi-level-cache ## Basic Information - **Project Name**: multi level cache - **Description**: No description available - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-03-18 - **Last Updated**: 2026-03-20 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Multi-Level Cache ## 项目简介 这是一个基于 Spring Boot 2.7 的多级缓存示例项目,核心目标是: - 用 **本地热点缓存** 提升高频 Key 的读取性能 - 用 **远端缓存** 作为统一的数据来源 - 用 **热点探测** 动态决定哪些 Key 应该进入本地缓存 - 用 **失效广播** 保证写操作后各节点本地缓存尽快失效 - 通过 **指标与接口** 观察缓存命中、热点情况和失效次数 当前项目已经支持: - 本地热点缓存 - 基于滑动窗口的热点探测 - Redis 远端缓存 - Redis Pub/Sub 失效广播 - In-Memory 模式回退 - Controller/API 访问与指标观测 - 单元测试、Redis 集成测试、Controller 联调测试 --- ## 1. 业务目标 项目解决的是一个典型问题: > 远端缓存(例如 Redis)适合集中存储,但所有请求都直接打到远端,热点 Key 会带来额外网络开销; > 如果把所有 Key 都放到本地缓存,又会带来容量、过期和一致性问题。 因此这里采用了 **多级缓存设计**: - **第一级缓存**:本地热点缓存 `LocalHotCache` - 只缓存“热点 Key” - 容量有限、带 TTL - 命中后性能最好 - **第二级缓存**:远端缓存 `RemoteCacheStore` - 当前默认实现为 Redis - 作为统一回源位置 - **失效机制**:`InvalidationBroadcaster` - 写操作后广播失效消息 - 各节点收到消息后清理本地热点缓存 这样可以同时兼顾: - 读性能 - 一致性 - 可扩展性 - 实现复杂度 --- ## 2. 整体架构 ### 2.1 模块划分 项目核心模块如下: - 启动入口 - `src/main/java/com/example/multilevelcache/MultiLevelCacheApplication.java` - 统一缓存服务 - `src/main/java/com/example/multilevelcache/cache/MultiLevelCacheService.java` - 本地热点缓存 - `src/main/java/com/example/multilevelcache/cache/LocalHotCache.java` - 远端缓存抽象与实现 - `src/main/java/com/example/multilevelcache/cache/RemoteCacheStore.java` - `src/main/java/com/example/multilevelcache/cache/RedisRemoteCacheStore.java` - `src/main/java/com/example/multilevelcache/cache/InMemoryRemoteCacheStore.java` - 失效广播抽象与实现 - `src/main/java/com/example/multilevelcache/cache/InvalidationBroadcaster.java` - `src/main/java/com/example/multilevelcache/cache/RedisInvalidationBroadcaster.java` - `src/main/java/com/example/multilevelcache/cache/InMemoryInvalidationBroadcaster.java` - 热点探测 - `src/main/java/com/example/multilevelcache/detection/HotKeyDetector.java` - 指标统计 - `src/main/java/com/example/multilevelcache/metrics/CacheMetrics.java` - Web 接口 - `src/main/java/com/example/multilevelcache/endpoint/CacheController.java` - 配置 - `src/main/java/com/example/multilevelcache/config/MultiLevelCacheProperties.java` - `src/main/java/com/example/multilevelcache/config/MultiLevelCacheConfiguration.java` ### 2.2 架构关系 ```text Client | v CacheController | v MultiLevelCacheService |--- LocalHotCache |--- RemoteCacheStore | |--- RedisRemoteCacheStore | \--- InMemoryRemoteCacheStore |--- HotKeyDetector |--- CacheMetrics \--- InvalidationBroadcaster |--- RedisInvalidationBroadcaster \--- InMemoryInvalidationBroadcaster ``` ### 2.3 设计原则 这个项目的设计明显遵循了几个原则: 1. **统一入口** - 所有缓存读写统一走 `MultiLevelCacheService` - Controller 不直接操作 Redis 或本地缓存 2. **抽象隔离实现** - 远端缓存通过 `RemoteCacheStore` 抽象 - 失效广播通过 `InvalidationBroadcaster` 抽象 - 这样便于 Redis / 内存实现切换 3. **热点缓存与全量缓存分离** - 本地缓存不是缓存全部数据,而只缓存热点数据 - 避免本地缓存无限膨胀 4. **一致性优先于极致命中率** - 写操作后先删本地,再广播失效 - 保证节点间尽快收敛 5. **配置驱动装配** - 通过 `@ConditionalOnProperty` 控制 Redis / In-Memory 实现 --- ## 3. 业务流程梳理 ## 3.1 读流程 入口:`GET /cache/{key}` -> `CacheController.get()` -> `MultiLevelCacheService.get()` 对应代码: - `src/main/java/com/example/multilevelcache/endpoint/CacheController.java` - `src/main/java/com/example/multilevelcache/cache/MultiLevelCacheService.java` ### 读流程步骤 1. 记录一次请求次数 2. 记录该 Key 的一次访问事件,供热点探测使用 3. 优先查询本地热点缓存 `LocalHotCache` 4. 如果本地命中: - 返回本地值 - 记录本地命中次数 5. 如果本地未命中: - 从远端缓存 `RemoteCacheStore` 查询 - 若当前 Key 已经是热点,则通过 `putIfHot()` 放入本地缓存 - 返回远端值 ### 流程图 ```text 请求读取 key | v 记录 totalRequests | v 记录访问事件到 HotKeyDetector | v 查 LocalHotCache |---- 命中 ----> recordLocalHit -> 返回 | \---- 未命中 --> 查 RemoteCacheStore | v 如果 key 已是热点 | v 放入 LocalHotCache | v 返回 ``` ### 关键特点 - 热点判断不是在单次请求中立刻完成,而是由统计结果决定 - `refreshHotKeys()` 只是刷新“哪些 key 是热点” - 真正把值放入本地缓存,是在后续读请求触发 `putIfHot()` 时完成 --- ## 3.2 热点识别流程 入口:`MultiLevelCacheService.refreshHotKeys()` 对应代码: - `src/main/java/com/example/multilevelcache/cache/MultiLevelCacheService.java` - `src/main/java/com/example/multilevelcache/detection/HotKeyDetector.java` ### 流程说明 1. 每次 `get(key)` 都会向 `HotKeyDetector` 上报一条访问事件 2. `HotKeyDetector` 使用滑动窗口计数器统计每个 Key 的热度 3. 定时任务按 `bucketDuration` 周期执行 `refreshHotKeys()` 4. 根据阈值 `threshold`、窗口大小、TopN 筛选热点 Key 5. 将热点 Key 集合写入: - `LocalHotCache.replaceHotKeys()` - `CacheMetrics.replaceHotKeys()` ### 为什么这样设计 因为热点是一个**时间窗口内的动态概念**: - 不能因为一次请求就认定是热点 - 也不能永远保留历史热点 - 使用滑动窗口可以兼顾实时性与平滑性 ### 当前热点探测相关配置 见 `application.yml`: ```yaml multi-level-cache: detection: bucket-count: 10 bucket-duration: PT3S top-n: 20 threshold: 5 ``` 含义: - `bucket-count`:窗口桶数量 - `bucket-duration`:每个桶的时间长度 - `top-n`:最多保留多少个热点 Key - `threshold`:热度阈值,达到才算热点 --- ## 3.3 写流程 包括: - `POST /cache/{key}` -> set - `DELETE /cache/{key}` -> delete - `POST /cache/{key}/expire` -> expire 对应代码: - `src/main/java/com/example/multilevelcache/endpoint/CacheController.java` - `src/main/java/com/example/multilevelcache/cache/MultiLevelCacheService.java` ### 写流程步骤 以 `set(key, value)` 为例: 1. 先写远端缓存 `RemoteCacheStore.set()` 2. 调用 `invalidateEverywhere(key)` 3. 当前节点本地缓存删除该 key 4. 当前节点记录一次 invalidation 指标 5. 广播失效消息给其它节点 `delete()` 和 `expire()` 也是同样模式。 ### 流程图 ```text 写入/删除/过期 key | v 操作 RemoteCacheStore | v invalidateEverywhere(key) |--- 删除当前节点 LocalHotCache 中的 key |--- metrics.invalidations +1 \--- InvalidationBroadcaster.publish(key, nodeId) ``` ### 设计取舍 这里采用的是: - **远端先更新** - **本地再失效** - **最后广播** 这样做的目标是让远端缓存尽快成为最新状态,本地缓存只作为热点副本。 --- ## 3.4 分布式失效流程 对应代码: - `src/main/java/com/example/multilevelcache/cache/RedisInvalidationBroadcaster.java` - `src/main/java/com/example/multilevelcache/cache/MultiLevelCacheService.java` - `src/main/java/com/example/multilevelcache/config/MultiLevelCacheConfiguration.java` ### 流程说明 当某个节点发生写操作时: 1. 节点调用 `InvalidationBroadcaster.publish(key, sourceNode)` 2. 如果是 Redis 实现,则向 Redis Pub/Sub channel 发送消息 3. 其它节点监听该 channel 4. 收到消息后调用 `onInvalidate(key, sourceNode)` 5. 节点删除自己本地热点缓存中的该 key 6. 记录一次失效指标 ### 消息格式 当前 Redis 广播消息体格式是: ```text key|sourceNode ``` 例如: ```text user:1|node-2 ``` ### 为什么要带 `sourceNode` 因为当前节点自己也会收到广播消息。为了避免重复失效,`MultiLevelCacheService.onInvalidate()` 中有如下逻辑: - 如果 `sourceNode == 当前节点 nodeId` - 则忽略这条消息 这样可以避免: - 当前节点本地失效被重复执行 - invalidation 指标重复累加 ### Redis 监听容器 `MultiLevelCacheConfiguration` 中注册了 `RedisMessageListenerContainer`,它负责: - 连接 Redis - 订阅指定频道 - 把收到的消息转发给 `RedisInvalidationBroadcaster` --- ## 4. 核心组件说明 ## 4.1 MultiLevelCacheService 文件:`src/main/java/com/example/multilevelcache/cache/MultiLevelCacheService.java` 这是整个系统的核心协调者,职责包括: - 统一处理读写请求 - 串联本地缓存、远端缓存、热点探测、失效广播、指标统计 - 接收失效回调并清理本地缓存 - 定时刷新热点 Key 集 可以把它理解为整个项目的“缓存编排层”。 --- ## 4.2 LocalHotCache 文件:`src/main/java/com/example/multilevelcache/cache/LocalHotCache.java` 职责: - 保存热点 Key 的本地副本 - 通过 TTL 控制本地缓存生命周期 - 通过 `maxEntries` 控制容量上限 - 只允许热点 Key 写入 ### 关键实现点 - 使用 `LinkedHashMap` 实现近似 LRU 淘汰 - 每个 Entry 带过期时间 `expiresAt` - `putIfHot()` 只有在 key 已经属于热点集合时才写入 - `replaceHotKeys()` 会删除不再属于热点集合的旧数据 ### 设计意义 本地缓存只存热点数据,可以避免: - 本地内存被全量缓存占满 - 冷数据污染本地缓存 - 写扩散成本过高 --- ## 4.3 RemoteCacheStore 文件:`src/main/java/com/example/multilevelcache/cache/RemoteCacheStore.java` 这是远端缓存抽象,定义了四个核心操作: - `get` - `set` - `delete` - `expire` ### Redis 实现 文件:`src/main/java/com/example/multilevelcache/cache/RedisRemoteCacheStore.java` 实现特点: - 使用 `StringRedisTemplate` - 当前 `CacheValue` 只封装字符串,因此 Redis 中直接存 String value - 通过 `redis-key-prefix` 做业务前缀隔离 - 当前 `expire(key)` 语义等价于删除 key ### In-Memory 实现 文件:`src/main/java/com/example/multilevelcache/cache/InMemoryRemoteCacheStore.java` 用途: - 本地快速测试 - 无 Redis 环境下可切回内存模式 --- ## 4.4 InvalidationBroadcaster 文件:`src/main/java/com/example/multilevelcache/cache/InvalidationBroadcaster.java` 这是失效广播抽象,定义两个能力: - `publish(key, sourceNode)`:发布失效消息 - `register(listener)`:注册监听器 ### In-Memory 实现 文件:`src/main/java/com/example/multilevelcache/cache/InMemoryInvalidationBroadcaster.java` 特点: - 在单 JVM 内部直接回调 listener - 适合单元测试和本地简单运行 - 默认配置下就是这个实现 ### Redis 实现 文件:`src/main/java/com/example/multilevelcache/cache/RedisInvalidationBroadcaster.java` 特点: - 借助 Redis Pub/Sub 做跨节点广播 - 兼具 `publish` 和 `subscribe` 能力 - 通过 `MessageListener` 接收 Redis 消息 --- ## 4.5 HotKeyDetector 文件:`src/main/java/com/example/multilevelcache/detection/HotKeyDetector.java` 职责: - 记录 key 的访问事件 - 基于滑动窗口计算热度 - 识别热点 Key 列表 - 提供热点明细与 bucket 视图 ### 当前算法特征 - 维度:按 key 聚合 - 统计:滑动窗口累计访问权重 - 筛选:阈值过滤 + 热度排序 + TopN 截断 ### 优点 - 实现简单 - 适合演示热点识别思路 - 参数清晰,便于调优 ### 局限 - 统计保存在应用内存中,不跨节点共享 - 当前没有针对超大 key 数量做清理策略 --- ## 4.6 CacheMetrics 文件:`src/main/java/com/example/multilevelcache/metrics/CacheMetrics.java` 职责: - 统计总请求数 `totalRequests` - 统计本地命中数 `localHits` - 统计失效次数 `invalidations` - 保存当前热点 Key 集合 这些指标通过 `/cache/metrics` 暴露给外部。 --- ## 4.7 CacheController 文件:`src/main/java/com/example/multilevelcache/endpoint/CacheController.java` 提供对外接口: - `GET /cache/{key}`:读取缓存 - `POST /cache/{key}`:写入缓存 - `DELETE /cache/{key}`:删除缓存 - `POST /cache/{key}/expire`:主动过期 - `GET /cache/metrics`:查看缓存指标 - `GET /cache/hot-keys`:查看热点 Key 列表 它本身很薄,只负责: - 接收 HTTP 请求 - 调用 `MultiLevelCacheService` - 返回基础结构化响应 这是比较清晰的分层方式。 --- ## 5. 配置设计 配置文件:`src/main/resources/application.yml` 当前默认配置如下: ```yaml spring: application: name: multi-level-cache redis: host: localhost port: 6379 timeout: 2s multi-level-cache: node-id: node-1 local: max-entries: 1000 ttl: PT30S detection: bucket-count: 10 bucket-duration: PT3S top-n: 20 threshold: 5 remote-store: type: redis redis-key-prefix: 'multi-level-cache::data::' invalidation: type: in-memory redis-channel: 'multi-level-cache::invalidation' ``` ## 5.1 配置分类 ### Spring 原生配置 - `spring.redis.*` - 负责 Redis 连接 ### 业务配置 - `multi-level-cache.node-id` - 当前节点标识 - `multi-level-cache.local.*` - 本地热点缓存容量和 TTL - `multi-level-cache.detection.*` - 热点探测参数 - `multi-level-cache.remote-store.*` - 远端缓存实现类型与 Redis key 前缀 - `multi-level-cache.invalidation.*` - 失效广播实现类型与 Redis channel ## 5.2 条件装配策略 项目通过 `@ConditionalOnProperty` 控制具体实现装配: ### 远端缓存 - `multi-level-cache.remote-store.type=redis` - 启用 `RedisRemoteCacheStore` - `multi-level-cache.remote-store.type=in-memory` - 启用 `InMemoryRemoteCacheStore` ### 失效广播 - `multi-level-cache.invalidation.type=redis` - 启用 `RedisInvalidationBroadcaster` - `multi-level-cache.invalidation.type=in-memory` - 启用 `InMemoryInvalidationBroadcaster` ### 当前默认策略 - 远端缓存默认:**Redis** - 失效广播默认:**In-Memory** 这意味着默认情况下: - 数据存 Redis - 失效广播仍是单机内存广播 如果要开启完整分布式广播,需要把: ```yaml multi-level-cache: invalidation: type: redis ``` --- ## 6. 设计方案说明 ## 6.1 为什么是“热点本地缓存 + 远端 Redis” 这是一个典型的折中方案。 ### 如果只有 Redis 优点: - 数据集中 - 一致性更容易控制 缺点: - 所有请求都要走网络 - 高频热点 Key 压力集中到 Redis ### 如果全量本地缓存 优点: - 读取极快 缺点: - 容量不可控 - 一致性困难 - 多节点同步成本高 ### 当前方案 只把热点 Key 放本地,其他 key 仍走 Redis: 优点: - 兼顾性能和一致性 - 本地内存可控 - 实现复杂度适中 --- ## 6.2 为什么要保留抽象层 项目没有让业务直接依赖 Redis API,而是通过: - `RemoteCacheStore` - `InvalidationBroadcaster` 做了一层抽象。 这样带来的好处: 1. **便于测试** - 单元测试可直接用内存实现 2. **便于演进** - 后续远端存储可以替换为别的实现 3. **便于分环境配置** - 开发、测试、生产可以切不同模式 4. **控制改动范围** - 核心业务逻辑集中在 `MultiLevelCacheService` --- ## 6.3 为什么写操作后选择“失效”而不是“更新所有本地缓存” 因为本地缓存是多节点分散的,直接推送新值到所有节点会更复杂: - 需要处理消息乱序 - 需要处理版本冲突 - 需要保证所有节点都能可靠更新 相比之下,**广播失效** 更简单稳妥: - 各节点收到消息后只删本地副本 - 下一次读取再从 Redis 获取最新值 - 实现成本低,行为更容易解释 这是缓存系统里非常常见的设计。 --- ## 6.4 为什么热点集合和热点数据分开管理 项目中这两件事是分离的: - `replaceHotKeys()`:更新热点 key 名单 - `putIfHot()`:在读到远端值后,决定是否把值放入本地缓存 好处: - 热点判定与数据加载解耦 - 避免在刷新热点时批量回源 Redis - 只有真正发生读取时才把值放到本地 这是一种更轻量的按需缓存方式。 --- ## 7. 对外接口说明 ## 7.1 获取缓存值 ```http GET /cache/{key} ``` 响应示例: ```json { "key": "user:1", "value": "tom", "present": true } ``` --- ## 7.2 写入缓存值 ```http POST /cache/{key} Content-Type: application/json { "value": "tom" } ``` --- ## 7.3 删除缓存值 ```http DELETE /cache/{key} ``` --- ## 7.4 主动过期 ```http POST /cache/{key}/expire ``` 当前实现里 `expire` 语义等同于删除远端 key 并触发失效广播。 --- ## 7.5 查看指标 ```http GET /cache/metrics ``` 返回字段包括: - `totalRequests` - `localHits` - `hitRate` - `invalidations` - `hotKeyCount` - `hotKeys` --- ## 7.6 查看热点 Key ```http GET /cache/hot-keys ``` 返回示例: ```json [ { "key": "user:1", "heat": 12 } ] ``` --- ## 8. 测试方案 当前项目测试已经覆盖了几层: ## 8.1 单元测试 - `LocalHotCacheTest` - `HotKeyDetectorTest` - `MultiLevelCacheServiceTest` - `RedisRemoteCacheStoreTest` - `RedisInvalidationBroadcasterTest` 特点: - 快 - 无外部依赖或只做 mock - 验证核心逻辑边界 ## 8.2 Redis 集成测试 - `RedisRemoteCacheStoreIntegrationTest` - `RedisInvalidationBroadcasterIntegrationTest` - `MultiLevelCacheServiceRedisIntegrationTest` 特点: - 连接真实 Redis - 验证远端缓存读写、Pub/Sub 广播、服务级链路 ## 8.3 Controller 集成测试 - `CacheControllerIntegrationTest` 特点: - 启动 Web 环境 - 从 HTTP 接口层验证缓存行为、指标和热点输出 --- ## 9. 当前实现现状总结 ### 已实现 - 多级缓存主流程 - 热点探测 - 本地热点缓存 - Redis 远端缓存 - Redis Pub/Sub 失效广播 - 条件化装配 - 业务接口与指标接口 - Redis 真实集成测试 ### 当前默认运行方式 - 远端缓存:Redis - 失效广播:In-Memory ### 若要开启完整分布式模式 建议配置: ```yaml multi-level-cache: remote-store: type: redis invalidation: type: redis ``` 并保证 Redis 可连接。 --- ## 10. 后续可演进方向 基于当前结构,后续比较容易扩展的方向包括: 1. **真正的远端 TTL 过期语义** - 当前 `expire()` 实际是 delete,可扩展成 Redis EXPIRE 2. **更强的分布式一致性策略** - 增加版本号、消息幂等、延迟容忍设计 3. **热点统计清理机制** - 对长期不访问的 Key 清理计数器 4. **更丰富的缓存值模型** - 当前 `CacheValue` 只支持字符串,可扩展为 JSON / 泛型对象 5. **监控集成** - 接入 Micrometer / Prometheus / Grafana 6. **多节点端到端测试** - 增加双节点 Spring Context 的分布式测试 --- ## 11. 快速启动 ### 1)准备 Redis 确保本地 Redis 可用,例如: - host: `localhost` - port: `6379` ### 2)启动应用 ```bash mvn spring-boot:run ``` ### 3)调用接口示例 写入: ```bash curl -X POST http://localhost:8080/cache/user:1 -H "Content-Type: application/json" -d '{"value":"tom"}' ``` 读取: ```bash curl http://localhost:8080/cache/user:1 ``` 查看指标: ```bash curl http://localhost:8080/cache/metrics ``` 查看热点: ```bash curl http://localhost:8080/cache/hot-keys ``` --- ## 12. 一句话总结 这个项目采用的是: > **Redis 作为远端统一缓存,应用本地只缓存热点数据,通过滑动窗口识别热点,并通过失效广播保证多节点本地缓存收敛。** 它不是一个“全功能缓存框架”,而是一个结构清晰、便于理解和演进的多级缓存参考实现。