本地缓存与分布式缓存深度解析:从原理到实战避坑指南
1. 从一次线上故障说起为什么我们需要理解缓存那天凌晨我被一阵急促的告警电话叫醒。监控显示核心交易服务的响应时间从平时的50毫秒飙升至5秒大量请求超时用户页面白屏。团队紧急排查数据库连接池、网络、CPU均未见异常。最终问题定位到了一个看似不起眼的本地缓存组件上。由于一个热点商品的数据更新策略不当导致集群中数千个服务实例的本地缓存同时失效所有请求瞬间穿透到数据库引发了雪崩。这次事故让我深刻意识到缓存尤其是本地缓存绝非一个“用了就行”的简单组件。它是一把双刃剑用好了是性能利器用不好就是系统稳定性的定时炸弹。而当我们谈论缓存时通常绕不开两个核心概念本地缓存和分布式缓存。很多人对它们的理解停留在“一个在内存里一个在Redis里”的层面但这远远不够。今天我们就来彻底拆解这两者从设计哲学、适用场景、核心实现到避坑实践让你不仅知道是什么更明白为什么这么选以及在实际项目中如何驾驭它们。简单来说本地缓存是将数据存储在应用进程的内存中访问速度极快但数据无法在多个应用实例间共享分布式缓存则是将数据存储在一个独立的外部集群中如Redis、Memcached可以被所有应用实例访问保证了数据的一致性但引入了网络开销。选择哪一种或者如何组合使用取决于你的数据特性、一致性要求以及系统架构。2. 本地缓存深度解析极速背后的取舍与智慧本地缓存顾名思义它的数据生命周期和应用进程绑定在一起。当你的Java应用启动一个HashMap来存一些配置或者使用Spring Cache的ConcurrentMapCacheManager时你就在使用本地缓存。它的最大优势是零网络开销、纳秒级读取速度。但正是这种“极致的快”带来了诸多设计上的挑战。2.1 核心特性与典型实现本地缓存的核心特性可以概括为以下几点超低延迟数据就在本进程堆内或堆外内存访问路径最短。应用生命周期绑定应用重启缓存清空。这对缓存数据的“可丢失性”提出了要求。无网络依赖不担心缓存集群的网络抖动或宕机提升了应用的局部可用性。数据不一致这是最大的痛点。在集群部署时每个实例的本地缓存都是独立的一个实例更新了数据其他实例无法感知导致用户看到过期数据。目前主流的本地缓存库早已不是简单的HashMap它们提供了丰富的生产级特性。以Caffeine和Guava Cache为例它们是Java领域的事实标准。Caffeine可以看作是Guava Cache的现代高性能继承者。它的设计充分考虑了现代多核CPU的特性在并发读写性能上尤其出色。其API设计流畅是当前新建项目的首选。// Caffeine 缓存示例设置大小、过期时间、刷新策略 CacheString, Object cache Caffeine.newBuilder() .maximumSize(10_000) // 基于容量驱逐 .expireAfterWrite(10, TimeUnit.MINUTES) // 写入后10分钟过期 .refreshAfterWrite(1, TimeUnit.MINUTES) // 写入后1分钟刷新异步 .build(key - loadDataFromDB(key)); // 缓存加载逻辑Guava Cache虽然较老但依然稳定可靠有庞大的存量用户。它的功能与Caffeine类似但在高并发场景下的性能略逊一筹。注意选择Caffeine还是Guava对于新项目无脑选Caffeine。如果是老项目维护且Guava Cache工作稳定没有遇到性能瓶颈则不必强行更换。但如果你正在为高并发下的缓存性能问题头疼迁移到Caffeine可能会带来意想不到的收益。2.2 缓存淘汰策略空间管理的艺术内存是有限的当缓存满时如何决定淘汰哪些数据这就是淘汰策略。常见的策略有FIFO先进先出。简单但效果通常不好因为它忽略了数据的访问频率。LRU最近最少使用。这是最经典、最常用的策略。它认为最近被访问过的数据未来再次被访问的概率更高。Caffeine和Guava Cache的默认策略就是基于LRU的变种。LFU最不经常使用。根据数据的历史访问频率来淘汰频率最低的先被淘汰。它适合访问模式非常稳定的场景但对突发性的热点数据不友好。W-TinyLFU这是Caffeine采用的先进算法。它综合了LRU和LFU的优点使用一个频率草图来高效地估算访问频率并对新加入的数据有一定保护期在复杂的工作负载下表现非常出色。为什么Caffeine默认用W-TinyLFU因为在真实的互联网业务中数据访问模式往往是混合的既有长期稳定的热门数据也有短暂爆发的热点。传统的LRU容易被周期性的批量扫描操作污染比如定时任务遍历所有数据导致真正的热点数据被挤出去。W-TinyLFU能更好地适应这种复杂模式提供更高的命中率。2.3 过期与刷新数据新鲜度的博弈让缓存数据过期是保证一致性的最基本手段。但“如何过期”大有讲究。** expireAfterWrite**这是你最应该优先考虑的策略。数据在写入缓存后固定时间过期。例如expireAfterWrite(5m)意味着数据最多有5分钟的延迟。这种策略简单可控能提供最强的一致性上限数据最多过期5分钟。它适合那些可以容忍一定延迟但需要明确过期窗口的场景如商品详情、用户昵称。** expireAfterAccess**在最后一次访问后固定时间过期。这适合用于缓存非常昂贵、但一旦缓存希望尽可能保留的数据。但要注意它可能导致“冷数据”长期占用内存。** refreshAfterWrite**这是一个异步刷新机制。当数据写入后达到刷新时间下一次访问会触发异步加载新值但当前请求立刻返回旧值。这平滑了刷新操作避免了在过期瞬间大量请求同时穿透数据库。它通常与expireAfterWrite结合使用refresh时间短于expire时间既保证了数据的相对新鲜又提供了最终过期兜底。实操心得不要单纯依赖expireAfterAccess。我曾见过一个缓存用户会话的配置用了expireAfterAccess(30d)结果因为一些爬虫或内部工具的持续访问导致大量早已不活跃的用户会话数据常驻内存最终引发OOM。对于会话这类数据expireAfterWrite是更安全的选择。3. 分布式缓存全景一致性、扩展性与高可用当你的应用从单机走向集群本地缓存的数据不一致问题就变得无法忍受。这时你需要一个所有实例都能访问的“中央存储”这就是分布式缓存。Redis是这一领域的绝对霸主它不仅仅是一个缓存更是一个高性能的数据结构服务器。3.1 为什么是Redis不仅仅是Get/Set选择Redis而不是Memcached或其他根本原因在于其丰富的数据结构。这些数据结构让你能以最贴合业务模型的方式存储数据从而设计出极其高效的缓存方案。数据结构缓存场景示例优势分析String缓存单个对象JSON序列化、计数器、锁最通用支持原子增减操作。Hash缓存一个对象的多个字段如用户信息name, age, email可以部分更新字段网络传输体积小存储更紧凑。List消息队列、最新N条动态列表支持左右推入弹出实现简单队列或堆栈。Set好友关系、标签、抽奖去重提供交集、并集等操作适合关系型数据。Sorted Set排行榜、延迟队列自带分数排序范围查询效率高。例如缓存用户信息用String存整个JSON很简单。但如果只需要更新用户的“最后登录时间”这一个字段呢用String就需要序列化整个对象、传输、反序列化、修改、再序列化、传输、写回非常低效。而用Hash只需要一个HSET user:123 last_login即可网络和计算开销极小。3.2 高可用与持久化数据不能丢的底线分布式缓存作为核心中间件高可用是必须的。Redis提供了两种主流方案主从复制一个主节点负责写多个从节点负责读。主节点宕机后需要手动或通过哨兵切换从节点为主节点有短暂的不可用时间。Redis Cluster官方分布式方案。数据自动分片到多个主节点上每个主节点也有对应的从节点。任意节点宕机其从节点会自动接替实现无缝故障转移。对于生产环境Redis Cluster是推荐的选择。关于持久化Redis提供RDB和AOFRDB在特定时间点生成内存快照。恢复快文件小但可能丢失最后一次快照后的数据。AOF记录每一条写命令。数据安全性高最多丢失一秒数据但文件大恢复慢。生产环境建议同时开启RDB和AOF。用AOF保证数据安全用RDB做冷备和快速恢复。可以配置appendfsync everysec在性能和数据安全间取得平衡。记住缓存可以重建但某些场景下如缓存了耗时极长的计算结果丢失缓存意味着服务雪崩因此持久化配置需要谨慎评估。3.3 缓存模式穿透、击穿、雪崩的防御工事使用分布式缓存必须系统性地应对三大经典问题缓存穿透查询一个根本不存在的数据请求每次都穿透到数据库。解决方案布隆过滤器。在查询缓存前先用一个高效的布隆过滤器判断key是否存在。如果布隆过滤器说“不存在”那一定不存在直接返回空。如果布隆过滤器说“可能存在”再去查缓存/数据库。对于查不到的数据也缓存一个空值如NULL并设置一个较短的过期时间避免同一恶意key的持续攻击。缓存击穿某个热点key过期瞬间大量并发请求同时穿透到数据库。解决方案互斥锁。当缓存未命中时不是所有线程都去查数据库而是让一个线程去查其他线程等待。在Redis中可以用SETNX命令实现分布式锁。更优雅的方式是使用Redis的SET key value NX PX命令原子性地实现加锁和超时设置。// 伪代码示例使用Redis分布式锁防止击穿 public Object getData(String key) { Object value redis.get(key); if (value ! null) { return value; } // 尝试获取锁 String lockKey lock: key; boolean locked redis.set(lockKey, 1, NX, PX, 3000); // 锁3秒 if (locked) { try { // 双重检查防止其他线程已经更新了缓存 value redis.get(key); if (value null) { value loadFromDB(key); // 从数据库加载 redis.setex(key, 3600, value); // 写入缓存 } } finally { redis.del(lockKey); // 释放锁 } } else { // 未获取到锁等待片刻后重试或直接返回旧值/默认值 Thread.sleep(100); return getData(key); // 简单重试 } return value; }缓存雪崩大量key在同一时间点或时间段内过期导致所有请求穿透到数据库。解决方案差异化过期时间。这是最关键、最有效的一招。不要在代码里写死expire(3600)而是使用一个基础时间加上一个随机抖动。例如expire(3600 Random.nextInt(600))让过期时间在3600~4200秒之间随机分布避免集体失效。踩坑实录我曾遇到一个“伪雪崩”。某个服务在启动时会全量加载一批配置数据到Redis并设置相同的过期时间。每天凌晨这批key同时失效虽然每个key的访问量不大但总量巨大导致数据库连接池瞬间被打满。解决方案就是在批量加载时为每个key的过期时间加上一个随机偏移量。4. 混合架构实践本地缓存与分布式缓存的组合拳在大型系统中纯本地缓存或纯分布式缓存往往都无法满足所有需求。更成熟的架构是多级缓存其中最常见的就是“本地缓存 分布式缓存”的二级缓存架构。这能兼顾极致的读取性能和集群数据一致性。4.1 二级缓存架构设计典型的读请求路径如下请求到达应用实例。首先查询本地缓存如Caffeine命中则直接返回。本地缓存未命中查询分布式缓存如Redis命中则写入本地缓存并返回。Redis未命中查询数据库将结果写入Redis和本地缓存可选并返回。这个架构带来了显著的性能提升但也引入了新的复杂度如何保证各级缓存的一致性4.2 一致性挑战与解决方案这是二级缓存最棘手的问题。当数据在数据库被更新后如何让成千上万个实例的本地缓存失效基于过期时间的最终一致性这是最简单、最常用的方式。为本地缓存设置一个较短的过期时间如30秒依赖过期机制来达到最终一致。这适用于数据变更不频繁、允许秒级延迟的场景。这是成本最低、实现最简单的方案多数场景下足够使用。消息队列广播失效当数据更新时除了更新数据库和Redis还发送一条消息到消息队列如Kafka、RocketMQ。每个应用实例都订阅这个消息收到后删除自己本地缓存中对应的key。这种方式能达到准实时的一致性但系统复杂度高需要维护消息队列和消费者。Redis Pub/Sub 订阅失效利用Redis自身的发布订阅功能。更新服务在更新数据后向一个特定的Channel发布一条失效消息。所有应用实例订阅该Channel收到消息后清理本地缓存。相比消息队列省去了中间件但Redis的Pub/Sub消息不保证持久化客户端断开连接会丢失消息可靠性稍弱。方案选型建议优先考虑过期时间。评估业务是否能接受秒级延迟如果可以这就是最优解。如果无法接受延迟且数据变更的QPS不高可以考虑Redis Pub/Sub实现相对简单。如果对一致性要求极高且系统架构中已有消息队列则采用消息队列广播这是最可靠的方式。4.3 容量与更新策略本地缓存是有限的不能无脑缓存所有从Redis读到的数据。选择性缓存只将真正的热点数据、变更频率低的数据放入本地缓存。例如城市列表、配置项、热门商品信息等。主动更新对于非常重要的配置类数据可以在应用启动时主动加载到本地缓存并监听变更消息进行更新而不是等待被动查询。监控与度量必须监控本地缓存的命中率、淘汰数量、平均加载时间等指标。如果命中率过低说明缓存的数据价值不大反而浪费内存如果淘汰数过高可能是容量设置太小或数据热点变化太快。5. 实战避坑指南那些教科书上不会写的教训理论终须付诸实践。结合我多年的经验这里有几个容易踩坑的实战要点。5.1 Key的设计可读性、可管理性与性能的平衡Key设计不好后期运维和排查将是噩梦。使用冒号分隔的命名空间如user:profile:123,order:detail:456,config:system:timeout。这清晰明了也便于使用KEYS user:profile:*或SCAN命令进行模式匹配操作。避免过长的KeyKey太长会占用更多内存并且在集群模式下过长的Key可能导致哈希计算不均匀。尽量使用缩写或ID。将可变参数放在最后例如product:detail:{skuId}这样便于扫描同一类数据。警惕Big Key一个Key对应的Value体积巨大如一个List里存了10万个元素。Big Key会导致网络传输阻塞、Redis单线程操作变慢甚至引发集群迁移失败。对于列表型数据考虑分片如user:msg:{userId}:{pageIndex}。5.2 序列化性能与兼容性的隐形杀手Java对象存入Redis前需要序列化。常见的序列化方式有JDK序列化默认但性能差序列化后体积大绝不推荐用于生产环境。JSON可读性好兼容性强但序列化/反序列化性能一般体积较大。常用库有Jackson、Fastjson、Gson。Protobuf、Msgpack、Kryo二进制协议性能极高体积小但需要预定义Schema或注册类可读性差。选型建议对于缓存这种对性能要求极高的场景优先考虑高性能二进制序列化如Kryo。如果团队更看重可读性和调试便利性Jackson是一个不错的折中选择。绝对不要使用JDK默认序列化。5.3 缓存预热与降级应对流量洪峰在大促或活动开始前如果缓存是冷的瞬间的流量会直接压垮数据库。缓存预热在流量到来之前通过离线任务或低峰期脚本将预估的热点数据提前加载到缓存中。可以分析历史日志找出热点商品、热门文章等提前刷入Redis。降级策略当Redis集群本身出现故障或网络异常时应用不能直接挂掉。一种思路是“降级到本地缓存”即当发现Redis不可用时短时间内如5分钟完全依赖本地缓存虽然数据可能更旧但保证了核心服务的可用性。这需要你的本地缓存有足够的热点数据覆盖。5.4 监控与治理没有度量就没有优化缓存用起来之后必须建立完善的监控体系。核心监控指标命中率这是衡量缓存效益的核心指标。命中率过低要反思缓存策略和Key设计。平均响应时间监控缓存操作的耗时异常升高可能预示网络或Redis负载问题。内存使用率避免Redis内存打满触发淘汰或OOM。连接数防止应用连接泄漏导致Redis连接池耗尽。慢查询定期检查Redis慢查询日志优化KEYS、HGETALL大Key等操作。治理操作建立安全的缓存清理和查询通道。例如提供一个内部管理界面支持按模式扫描和删除缓存Key方便在数据出问题时快速清理。回到开头那个故障我们的解决方案正是综合运用了以上策略首先为那个热点商品Key设置了差异化的过期时间避免集体失效。其次引入了refreshAfterWrite机制在缓存过期前就异步更新平滑了数据加载。最后加强了该Key的监控告警。缓存的世界没有银弹只有对原理的深刻理解和对场景的灵活权衡才能让它真正成为系统的加速器而非火药桶。

