SpringBoot本地缓存热点key自动探测动态TTL延长解决高并发信息流分页场景整体架构这是一个可配置、高性能、自动识别热点、自动延长缓存时间的本地缓存系统配置类统一缓存大小过期时间热点阈值缓存构建类创建本地caffeine本地缓存热点探测器滑动窗口统计访问热度分级动态延长TTL作用让热门内容缓存更久、冷门内容自动过期大幅减轻数据库 / 服务压力。实现详解CacheProperties配置绑定类当本地 Caffeine 缓存里的缓存键值对数量超过 maxSize1000 条时Caffeine 自动按照自己内置淘汰算法删掉一部分缓存控制总数不超限。这里讲一下caffeine的逐出策略-LRU只淘汰最久没访问的容易被冷门一次性数据污染缓存LFU只淘汰访问次数最少的老旧冷门容易占坑Window TinyLFUCaffeine 默认结合访问频率 最近访问时间兼顾经常访问的热点 key 尽量保留 偶尔访问的一次性冷数据优先被清 避免缓存污染、命中率极高 代码采用容量限制 写入固定过期时间 双重控制时间到了不管满不满都过期数量超了按 Caffeine 算法主动踢掉冷门package com.tongji.cache.config;import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component;/**缓存相关配置项。配置前缀{code cache}用于绑定 {code application.yml} 中的缓存参数。*/ Component ConfigurationProperties(prefix cache) Data public class CacheProperties { // 进程内缓存本地二级缓存配置。 private L2 l2 new L2();// 热点 Key 识别与扩展策略配置。 private Hotkey hotkey new Hotkey(); Data public static class L2 { // 公共信息流缓存配置。 private PublicCfg publicCfg new PublicCfg(); // 个人信息流缓存配置。 private MineCfg mineCfg new MineCfg(); // 知文详情缓存配置 private DetailCfg detailCfg new DetailCfg(); } Data public static class PublicCfg { // TTL秒写入后在本地缓存中保留的时长。 private int ttlSeconds 15; // 最大条目数超过后按 Caffeine 策略逐出。 private long maxSize 1000; } Data public static class MineCfg { // TTL秒写入后在本地缓存中保留的时长。 private int ttlSeconds 10; // 最大条目数超过后按 Caffeine 策略逐出。 private long maxSize 1000; } Data public static class DetailCfg { // TTL秒写入后在本地缓存中保留的时长。 private int ttlSeconds 30; // 最大条目数超过后按 Caffeine 策略逐出。 private long maxSize 5000; } Data public static class Hotkey { // 热点统计窗口长度秒。 private int windowSeconds 60; // 统计窗口切片大小秒用于按段累计访问次数。 private int segmentSeconds 10; // 低热度阈值窗口内访问次数达到该值视为低热。 private int levelLow 50; // 中热度阈值窗口内访问次数达到该值视为中热。 private int levelMedium 200; // 高热度阈值窗口内访问次数达到该值视为高热。 private int levelHigh 500; // 低热度额外延长 TTL秒。 private int extendLowSeconds 20; //中热度额外延长 TTL秒。 private int extendMediumSeconds 60; // 高热度额外延长 TTL秒。 private int extendHighSeconds 120; }CacheConfig缓存构建类使用 Caffeine 创建两个本地缓存 Bean分别给公共信息流广场 / 推荐 个人信息流我的发布 两个 BeanfeedPublicCache公共页缓存容量publicCfg.maxSize写入过期publicCfg.ttlSecondsfeedMineCache个人页缓存1. 容量mineCfg.maxSize 1. 写入过期mineCfg.ttlSecondsBean(feedPublicCache) 把方法返回的caffeine对象交给Spring管理FeedPageResponse 一页公共信息的流的返回结果Key一般是分页参数是一个专门用来放一页信息流的封装结果类是java16的新Record类型专门用了存数据不存逻辑自动生成各种方法getset)List items记录一页中所有内容比如一页返回 10 条帖子这里就是 10 个 FeedItem。public record FeedPageResponse( ListFeedItemResponse items, int page, int size, boolean hasMore ) {}Caffeine.newBuilder() 链式调用Bean(feedPublicCache) public CacheString, FeedPageResponse feedPublicCache(CacheProperties props) { return Caffeine.newBuilder() .maximumSize(props.getL2().getPublicCfg().getMaxSize()) .expireAfterWrite(Duration.ofSeconds(props.getL2().getPublicCfg().getTtlSeconds())) .build(); }过期设置写入后多久过期‘package com.tongji.cache.config; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import com.tongji.knowpost.api.dto.FeedPageResponse; import com.tongji.knowpost.api.dto.KnowPostDetailResponse; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.time.Duration; /** * Caffeine 本地缓存配置。 * * p用于在应用进程内缓存分页结果降低数据库与下游服务压力。/p */ Configuration public class CacheConfig { /** * 公共信息流广场/推荐分页缓存。 * * p键通常由分页游标、页大小、过滤条件等组合而成值为一页的 {link FeedPageResponse}。/p */ Bean(feedPublicCache) public CacheString, FeedPageResponse feedPublicCache(CacheProperties props) { return Caffeine.newBuilder() .maximumSize(props.getL2().getPublicCfg().getMaxSize()) .expireAfterWrite(Duration.ofSeconds(props.getL2().getPublicCfg().getTtlSeconds())) .build(); } /** * 我的信息流个人主页/我的发布等分页缓存。 * * p键通常包含用户标识与分页参数TTL 与容量由配置项控制。/p */ Bean(feedMineCache) public CacheString, FeedPageResponse feedMineCache(CacheProperties props) { return Caffeine.newBuilder() .maximumSize(props.getL2().getMineCfg().getMaxSize()) .expireAfterWrite(Duration.ofSeconds(props.getL2().getMineCfg().getTtlSeconds())) .build(); } /** * 知文详情本地缓存。 * * p键为 knowpost:detail:{id}:v{version}值为 {link KnowPostDetailResponse}。/p */ Bean(knowPostDetailCache) public CacheString, KnowPostDetailResponse knowPostDetailCache(CacheProperties props) { return Caffeine.newBuilder() .maximumSize(props.getL2().getDetailCfg().getMaxSize()) .expireAfterWrite(Duration.ofSeconds(props.getL2().getDetailCfg().getTtlSeconds())) .build(); } }HotKeyDetector热点 Key 探测器 —— 核心滑动窗口 热度分级 动态 TTL 延长/** 缓存配置包含窗口/分段参数、等级阈值、扩展秒数 */ private final CacheProperties properties; /** 每个 key 的滑窗分段计数数组长度为 segments */ private final MapString, int[] counters new ConcurrentHashMap(); /** 当前活跃分段索引原子维护 */ private final AtomicInteger current new AtomicInteger(0); /** 滑窗分段数量windowSeconds / segmentSeconds */ private final int segments;new ConcurrentHash高并发安全Hash使用不同的Hashmap会出错数据丢失多线程读写会脏数据死循环为什么不用Hashtable 加锁太重并发差性能低ConcurrentHashMap 优点线程安全 高并发无锁 读多写多都极快 适合热点统计这种超高 QPS 场景 这个到底存什么窗口 60s 切片 10s 分成 6 段 key1: [ 12, 5, 3, 0, 0, 0 ] → 总热度 20 key2: [ 100, 80, 60, 40, 0, 0 ] → 总热度 280 → 高热 key3: [ 0, 0, 0, 0, 0, 0 ] → 无热度我后续会专门出一期高并发Hash精讲AtomicIntegerAtomicInteger 线程安全的 int 计数器它能保证多线程同时修改同一个数字不会算错、不会乱套。原子操作整数类“原子” 不可分割、一步完成package com.tongji.cache.hotkey; import com.tongji.cache.config.CacheProperties; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.util.Arrays; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; /** * 热键探测器滑动时间窗口计数 热度分级 TTL 动态扩展。 * p * 设计说明 * - 采用固定分段滑动窗口窗口长度 windowSeconds分段长度 segmentSeconds段数 segmentswindow/segment * - 每个 key 维护长度为 segments 的数组 counters[key]current 指向当前活跃段 * - 周期性 rotate 将 current 前移并清零新段实现近窗口热度的自然衰减 * - 根据总热度 hΣ段计数映射到 NONE/LOW/MEDIUM/HIGH 的热度等级 * - 提供 ttlForPublic/ttlForMine在基准 TTL 上叠加等级扩展秒数保护热点请求。 * p * 并发语义 * - 使用 ConcurrentHashMap 存储计数数组AtomicInteger 维护段游标 * - 计数递增为无锁数组操作rotate 仅清零新段避免大范围写冲突 * - 统计为近似滑窗保证在高并发下的稳定与低开销。 */ Component public class HotKeyDetector { public enum Level { NONE, LOW, MEDIUM, HIGH } /** 缓存配置包含窗口/分段参数、等级阈值、扩展秒数 */ private final CacheProperties properties; /** 每个 key 的滑窗分段计数数组长度为 segments */ private final MapString, int[] counters new ConcurrentHashMap(); /** 当前活跃分段索引原子维护 */ private final AtomicInteger current new AtomicInteger(0); /** 滑窗分段数量windowSeconds / segmentSeconds */ private final int segments; /** * 初始化探测器根据配置计算分段数量。 * param properties 缓存配置hotkey */ public HotKeyDetector(CacheProperties properties) { //表示当前对象的properties this.properties properties; int segSeconds properties.getHotkey().getSegmentSeconds(); int winSeconds properties.getHotkey().getWindowSeconds(); this.segments Math.max(1, winSeconds / Math.max(1, segSeconds)); } /** * 记录一次访问将计数累加到当前分段。 * param key 缓存键 */ public void record(String key) { int[] arr counters.computeIfAbsent(key, k - new int[segments]); arr[current.get()]; } /** * 计算近窗口总热度各分段求和。 * param key 缓存键 * return 热度值 */ public int heat(String key) { int[] arr counters.get(key); if (arr null) { return 0; } int sum 0; for (int v : arr) { sum v; } return sum; } /** * 计算热度评级根据总热度与阈值映射到等级。 * 阈值来源properties.hotkey.levelLow/Medium/High。 * param key 缓存键 * return 热度等级 */ public Level level(String key) { int h heat(key); if (h properties.getHotkey().getLevelHigh()) { return Level.HIGH; } if (h properties.getHotkey().getLevelMedium()) { return Level.MEDIUM; } if (h properties.getHotkey().getLevelLow()) { return Level.LOW; } return Level.NONE; } /** * 计算公共页面的动态 TTL基准 TTL 等级扩展秒数。 * param baseTtlSeconds 基准 TTL 秒数 * param key 缓存键 * return 动态 TTL 秒数 */ public int ttlForPublic(int baseTtlSeconds, String key) { Level l level(key); return baseTtlSeconds extendSeconds(l); } /** * 计算“我的发布”页面的动态 TTL基准 TTL 等级扩展秒数。 * param baseTtlSeconds 基准 TTL 秒数 * param key 缓存键 * return 动态 TTL 秒数 */ public int ttlForMine(int baseTtlSeconds, String key) { Level l level(key); return baseTtlSeconds extendSeconds(l); } /** * 根据热度等级返回扩展秒数。 * param l 热度等级 * return 扩展秒数 */ private int extendSeconds(Level l) { return switch (l) { case HIGH - properties.getHotkey().getExtendHighSeconds(); case MEDIUM - properties.getHotkey().getExtendMediumSeconds(); case LOW - properties.getHotkey().getExtendLowSeconds(); default - 0; }; } /** * 定时轮转当前分段清零新分段以实现滑动窗口统计。 * 触发频率由配置 cache.hotkey.segment-seconds 指定单位秒。 */ Scheduled(fixedRateString ${cache.hotkey.segment-seconds:10}000) public void rotate() { //找到即将被循环使用的 “最老段” int next (current.get() 1) % segments; //所有 record(key) 都会往这个新分段计数。 current.set(next); for (int[] arr : counters.values()) { arr[next] 0; } } /** * 重置指定 key 的滑窗计数全部清零。 * 用于手动降级或在配置变更后清理历史热度。 * param key 缓存键 */ public void reset(String key) { int[] arr counters.get(key); if (arr ! null) Arrays.fill(arr, 0); } }