Caffeine热点Key识别与两级缓存联动实践
先说个无关紧要但又容易让人搜不到东西的问题标题里的“caffine”是 Caffeine 的笔误正确拼写是 C-A-F-F-E-I-N-E。Caffeine 是 Java 生态里很常见的高性能本地缓存库我前两天刚好有人问到一个需求用什么方式最简便地判断哪些 key 是热点 key然后再把这些热点 key 优先放进 Caffeine 缓存里。这个需求听起来不难但真要落地时会踩不少坑。今天这篇就把我实践中采用的方案、代码、避坑点完整写出来。这个需求最常见于多级缓存场景。比如业务先查本地缓存如 Caffeine没命中再查 Redis再没命中才落到数据库或下游服务。热点 key 一多本地缓存容量又有限就会出现“热门数据反复被淘汰、冷却数据反而占着坑位”的现象。直接后果就是缓存命中率下降数据库压力飙升甚至被打垮。所以我们需要一个机制识别热点 key让它们优先留在本地缓存里。最理想的情况是整个判断逻辑只依赖应用内代码不额外引入组件代码量越少越好。下面这套方案是我实际项目里用过、并且认为是“最简便但不简陋”的做法。1. 先定义清楚热点 key 要解决的是缓存“重复淘汰”问题1.1 一个典型故障场景我之前接手过一个商品详情服务本地缓存用的就是 Caffeine容量设了 10 万条Redis 存全量商品信息。平时数据量不大一切正常。但一到整点活动或秒杀前的预热阶段突发的热门商品查询会瞬间把某个 key 的访问量拉高几十倍。问题在于这个 key 在此之前并不算热门按访问频次它在本地缓存里排不上前 10 万。等到突发流量打过来第一次查缓存 miss穿透到 RedisRedis 返回后写入本地缓存。但本地缓存容量已满Caffeine 内部的淘汰策略可能刚刚淘汰掉一个“未来 5 分钟内会被频繁访问”的 key而这个刚刚写入的热门 key又因为容量约束随时可能被再次淘汰。这就是典型的“热点 key 反复淘汰”问题热点流量大但缓存却没留住它。你会发现监控面板上的本地缓存命中率在大量请求时反而下降数据库的慢查询数量突然飙升。1.2 判断热点 key 的三个核心维度要设计一个“判断热点 key”的方案第一步不是写代码而是想清楚什么样的 key 算热点。我总结下来至少要看三个维度频次单位时间内的访问量。这是最直接的第一指标比如 10 秒内被访问 100 次平均 10 QPS这已经能对单库造成明显压力。时间分布是突发型热点还是持续型热点。有些 key 是“平时冷、活动时热”这种最容易骗过 LRU 这类只关注“最近访问”的淘汰策略。反而是持续高温的 key一旦进入缓存往往能自己稳定下来。访问代价一次 cache miss 之后下游要花多少成本。查询 DB 可能要几十毫秒和连接池占用查询第三方接口可能要几百毫秒。代价越高的 key越值得优先识别并留在缓存里。真正有价值的判断方案需要同时覆盖“频次”和“时间分布”最好能自然识别突发性热点。而“访问代价”更多是业务层面的事可以在缓存未命中时通过记录耗时来做二次加权不属于最基础的判断逻辑。2. 最简实现用 Caffeine 自己做一个 10 秒滑动窗口计数器2.1 为什么不用 Redis 做计数很多人第一时间想到的是对每个 key 在 Redis 里做 INCR EXPIRE超过阈值就算热点。这个思路没错但它有一个致命的代价每个读请求都要为计数发一次网络请求。热点场景本身就是高并发这么一来计数器请求会加重 Redis 的负担甚至把 Redis 连接池占满。我们做的是本地缓存方案结果把判断逻辑做成 Redis 依赖等于把压力又引回了 Redis非常不划算。更好的做法是计数逻辑完全放在应用内存中用极短的生命周期兜住“高频访问”这个特征。这也是“最简便”方案的核心思路——本地、自包含、代码量少。2.2 核心代码计数器与热点判定具体实现我选用了 Caffeine 本身来做计数缓存。利用它的expireAfterWrite特性一个 key 在 10 秒内被持续访问它的计数条目就一直存活超过 10 秒没有访问条目自动消失相当于一个粗略的滑动窗口复位。import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.LongAdder; public class HotKeyDetector { // 计数缓冲10秒内未被访问的key自动消失近似滑动窗口 private final CacheString, LongAdder counter Caffeine.newBuilder() .expireAfterWrite(10, TimeUnit.SECONDS) .maximumSize(200_000) .build(); // 已判定的热点key5分钟后自动降温避免热点消失后仍占缓存 private final CacheString, Boolean hotKeys Caffeine.newBuilder() .expireAfterWrite(5, TimeUnit.MINUTES) .build(); private final int threshold 100; // 10秒内访问100次即平均10 QPS判定为热点 public boolean isHot(String key) { LongAdder count counter.get(key, k - new LongAdder()); count.increment(); if (count.sum() threshold) { hotKeys.put(key, Boolean.TRUE); } return hotKeys.getIfPresent(key) ! null; } }这段代码的作用很直接每次读请求进来先走isHot判断。如果是热点 key后续逻辑才允许写入本地缓存主缓存池如果不是热点就不让它占用本地缓存容量。10 秒的窗口会自动滑动——因为expireAfterWrite会在 key 被写入或更新后重新计时一个持续被访问的 key 会一直活着一个停止被访问的 key 10 秒后就被回收。2.3 LongAdder 与 expireAfterWrite 的配合原理这里有两个容易被忽视的细节值得单独讲清楚。第一个为什么用LongAdder而不是AtomicLong。热点计数器是最典型的高频写场景大量线程同时对同一个 key 累加计数。AtomicLong在高并发下 CAS 竞争非常激烈性能会明显下降。LongAdder内部做了分段累加把竞争分散到多个槽位最后读取时再求和。它特别适合“写多读少”的场景而我们这里恰恰就是每个请求都写一次、只有少数逻辑读一次sum()所以LongAdder是更合适的选择。第二个为什么用expireAfterWrite而不是expireAfterAccess。expireAfterAccess的语义是“只要这个 key 还在被访问就一直不过期”看起来也没问题但它修的是“最近访问”侧对热点判断不够精确。expireAfterWrite严格从最后一次写入/更新开始计时10 秒窗口一到就清空计数即使这个 key 一直被访问只要它是在持续被访问那么每次increment都会重新触发写入计时效果上依然能保持窗口存活。两者在高频访问下表现相似但expireAfterWrite的语义更清晰也更符合“过去 10 秒内访问了多少次”的窗口定义。所以计数窗口用expireAfterWrite是对的。3. 热点结果怎么喂给 Caffeine两级缓存联动3.1 核心主缓存的配置思路判断出热点 key 之后下一步把它存到 Caffeine 主缓存。主缓存配置要考虑到热数据尽量不被淘汰、冷数据不要占用太多空间。我一般这样配置主缓存import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import java.time.Duration; public class MainCacheConfig { public CacheString, Object buildMainCache() { return Caffeine.newBuilder() .maximumSize(10_000) .expireAfterWrite(Duration.ofMinutes(10)) .build(); } }maximumSize的容量需要根据实际情况估算如果你希望热点数据稳定留存并覆盖所有“可能成为热点”的 key容量可以适当膨胀。这里的 1 万只是一个示例实际项目里我会结合内存预算来设定比如分配 128 MB 给本地缓存每条缓存条目平均 2 KB那么容量上限就大约在 6 万个条目左右。expireAfterWrite(10分钟)是让即使热点 key 也会有一个绝对过期时间避免因为缓存永不失效导致的数据脏读。不过主缓存本身还可以通过权重方式更精细地控制内存但那是进阶玩法基础方案里设置maximumSize就够了。3.2 完整代码演示读写都必须考虑“非热点”把HotKeyDetector和主缓存组合起来就是完整的两级缓存联动。关键在于读请求应先判断热点只有热点 key 才走本地缓存主缓存池非热点 key 直接查询下游不写入主缓存。否则本地缓存很容易被大量低频数据填满热点数据反而进不来。public class CacheService { private final HotKeyDetector detector new HotKeyDetector(); private final CacheString, Object localCache Caffeine.newBuilder() .maximumSize(10_000) .expireAfterWrite(Duration.ofMinutes(10)) .build(); // 模拟下游数据库或远程缓存 private Object loadFromDb(String key) { // 这里省略实际查询逻辑 return new Object(); } public Object get(String key) { // 非热点直接查下游不占用本地缓存容量 if (!detector.isHot(key)) { return loadFromDb(key); } // 热点key优先查本地缓存 Object value localCache.getIfPresent(key); if (value ! null) { return value; } // 本地缓存未命中从下游加载后回填 value loadFromDb(key); localCache.put(key, value); return value; } }这里有一个更值得说的点热点判断本身要先于缓存查询。否则每个请求都先查一遍本地缓存热点判断就变成事后诸葛。先判断、再查、再回填这套顺序保证了热点数据能及时被“保护”起来。3.3 关于热点自动降温的处理热点不是永恒的。一个商品活动结束后它的访问量会快速下降。如果它一直留在本地缓存中不仅浪费空间还会挤掉后续的新热点。所以上面代码里我设计了两个自动降温机制hotKeys使用 5 分钟过期一个 key 被标记为热点后5 分钟后自动“降级”不再强制走本地缓存。这 5 分钟的窗口也和计数窗口的 10 秒形成呼应10 秒的高频访问定义热点5 分钟后给热点一个重新验证的机会。主缓存的expireAfterWrite(10分钟)即使热点标记已过期主缓存条目也会在 10 分钟后被清除下一次请求会重新判断决定是否继续以热点身份缓存。这个“自动降温”非常关键否则这个方案会变成另一个坑热点 key 集合无限膨胀最后热点判断本身占用的内存比缓存条目还多。4. 线上运行后的调参与避坑记录4.1 计数器内存膨胀Caffeine 计数器也要设上限很多人在第一步只写Caffeine.newBuilder().expireAfterWrite(10, TimeUnit.SECONDS).build()忘了设maximumSize。后果是什么呢假如业务高峰期每秒钟有 5 万个不同的 key 在访问10 秒滑动窗口里就会有 50 万个计数条目。每个条目包含 key 字符串、LongAdder对象、Caffeine 的内部节点粗算 300 字节起步50 万条就是 150 MB 左右。这还是在只统计 10 秒的情况下如果窗口设置成 60 秒内存直接奔着 GB 级去了。所以计数器缓存也必须设置上限而且上限要大于“窗口时间内可能出现的最多不同 key 数量”。如果上限设小了比如 10 秒内实际有 20 万个不同的 key计数器上限却只有 10 万那么一部分稍早的计数条目会被强制淘汰这部分 key 的热点判断就不准。实际项目里我会根据峰值 QPS 和窗口大小做估算再留出 2 到 3 倍余量。4.2 阈值怎么定从 10 QPS 还是 50 QPS 开始阈值不能拍脑袋。我建议先估算下游服务承受能力再反推阈值。比如你查一次数据库平均耗时 30 毫秒连接池 50 个连接那么单库理论 QPS 峰值大约 50 / 0.03 ≈ 1667。但为了保护数据库不能让它跑到这个上限通常取 70%也就是单 key 超过 1167 QPS 才开始告警。但 1167 QPS 作为热点阈值太高了还没到阈值数据库已经受到冲击。正确做法是把单 key 的常规访问量作为参考。假设业务中 Top 级热点的访问量是每秒 200 次那阈值设在 100 次/10 秒平均 10 QPS就比较合适宁可多识别一些“接近热点”的 key也不漏掉真热点。窗口和阈值是配套的10 秒内 100 次等价于持续 10 QPS 10 秒。如果业务波动更大可以把窗口缩到 5 秒、阈值设 50 次提高灵敏度如果希望减少误判可以拉长窗口到 30 秒、阈值设 300 次识别“持续稳定的热点”。没有绝对正确的值需要在压测中调整。4.3 多实例部署下计数偏差这是工程化必然要面对的问题单机方案最大的局限在于多实例部署。假设你有 4 个应用实例每个实例各自维护自己的计数器同一个 key 在单个实例上 10 秒只被访问 80 次加总后实际访问 320 次但单实例判断都认为它不是热点。这就会漏判。两种解法各有取舍一种是不追求精确把每个实例的阈值除以实例数比如总阈值 100 次4 个实例就各判 25 次。这样实现简单但在流量分配不均时会误判。另一种是引入分布式计数比如让 Redis 做精确计数但这又回到了前面说过的“每个请求都要 INCR 一次 Redis”的问题。实际项目中我一般组合使用日常以本地计数为主对于能预知的重大活动热点直接通过后台配置主动预热热点 key 到所有实例。这套组合虽然不完美但实施成本最低也足够应付绝大多数场景。4.4 这个简便方案真正该有的能力边界把话说明白一点这套方案判断的是“访问频次高”的热点但对于“瞬时突发且只持续几秒”的峰值10 秒窗口还是有点长。比如某 key 在 2 秒内被打了 100 次之后不再访问它不会被判定成热点但事实上它已经对下游产生了一次不小的冲击。对这种超短时突发热点单靠计数器是挡不住的。能彻底解决的方案是动态本地缓存白名单把“可能成为热点”的 key 在活动开始前就手动预热进所有实例里配合这套自动热点判定一起用。所以准确地说这套方案负责的是“运行期间持续高频的 key”它解决了 80% 的痛点另外 20% 的极端突发需要你提前做预案。5. 换一个视角Caffeine 自己就在做热点识别5.1 W-TinyLFU 是怎么保住热点 key 的如果你只是想“别让热点 key 被淘汰”而不是“主动拿到热点名单”其实不用自己写任何判断逻辑。Caffeine 默认的淘汰策略是 W-TinyLFU它内部使用一个叫 Frequency Sketch 的近似计数结构用 4-bit 计数器记录每个 key 的访问频率并周期性地做频率衰减。它的本质就是一个“说人话”版的热点识别器访问频率高的 key会被标记进去在缓存容量紧张时也不容易被淘汰频率低的冷 key 会优先被清理。这个机制的好处是它已经内嵌在上文说的主缓存里你什么都不用做热点 key 的留存率就比传统 LRU 好很多。所以如果你没有“主动知道热点是谁”这个需求就单纯想让 Caffeine 不淘汰热点那我建议别写任何热点判断代码直接配置好maximumSize和expireAfterWrite就够了。但如果你需要的是主动识别热点 key比如做监控报表、做预热、做容量规划那 Caffeine 内部这个 Frequency Sketch 并不提供 API 让你拿到每个 key 的频率值你能拿到的只是整体淘汰统计。所以上文那套自己维护计数器的方案正是补足这一块缺口的最简便方式。5.2 recordStats把命中率统计接进监控还有一个很容易被忽略的能力recordStats()。只要在主缓存构建时加上这个配置Caffeine 就会提供一个CacheStats统计对象包含命中次数、未命中次数、命中率、淘汰数量等指标。这样你就不需要自己埋点算命中率了。CacheString, Object localCache Caffeine.newBuilder() .maximumSize(10_000) .expireAfterWrite(Duration.ofMinutes(10)) .recordStats() .build();之后用localCache.stats()就能拿到hitCount()总命中次数missCount()总未命中次数hitRate()命中率evictionCount()因容量淘汰的条目数比较典型的关联是如果你的热点判断准确率足够高那么加了recordStats()后命中率曲线应该保持平稳甚至上升淘汰数量下降。如果命中率上不去很可能是阈值设太高、热点 key 没识别出来也可能是主缓存容量太小。这就是一个完整闭环先识别热点再通过命中率验证识别效果再反推调参。我在实际项目里还会把evictionCount()的每次采样值单独打到监控系统观察它是否有突刺。一旦淘汰数量突然飙高几乎可以断定“有一个新的突发热点正在被反复踢出缓存”这时候结合计数器方案很快就能定位到具体是哪个 key。回头再总结一句我自己的体会这套方案能落地最关键的其实就是把“计数缓存”和“主缓存”分开而不是把所有数据都塞进一个 Caffeine 实例。用一个短周期的缓存专门统计访问频次再用一个长周期的缓存专门保护已验证的热点数据两个都有清晰的 TTL热点会自动产生、自动降温你基本上只需要关注阈值和容量这两个数字。如果以后要扩展也可以在HotKeyDetector里加上“热点识别后发送事件通知”的逻辑比如回调某个预热服务提前把热点 key 的完整数据加载进本地缓存那就比现在这段代码更主动了。

相关新闻

Ctrl+A失效的三大根源:焦点、输入法与语义选择

Ctrl+A失效的三大根源:焦点、输入法与语义选择

1. 问题本质与真实场景还原“CtrlA不能全选”这个看似简单的键盘操作失效现象,其实不是某个软件的Bug,而是一套跨平台、跨应用、跨输入法状态的交互逻辑被意外触发后的综合表现。我接触过上百个类似案例,从某高校实验室的Python数据处理脚本编…

2026/10/10 13:20:20 阅读更多 →
基于PJ85718DM与STM32F446ZE的HVAC温度监测系统设计

基于PJ85718DM与STM32F446ZE的HVAC温度监测系统设计

1. 项目背景与核心需求拆解温度监测这件事,听起来像是电子工程入门第一课的内容——不就是读个传感器嘛。但真正落到工业级嵌入式和 HVAC(暖通空调)场景里,事情远没有想象中那么简单。我做过好几个环境监测类的项目,踩…

2026/10/10 13:20:20 阅读更多 →
TCP协议实战手册:从抓包分析到内核调优

TCP协议实战手册:从抓包分析到内核调优

1. 这不是教科书里的TCP,而是我亲手抓包、调参、踩坑后写下的“协议人话手册”你点开这个标题,大概率不是为了背诵“三次握手四次挥手”的标准答案——那玩意儿在面试前突击半小时就能默写,但真让你调试一个卡在SYN_SENT状态的连接&#xff0…

2026/10/10 13:19:18 阅读更多 →

最新新闻

WinSxS文件夹清理指南:用DISM安全释放系统盘空间

WinSxS文件夹清理指南:用DISM安全释放系统盘空间

1. 先搞清楚 WinSxS 到底是个什么东西很多人第一次打开C:\Windows\WinSxS这个文件夹,看到属性里显示十几个 G,甚至二十几个 G,第一反应就是:这玩意儿是不是垃圾?能不能直接删掉腾空间?我当年也是这么想的&a…

2026/10/10 14:54:00 阅读更多 →
Kettle(PDI)安装配置完全指南:版本匹配与避坑实践

Kettle(PDI)安装配置完全指南:版本匹配与避坑实践

简介:面向数据集成初学者、数据分析师及需要快速搭建ETL环境的开发人员,这是一份以Pentaho Data Integration(PDI)下载安装与基础配置为核心的PDF速查教程。Kettle作为开源ETL工具,常用于多平台数据抽取、转换与加载&a…

2026/10/10 14:54:00 阅读更多 →
Clude安装流程全解析:四步跑通本地AI命令行工作台

Clude安装流程全解析:四步跑通本地AI命令行工作台

前阵子有个朋友跑来问我,说手里的AI工具一直停留在网页聊天框的阶段,想要找个能接进本地工作流的方式,问我有没有推荐的方案。我直接丢给他一款叫Clude的开源个人AI工作台——它跟那种只能在浏览器里对话的产品不太一样,装好之后你…

2026/10/10 14:54:00 阅读更多 →
Kettle(PDI)安装与启动实战:从下载到跑通第一个转换

Kettle(PDI)安装与启动实战:从下载到跑通第一个转换

简介:Kettle(Pentaho Data Integration,简称 PDI)是一款开源 ETL 工具,面向需要进行数据抽取、转换与加载的开发者,重点解决该工具在 Windows、Linux、macOS 等平台下的获取、安装与基础配置难题。资料以单…

2026/10/10 14:54:00 阅读更多 →
AIRI 浏览器本地语音识别(Browser Local ASR/STT):当前状态、WIP 占位实现与可用替代方案

AIRI 浏览器本地语音识别(Browser Local ASR/STT):当前状态、WIP 占位实现与可用替代方案

AI 应用人工智能大模型数字人AI Agent语音前端后端 【免费下载链接】airi 💖🧸 Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-samas altitude. Capa…

2026/10/10 14:54:00 阅读更多 →
统一登录与单点登录实战:网关与认证中心的搭建全解

统一登录与单点登录实战:网关与认证中心的搭建全解

这段时间我一直在折腾一件事:把我们内部几个各自为战的业务系统,统一到一个登录入口底下。项目代号倒是很形象,sward 负责守门,soular 负责认人。说白了,sward 是一个网关层,soular 是一个身份认证中心&…

2026/10/10 14:52:58 阅读更多 →

日新闻

卫星轨道分类全解析:从LEO到GEO的选型逻辑与工程实践

卫星轨道分类全解析:从LEO到GEO的选型逻辑与工程实践

1. 从“卫星轨道分类”这个标题说起:为什么值得花时间搞懂第一次接触“卫星轨道分类”这个概念,很多人会觉得它离自己很远——不就是天上的星星怎么转吗?但如果你正在做航天任务规划、遥感数据接收、星座设计,甚至只是准备一场航天…

2026/10/10 0:00:39 阅读更多 →
Spring AOP 核心原理与实战:从概念到日志切面落地

Spring AOP 核心原理与实战:从概念到日志切面落地

1. 从一个真实痛点说起:为什么你的代码里到处都是重复逻辑刚入行那会儿,我写过一个用户管理模块,注册、登录、改密码、注销四个接口。每个接口里都塞了几乎一样的日志打印、参数校验、事务开启和提交。当时觉得没什么,能跑就行。直…

2026/10/10 0:00:40 阅读更多 →
Python招聘数据采集与分析可视化:从采集清洗到薪资技能城市可视化全链路

Python招聘数据采集与分析可视化:从采集清洗到薪资技能城市可视化全链路

简介:这是一套面向计算机相关专业学生与项目实战学习者的Python数据采集与分析可视化完整项目,以Boss直聘岗位数据为对象,适合用作毕业设计、课程设计或期末大作业。资源包共38个文件,约246KB,以13个py源码文件为核心&…

2026/10/10 0:00:40 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 11:14:25 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 1:36:08 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 11:14:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 5:23:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 21:32:20 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 10:38:42 阅读更多 →