相关新闻

硬件安规设计核心:电气间隙、爬电距离与绝缘穿透距离详解

硬件安规设计核心:电气间隙、爬电距离与绝缘穿透距离详解

1. 安规距离:产品设计的“生命线”与“高压线” 在硬件产品,尤其是涉及市电或更高电压等级的产品开发中,工程师们常常会听到一个词:“安规距离”。这简简单单的四个字,背后承载的是产品安全性的基石,是防止…

2026/10/11 23:39:02 阅读更多 →
基于WebRTC与Spring Boot的P2P文件传输系统实战

基于WebRTC与Spring Boot的P2P文件传输系统实战

1. 项目概述:从“传文件”到“瞬传”的蜕变 那天下午,同事在群里喊:“谁有最新的测试报告?发我一下,急!”紧接着就是一连串的“文件已过期”、“链接失效了”、“网盘太慢,还得登录”。这场景&a…

2026/10/7 11:59:18 阅读更多 →
鸿蒙6.1 @ohos.router坑:push/replace废迁pushUrl/replaceUrl+RouterMode enum非字符串

鸿蒙6.1 @ohos.router坑:push/replace废迁pushUrl/replaceUrl+RouterMode enum非字符串

本文是「鸿蒙 6.1 API 23 开发坑系列」第 9 篇(非 UI 系第 3 篇)。本篇讲 ohos.router namespace(API 8,鸿蒙 6.1 API 23 基座)——页面路由 router.pushUrl/router.replaceUrl/router.back RouterMode/RouterOptions…

