资讯动态

二级缓存设计原理与实操:从概念到避坑指南

发布时间:2026/10/24 1:28:29 来源:尧图企业网站定制
1. 二级缓存到底是个什么东西第一次听到“二级缓存”这个词很多人脑子里冒出来的问号是缓存就缓存怎么还分一级二级难道还有三级这又不是打游戏排位赛。我刚开始接触后端开发那会儿也是这个反应觉得数据库查得快就行了搞这么多层缓存纯属给自己找事。后来在一个日活不算小的项目里被现实狠狠教育了一顿才明白二级缓存这套东西存在的意义。先把概念说清楚。二级缓存英文叫 Second-Level Cache通常出现在 ORM 框架比如 Hibernate、MyBatis的语境里。它指的是跨越多个会话Session共享的缓存和它对应的是一级缓存也就是 Session 级别的缓存。一级缓存的生命周期跟着一次数据库会话走会话关了缓存就没了。二级缓存的生命周期跟着整个应用走只要应用还在跑缓存就在所有会话都能共享里面的数据。打个比方。一级缓存就像你去图书馆看书随手把要用的几本书放在自己桌子上别人拿不到你走了桌子也就清了。二级缓存则是图书馆门口的公共书架谁都能往上放书谁都能从上面取书只要没人主动清理书就一直在那儿。这个类比不一定完全精确但能帮你快速建立直觉。那二级缓存解决什么问题核心就一个字快。当你的应用频繁读取同一批数据而这些数据又不怎么变化的时候每次都去查数据库就是浪费。数据库连接是稀缺资源查询要走网络、要解析 SQL、要读磁盘每一步都是开销。二级缓存把这些热点数据放在应用进程的内存里下次再要直接从内存拿省掉了整个数据库往返的过程。适合用二级缓存的场景有这么几类读多写少的数据比如字典表、配置表、分类目录跨会话共享的数据比如多个用户都要查的商品分类信息对一致性要求不是特别苛刻的数据比如排行榜、统计信息。反过来如果你的数据频繁更新或者对实时一致性要求极高那二级缓存反而会给你添乱后面我会详细说这个坑。这篇文章我会从二级缓存的整体设计思路讲起然后拆解核心细节和实操要点接着给出一套完整的实操流程最后把我踩过的坑和常见问题整理成速查表。不管你是刚接触 ORM 框架的新手还是已经用过但没深究原理的老手应该都能从里面找到对自己有用的东西。2. 二级缓存的整体设计与思路拆解2.1 为什么需要跨会话共享缓存要理解二级缓存的设计得先理解一级缓存的局限性。一级缓存绑定在单个 Session 上它的作用范围非常有限。举个例子你在一个请求里查了一次用户 ID 为 1001 的信息同一个 Session 里再查一次框架会直接从一级缓存返回不会打数据库。这很好。但下一个请求来了新的 Session 创建了再查 ID 为 1001 的用户一级缓存里什么都没有还得老老实实去查数据库。在高并发场景下这种重复查询的量非常恐怖。假设你的首页要展示热门商品列表每秒有几千个请求进来每个请求都要查一次同样的商品数据。一级缓存帮不了你因为每个请求都是独立的 Session。这时候二级缓存就派上用场了——第一个请求查完数据库后把结果放进二级缓存后面的请求直接命中缓存数据库压力瞬间降下来。这就是二级缓存设计的核心动机把缓存的作用域从单个会话提升到整个应用级别让数据在多个会话之间共享从而大幅减少数据库查询次数。2.2 二级缓存和一级缓存的协作关系很多人以为二级缓存是替代一级缓存的其实不是。它们俩是协作关系查询的时候有明确的优先级顺序。当你的代码发起一次数据查询框架的处理流程大致是这样的先查一级缓存Session 级别如果有直接返回流程结束。一级缓存没有再查二级缓存SessionFactory 级别如果有把数据放进一级缓存然后返回。二级缓存也没有才去查数据库查到的结果先放进二级缓存再放进一级缓存最后返回。这个顺序很关键。一级缓存的优先级永远高于二级缓存因为一级缓存里的数据是当前会话最新的而二级缓存里的数据可能已经被其他会话修改过了。框架这样设计是为了尽量保证当前会话能读到最新的数据。你可以把一级缓存理解成你手边的笔记本二级缓存理解成办公室的公共白板。你要查一个信息先翻自己的笔记本没有再看白板白板上也没有才去问别人。这个顺序不能乱因为你自己笔记本上的信息是你刚记的最新鲜。2.3 缓存数据的存储结构设计二级缓存内部并不是简单地把对象丢进一个 Map 就完事了。它需要解决几个关键问题数据怎么存、怎么找、怎么失效。存储结构方面二级缓存通常按照“区域Region”来划分。每个区域可以独立配置缓存策略、过期时间、最大容量等参数。比如你可以给用户信息配一个区域给商品分类配另一个区域它们的缓存策略可以完全不同。这种设计的好处是灵活不同业务的数据特征不一样用同一套策略肯定不合适。数据定位方面缓存需要知道用什么样的 key 来存取数据。这个 key 通常由几个部分组成实体类名、主键 ID、以及可能的其他标识。比如查询 ID 为 1001 的用户缓存的 key 可能是类似User#1001这样的结构。当你更新了 ID 为 1001 的用户框架就能根据这个 key 精确地找到缓存中对应的数据并让它失效。失效策略方面这是二级缓存最复杂的部分。当数据发生增删改操作时框架需要决定哪些缓存数据要清理。如果清理不干净就会出现缓存和数据库不一致的问题如果清理过度缓存的命中率就会下降等于白做。后面我会专门讲这个。2.4 主流缓存实现的选型考量二级缓存本身只是一个规范或者说接口具体用什么来存是可以选的。常见的实现方式有几种缓存实现存储位置特点适用场景内存 Map应用进程内存零依赖速度最快但容量有限重启丢失开发测试、小规模应用本地缓存库应用进程内存支持过期策略、容量控制功能比裸 Map 丰富单机应用、对延迟敏感的场景分布式缓存独立缓存服务支持多应用实例共享容量大可持久化集群部署、大规模应用选哪种取决于你的部署架构和数据规模。单机应用用本地缓存就够了简单直接。但如果你是多实例部署每个实例都有自己的本地缓存那就会出现数据不一致的问题——实例 A 更新了数据实例 B 的缓存还是旧的。这时候就需要分布式缓存来统一管理。注意分布式缓存虽然解决了多实例共享的问题但引入了网络开销。每次读缓存都要走一次网络请求虽然比查数据库快但比本地内存慢。所以有些架构会采用“本地缓存 分布式缓存”的两级组合本地缓存作为第一层分布式缓存作为第二层兼顾速度和一致性。3. 核心细节解析与实操要点3.1 缓存命中率的关键影响因素缓存命中率是衡量二级缓存效果的核心指标。命中率高说明大部分查询都走了缓存数据库压力小命中率低说明缓存没起到应有的作用反而增加了内存开销和维护成本。影响命中率的因素主要有这么几个数据访问的集中度。如果大量请求都在访问同一批数据命中率自然高。比如电商首页的热门商品几万个请求都在查那几十个商品缓存效果非常好。但如果每个请求查的数据都不一样缓存就没啥用了因为每次都是冷启动。缓存容量。缓存能存的数据量是有限的如果热点数据的总量超过了缓存容量就会频繁发生淘汰命中率下降。你需要根据实际数据量和访问模式来估算合理的缓存容量。过期时间设置。过期时间太短数据刚放进去就过期了命中率低过期时间太长数据可能已经变了但缓存还没更新一致性差。这个需要根据业务对数据新鲜度的要求来权衡。更新频率。如果数据更新很频繁每次更新都会导致缓存失效那缓存基本上一直在被清理和重建命中率很难上去。这种场景下要么换一种缓存策略要么干脆不用缓存。3.2 缓存与数据库的一致性保障这是二级缓存最让人头疼的问题。缓存和数据库是两套独立的存储系统你更新了数据库缓存不一定知道你更新了缓存数据库也不一定知道。如果两边数据不一致用户就可能看到错误的信息。常见的处理策略有几种读写穿透策略。读的时候先查缓存缓存没有就查数据库并写入缓存写的时候先更新数据库然后让缓存失效。这是最常用的策略逻辑简单大部分场景够用。但有一个时间窗口的问题更新数据库之后、缓存失效之前如果有请求进来可能会读到旧数据。不过这个窗口通常很短对大多数业务来说可以接受。写穿策略。写的时候同时更新数据库和缓存保证两边数据一致。这种策略的问题是写入开销大而且如果缓存更新失败数据就不一致了。另外如果写操作很频繁缓存会被频繁更新影响读性能。写回策略。写的时候只更新缓存不立即更新数据库等缓存被淘汰或者达到某个条件时再批量写回数据库。这种策略写性能最好但风险也最大——如果缓存服务挂了还没写回的数据就丢了。实操心得大部分业务场景下读写穿透策略是最稳妥的选择。它的逻辑简单出问题的概率低而且即使出现短暂的不一致影响也有限。除非你的业务对一致性要求极高否则不建议用写穿或写回策略复杂度高但收益不明显。3.3 缓存粒度的选择缓存粒度指的是你缓存的是什么样的数据单位。粒度太粗缓存的数据量大更新时影响范围广粒度太细缓存管理复杂元数据开销大。常见的粒度选择有实体级别以单个实体对象为单位缓存比如一个用户、一个商品。这是最常见的粒度管理简单更新精确。集合级别以一组查询结果为单位缓存比如“所有分类列表”。这种粒度适合那些查询条件固定、结果集不大的场景。查询级别以具体的查询语句和参数为单位缓存。粒度最细但管理最复杂因为任何一条相关数据的更新都可能导致大量查询缓存失效。我的建议是优先用实体级别缓存。它的管理逻辑最清晰更新时只需要让对应实体的缓存失效即可不容易出错。集合级别和查询级别缓存虽然在某些场景下能提高命中率但维护成本高容易出 bug除非有明确的性能瓶颈否则没必要上。3.4 缓存失效的触发机制缓存失效是保证数据一致性的关键环节。当数据发生增删改操作时框架需要知道哪些缓存需要被清理。失效的触发方式主要有基于实体的失效。当你更新一个实体对象时框架自动让该实体对应的缓存失效。这是最精确的失效方式影响范围最小。基于集合的失效。当你更新一个实体时所有包含该实体的集合缓存都需要失效。比如你更新了一个商品的价格那“所有商品列表”这个集合缓存就得失效。这种失效方式影响范围大但能保证一致性。基于时间戳的失效。给缓存数据打上时间戳每次读取时检查是否过期。这种方式实现简单但无法精确控制失效时机可能会有较长的数据不一致窗口。注意集合缓存的失效是二级缓存中最容易出问题的地方。很多框架默认不会自动失效集合缓存需要你手动配置或者手动清理。如果你用了集合缓存但没处理好失效逻辑用户就可能看到过期的列表数据。这个坑我踩过不止一次。3.5 并发环境下的缓存安全多线程环境下缓存的读写需要考虑线程安全。如果多个线程同时读写同一个缓存条目可能会出现数据竞争的问题。框架层面通常会做一定的并发控制比如用读写锁来保护缓存的读写操作。但作为开发者你需要注意几点缓存对象的不可变性。如果缓存的对象是可变的一个线程修改了对象其他线程看到的也会变可能导致不可预期的行为。尽量缓存不可变对象或者在放入缓存前做深拷贝。缓存击穿的处理。当某个热点 key 失效的瞬间大量请求同时涌向数据库可能把数据库打垮。常见的解决方案是加互斥锁只让一个请求去查数据库其他请求等待。缓存雪崩的预防。如果大量缓存同时过期也会导致大量请求涌向数据库。解决办法是给过期时间加一个随机偏移量避免同时过期。4. 实操过程与核心环节实现4.1 环境准备与基础配置这一节我以 Java 生态中常见的 ORM 框架为例走一遍二级缓存的完整配置流程。不同框架的具体配置方式不一样但核心思路是相通的。首先你需要在项目里引入缓存实现的依赖。假设我们选用一个常见的分布式缓存作为二级缓存的存储后端需要在构建文件中添加对应的依赖项。具体依赖坐标根据你选用的缓存产品来定这里不展开。然后在框架的配置文件中开启二级缓存。以 XML 配置为例大致是这样的property namehibernate.cache.use_second_level_cachetrue/property property namehibernate.cache.region.factory_classyour_cache_region_factory/property property namehibernate.cache.use_query_cachefalse/property这里有几个关键点。use_second_level_cache是二级缓存的总开关必须设为 true。region.factory_class指定缓存区域的工厂类决定了你用哪种缓存实现。use_query_cache是查询缓存的开关默认关闭因为查询缓存的管理复杂度高不建议一上来就开。4.2 实体类的缓存标注开启总开关之后并不是所有实体都会自动被缓存。你需要显式地告诉框架哪些实体需要缓存。有两种方式注解方式和 XML 配置方式。注解方式更简洁直接在实体类上加标注Entity Table(name users) Cacheable org.hibernate.annotations.Cache(usage CacheConcurrencyStrategy.READ_WRITE) public class User { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; private String name; private String email; // getters and setters }Cacheable表示这个实体需要被缓存。Cache注解里的usage参数指定了并发策略常见的有四种并发策略说明适用场景READ_ONLY只读缓存数据放入后不可修改字典表、配置表等永不变化的数据READ_WRITE读写缓存使用软锁保证一致性大部分需要缓存的业务数据NONSTRICT_READ_WRITE非严格读写不保证强一致性对一致性要求不高的场景TRANSACTIONAL事务级缓存支持事务回滚对一致性要求极高的场景选哪种策略取决于你的业务对一致性的要求。READ_ONLY 性能最好但数据不能变READ_WRITE 最常用兼顾性能和一致性TRANSACTIONAL 最安全但性能开销大一般用不到。4.3 缓存区域参数的调优每个缓存区域都可以独立配置参数这些参数直接影响缓存的性能和效果。以常见的缓存配置为例你需要关注这几个参数最大缓存条目数。这个参数决定了缓存能存多少个对象。设得太小频繁淘汰命中率低设得太大占用内存多可能影响应用本身的运行。估算方法是热点数据的总条数乘以一个安全系数通常 1.5 到 2 倍。过期时间。决定了缓存数据多久之后自动失效。设得太短缓存频繁重建设得太长数据新鲜度差。对于变化不频繁的数据可以设几十分钟到几小时对于变化较频繁的数据设几分钟到十几分钟。空闲时间。指缓存条目多久没被访问就自动失效。这个参数用来清理那些“冷”数据给热点数据腾出空间。通常设得比过期时间长一些。配置示例cache namecom.example.entity.User maxEntriesLocalHeap10000 eternalfalse timeToIdleSeconds300 timeToLiveSeconds600 memoryStoreEvictionPolicyLRU/这里的memoryStoreEvictionPolicy指定了淘汰策略LRU最近最少使用是最常用的还有 LFU最不经常使用和 FIFO先进先出。LRU 适合大多数场景因为它能保留最近被访问过的数据。4.4 代码层面的缓存操作配置好之后大部分缓存操作框架会自动处理你不需要手动写缓存读写的代码。但有几个地方需要你特别注意。事务提交时机。二级缓存的写入通常发生在事务提交之后而不是事务进行中。这意味着如果你在同一个事务里更新了数据又立即查询可能读到的还是缓存里的旧数据。要避免这个问题可以在更新后手动清理缓存或者调整事务的传播行为。手动清理缓存。有些场景下框架不会自动清理缓存需要你手动干预。比如你用了原生 SQL 更新了数据框架不知道缓存就不会失效。这时候你需要手动调用缓存清理SessionFactory sessionFactory entityManagerFactory.unwrap(SessionFactory.class); sessionFactory.getCache().evictEntityData(User.class, userId);批量操作的缓存处理。批量更新或批量删除时如果逐条清理缓存性能开销很大。可以考虑在批量操作前关闭缓存操作完再开启或者批量操作后统一清理整个区域的缓存。4.5 缓存效果的监控与验证配好缓存不等于万事大吉你需要验证缓存是否真的生效了效果如何。开启缓存统计。大部分框架都支持缓存统计功能开启后可以查看命中次数、未命中次数、放入次数等指标property namehibernate.generate_statisticstrue/property然后在代码里获取统计信息Statistics stats sessionFactory.getStatistics(); long hitCount stats.getSecondLevelCacheHitCount(); long missCount stats.getSecondLevelCacheMissCount(); double hitRatio (double) hitCount / (hitCount missCount);命中率在 80% 以上算是不错低于 50% 就说明缓存策略有问题需要调整。日志验证。把缓存相关的日志级别调到 DEBUG可以看到每次查询是否命中了缓存。这是最直观的验证方式适合开发和调试阶段。性能对比测试。在开启缓存和关闭缓存两种情况下分别压测同一组接口对比响应时间和数据库 QPS。这是最有说服力的验证方式但需要在测试环境进行避免影响生产。5. 常见问题与排查技巧实录5.1 缓存不生效的排查思路配了二级缓存但发现查询还是走数据库这是最常见的问题。排查思路可以按这个顺序来第一步确认总开关开了没。检查配置文件里use_second_level_cache是不是 true。这个开关默认是 false很多人配了一堆东西但忘了开总开关。第二步确认实体加了缓存标注。不是所有实体都会自动缓存必须显式标注Cacheable和Cache。检查你的实体类上有没有这两个注解。第三步确认缓存实现依赖引入了。如果你选了分布式缓存作为后端但依赖没引入或者版本不兼容缓存区域工厂就创建不出来二级缓存自然不生效。检查依赖树确认缓存相关的 jar 包都在。第四步确认查询方式支持缓存。不是所有查询都会走二级缓存。比如原生 SQL 查询、某些特定的 HQL 查询可能不会命中缓存。尽量用框架标准的实体查询方式。第五步看日志。开启 DEBUG 日志观察缓存相关的输出。如果日志里完全没有缓存的记录说明缓存根本没被触发如果有记录但都是 miss说明缓存配置有问题。5.2 数据不一致的典型场景缓存和数据库不一致是二级缓存最危险的问题。以下是我遇到过的一些典型场景场景一原生 SQL 更新导致缓存未失效。你用原生 SQL 更新了数据库但框架不知道缓存里的旧数据还在。解决办法是手动清理缓存或者避免在缓存实体上使用原生 SQL。场景二集合缓存未随实体更新而失效。你更新了一个实体但包含该实体的集合缓存没有失效用户看到的列表还是旧的。解决办法是检查集合缓存的配置确保更新时集合缓存也会被清理。场景三多实例部署下的本地缓存不一致。每个应用实例都有自己的本地缓存实例 A 更新了数据实例 B 的缓存还是旧的。解决办法是改用分布式缓存或者配置缓存同步机制。场景四事务未提交导致缓存提前更新。有些配置下缓存会在事务提交前就更新如果事务回滚了缓存里的数据就是错的。解决办法是调整缓存写入时机确保事务提交后再写缓存。5.3 性能不升反降的原因分析有时候配了二级缓存性能反而下降了。这听起来反直觉但确实会发生。常见原因有缓存序列化开销大。分布式缓存需要把对象序列化成字节数组才能存进去读的时候还要反序列化。如果对象很大或者序列化方式效率低这个开销可能比查数据库还大。解决办法是优化序列化方式或者改用本地缓存。缓存命中率太低。如果缓存命中率只有 20%那 80% 的请求还是要查数据库而且还多了一次查缓存的开销。解决办法是分析访问模式调整缓存策略提高命中率。缓存淘汰太频繁。缓存容量设得太小数据刚放进去就被淘汰了等于白放。解决办法是增大缓存容量或者优化淘汰策略。网络延迟。分布式缓存在网络传输上有延迟如果缓存服务器和应用服务器之间的网络质量不好延迟可能很高。解决办法是优化网络拓扑或者改用本地缓存。5.4 常见问题速查表问题现象可能原因排查方法解决方案缓存完全不生效总开关未开检查配置文件设置 use_second_level_cache 为 true缓存完全不生效实体未标注检查实体类注解添加 Cacheable 和 Cache缓存完全不生效依赖缺失检查依赖树引入缓存实现依赖数据不一致原生 SQL 更新检查是否有原生 SQL手动清理缓存或避免原生 SQL数据不一致集合缓存未失效检查集合缓存配置配置集合缓存失效策略数据不一致多实例本地缓存检查部署架构改用分布式缓存性能下降序列化开销大分析序列化耗时优化序列化或改本地缓存性能下降命中率低查看缓存统计调整缓存策略性能下降淘汰频繁查看淘汰统计增大缓存容量内存溢出缓存容量过大监控内存使用减小缓存容量或设过期时间5.5 几个容易忽略的实操细节细节一缓存 key 的生成规则。不同框架生成缓存 key 的规则不一样有的用实体类名加主键有的还会加上租户 ID、版本号等。如果你手动操作缓存必须搞清楚 key 的生成规则否则可能清理错缓存或者清理不到。细节二懒加载和缓存的交互。懒加载的关联对象在缓存中的行为比较特殊可能不会像你预期的那样被缓存。如果发现关联查询没走缓存检查一下是不是懒加载的问题。细节三缓存预热。应用刚启动时缓存是空的所有请求都会打到数据库。如果启动瞬间流量很大数据库可能扛不住。可以在应用启动后主动加载热点数据到缓存这个过程叫缓存预热。细节四缓存降级。如果缓存服务挂了应用不能跟着挂。需要有一套降级机制缓存不可用时自动切换到直接查数据库保证核心功能可用。实操心得二级缓存不是银弹它解决的是特定场景下的性能问题。在决定用二级缓存之前先问自己三个问题数据是不是读多写少能不能接受短暂的数据不一致数据库是不是真的扛不住了如果答案都是肯定的那就上如果有任何一个是否定的先考虑其他优化手段比如加索引、优化 SQL、读写分离。6. 我个人的一些经验体会二级缓存这个东西刚接触的时候觉得挺简单不就是配个开关加个注解嘛。用得多了才发现里面的门道比想象中多得多。配置只是第一步真正难的是怎么保证数据一致性、怎么调优命中率、怎么在出问题的时候快速定位。我自己的经验是能不用二级缓存就不用。听起来有点反直觉但确实是这样。很多性能问题其实可以通过加索引、优化查询、读写分离来解决这些方案更简单、更可控。二级缓存引入的复杂度——一致性维护、失效管理、并发控制——在项目规模不大的时候收益远远抵不上成本。但如果你的项目确实到了需要二级缓存的阶段那就要认真对待。我的建议是从只读数据开始。先把那些永远不会变的字典表、配置表配上缓存跑一段时间看看效果积累经验。然后再逐步扩展到读写数据但一定要做好监控和告警一旦发现不一致的苗头立刻排查。还有一个教训是不要过度依赖框架的自动缓存。框架的自动缓存虽然方便但它的行为不一定完全符合你的预期。关键业务的数据最好手动控制缓存的读写和失效虽然代码多一点但心里踏实。最后说一个我踩过的大坑。有一次在一个多实例部署的项目里我用了本地缓存作为二级缓存的后端测试环境一切正常上线之后用户频繁反馈数据不对。排查了半天才发现两个实例的缓存数据不一致用户请求打到不同实例上看到的数据就不一样。后来改成分布式缓存才解决。这个坑让我明白了一个道理缓存方案的选择必须和部署架构匹配单机方案放到集群里用迟早要出事。如果你正在考虑引入二级缓存或者已经在用但遇到了一些问题希望这篇文章能给你一些参考。缓存这东西用好了是利器用不好就是给自己挖坑。多测试、多监控、多留一手降级方案总没错。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