2026/10/9 10:39:32 阅读更多 →

最新新闻

改进版Q-learning实战:Double Q、n步回报与经验回放

改进版Q-learning实战:Double Q、n步回报与经验回放

简介:基于Q-learning的改进版强化学习算法项目,聚焦路径规划场景,面向MATLAB用户及强化学习入门者。项目针对经典Q-learning收敛慢的问题,融合学习率衰减、动态ε-greedy探索、经验回放、目标网络与双线性更新等改进策略&#xff…

2026/10/11 23:38:45 阅读更多 →
定时任务从Crontab到XXL-JOB:选型、实现与运维避坑指南

定时任务从Crontab到XXL-JOB:选型、实现与运维避坑指南

定时任务这个东西,我在不同项目里来来回回用了好多年。最早是拿 shell 脚本挂着 crontab 跑,后来做 PHP 后台管理系统时研究过 likeadmin 这类框架里定时任务的执行机制,再到现在维护 SpringCloud 集群,又得面对分布式定时任务怎么…

2026/10/11 23:38:45 阅读更多 →
VOC垃圾检测数据集14963张:Darknet训练YOLO全流程指南

VOC垃圾检测数据集14963张:Darknet训练YOLO全流程指南

简介:面向YOLOv3/v4/v5及Darknet框架训练需求,这份VOC格式垃圾分类数据集提供了完整的图片、标注与标签体系。数据集中包含14963张垃圾图片,每张均对应同名xml标注文件和yolo格式txt文件,合计44类全英文标签,并额外提供…

2026/10/11 23:38:45 阅读更多 →
社交网络链路预测实战:Python图算法与VGAE工程化指南

社交网络链路预测实战:Python图算法与VGAE工程化指南

简介:本资源是一套面向高校本科生与研究生的社交网络链路预测实践项目,适用于毕业设计、课程设计及科研入门场景,聚焦图神经网络与传统相似性指标在关系预测中的建模与对比分析。压缩包含345个文件,总大小33.94MB,其中…

2026/10/11 23:38:45 阅读更多 →
HarmonyOS 7 GridRow:折叠屏筛选面板断点抖动门禁【鸿蒙心迹】

HarmonyOS 7 GridRow:折叠屏筛选面板断点抖动门禁【鸿蒙心迹】

相册筛选页最难解释的状态异常,有时并不出现在网络请求上。用户只是把折叠屏展开、收回,再展开,原本已经勾好的“城市、夜景、建筑”仍然亮着,结果列表却突然刷新两遍。更糟糕的是,第一次请求还没回来,第二…

2026/10/11 23:38:45 阅读更多 →
Q-learning改进版全解析:目标网络、经验回放与Double Q实战

Q-learning改进版全解析:目标网络、经验回放与Double Q实战

简介:这份资源是基于Q-learning改进的强化学习算法实现,开发工具为MATLAB,面向路径规划与人工智能学习者,适合机器人导航、网格寻路、游戏AI等场景下的最优策略求解问题。ZIP压缩包共包含21个文件,以19个.m脚本为核心&…

2026/10/11 23:37:44 阅读更多 →

日新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/11 0:00:27 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/11 0:00:27 阅读更多 →

月新闻

我发现了一个新思路:用 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/11 10:45:37 阅读更多 →
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/11 14:36:53 阅读更多 →
黑夜航拍船只数据集训练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/11 14:36:54 阅读更多 →