首页 / 资讯中心 / 文章详情

Hibernate二级缓存Provider详解:从原理到选型与排坑

Hibernate二级缓存Provider详解:从原理到选型与排坑 ★ FEATURED ARTICLE
被人突然问起“Hibernate的caching provider是什么”我第一反应是能问出这个词的人多半已经看到配置文件里那一堆以hibernate.cache开头的键但又没搞懂这些键到底在切换什么。这篇文章就把这个词拆开讲清楚重点解决三件事Provider和缓存本身有什么区别、Hibernate为什么非要加这个角色、以及真去接一个Provider或换一个Provider时会遇到的坑。适合正在做JPA项目、每天被二级缓存配置绕晕的同学也对刚接手老项目的人有帮助。先说结论Caching Provider可以理解为“二级缓存区域的管家”。它在Hibernate和缓存容器之间多垫了一层负责创建缓存区域、读写缓存项、失效旧数据、统计命中率。你配置的region.factory_class就是在选这个管家是谁而真正存数据的是另一个东西。很多人把这两者混成同一个概念后面才会出现“配置了半天缓存还是没生效”的情况。1. 先纠正一个常见误区Provider是缓存的管家不是缓存本身很多人在一个地方拐了弯以为加了某个缓存库的依赖就等于把Hibernate二级缓存打开了。其实在Hibernate眼中那个真正容纳数据的容器和那个帮你管理容器的角色是完全分开的。Provider就是后者。它负责的事情包括初始化缓存Region、决定缓存条目怎么淘汰、什么时候刷新、SessionFactory关闭时把数据清干净。Hibernate本身只跟Provider打交道不关心背后用的是哪种容器。打个比方缓存是仓库Provider是仓库管理员。你的代码会向管理员喊“我要取订单号123的数据”管理员就去某个货架上翻。货架本身可以换成堆内存、磁盘文件、远程缓存服务但管理员这个角色不用换——这就是Provider被拆出来的好处。Hibernate定义了一套接口要求管理员必须会做某些事剩下的管理风格它全不管。Hibernate给这个角色定义了一套接口。早期版本里叫CacheProvider后来演变成RegionFactory。你现在最常见的配置项就是这个hibernate.cache.region.factory_classjcache这行配置的真正意思是我要用符合JCache标准的某个Provider来当管理员。至于它背后是本地缓存还是远程缓存由你另外引入的依赖决定。所以“配了Provider”和“缓存了一个实体”是两码事。前者只是把管理员请到岗后者还需要你告诉Hibernate哪些实体可以被管理以及到底怎么管。还有一个常见的混淆点很多人以为Provider就是“缓存实现”于是到处搜“缓存没生效是不是因为没选对Provider”。实际上只要配置能正常加载Provider通常已经就位。真正让缓存不生效的往往是后面几条实体没加注解、查询没走缓存路径、或者并发策略选错。这些后面专门用一节来说。注意别一上来就在集群环境里选最复杂的Provider。先搞清楚Hibernate把Provider这个角色拆出来是为了解耦不是为了让你跟缓存方案死磕。2. 为什么Hibernate不自己管理缓存非要请一个Provider有人会问Hibernate连SQL都能生成为什么缓存不顺便自己做掉答案藏在一级缓存和二级缓存的区别里。一级缓存就是Session级别的那份数据没有它Hibernate连基本工作流都跑不起来这属于ORM自身的基本功。而二级缓存通常是跨Session、跨事务甚至跨进程的它面对的是淘汰策略、序列化、并发读写、集群同步这类问题这些不是“生成SQL”这一个思路能解决的。把二级缓存单独做成扩展点Hibernate就能保持核心精简。历史上Hibernate确实自己写了多个对接模块分别接不同的缓存库。每个方案有自己的一套配置和版本绑定维护起来非常累。后来社区把方向收敛到JCacheJSR-107这个规范上。Hibernate只需要写一套JCache适配代码各个缓存框架只要实现规范就能被接进来。这就是为什么现在很多新项目里factory_class直接写jcache。可以理解成Hibernate对Provider提了一份“接口清单”清单内容包括创建缓存区域、读写缓存项、清空区域、判断某个key是否命中、统计命中次数。谁把这些方法实现了谁就能当这个管理员。你的代码不需要知道管理员是本地人还是远程员工只需要把活儿交给它。这个角色带来的解耦价值很实际你在开发机器上用轻量缓存部署到多台服务器时换成支持分布式的组件Hibernate代码基本不用动只要跟进配置和依赖。为什么说“基本”因为真正涉及分布式缓存时序列化、网络开销、数据一致性这些事还是会反过来影响你的设计后面第五节会展开。3. 常见Provider之间差在哪按什么逻辑选市面上的“Hibernate Caching Provider”并不是同一个层面的东西。有的本质是进程内缓存库例如Ehcache、Caffeine有的是独立服务例如Redis也有的既能嵌入应用也能组成集群例如Hazelcast、Infinispan。把它们摆在同一个表里看选型思路会更清楚。Provider默认形态数据存放位置是否需独立服务适合场景Ehcache进程内堆内、堆外、磁盘通常不需要单实例应用、快速接入Caffeine进程内库堆内为主不需要高并发本地热点缓存Redis远程缓存独立服务内存需要多实例共享缓存、已有运维体系Hazelcast分布式数据网格多个JVM内存可嵌入或独立JCache标准、集群共享Infinispan分布式缓存节点内存可持久化可嵌入或独立强一致性、事务型缓存需求这个表不是让你挑功能最强的。我见过有项目给一个单机小后台接上Redis做二级缓存结果每次查询都多一次网络往返命中率又不高最后反而变慢。选择Provider之前第一个问题永远是你到底有几个实例在跑如果一直是单实例本地进程内缓存就够了Ehcache这类方案完全能覆盖不需要引入额外服务。第二个问题是这个缓存数据能不能接受一点点延迟不一致大多数业务能。多实例部署时如果只是担心A节点更新后B节点读到旧值可以先靠缩短TTL来缓解。真正扛不住再上分布式Provider也不迟。第三个问题是谁来运维。加了Redis这类组件你的团队就多了一个必须监控的服务。如果运维能力不富裕选一个能嵌进应用里的分布式缓存组件部署心智负担会小很多。还有一句话我在不同项目里反复说不要把所有实体都无脑加缓存注解。缓存是给读多写少的数据准备的。一张表每分钟被UPDATE很多次缓存它只会增加无效失效和一致性风险。4. 从零接入一个本地Provider照着做就能跑通下面用Ehcache配JCache这个组合走一遍因为它是目前单机场景里最省事的路线。假设你已经有一个能正常启动的Hibernate项目。4.1 依赖先加对版本是第一个坑Maven坐标大体是这样的dependency groupIdorg.hibernate.orm/groupId artifactIdhibernate-jcache/artifactId version和你当前Hibernate版本一致/version /dependency dependency groupIdorg.ehcache/groupId artifactIdehcache/artifactId version用3.x版本/version /dependency很多翻车现场都是因为jcache桥接模块和底层缓存库的版本跟Hibernate主版本不匹配结果启动时类找不到或方法签名对不上。先确认你用的Hibernate主版本再去找对应版本的桥接模块。别对着最新版本号硬套那是最容易踩的坑。4.2 核心配置和实体注解把下面的配置加到hibernate.properties或对应的环境里hibernate.cache.region.factory_classjcache hibernate.cache.use_second_level_cachetrue hibernate.cache.use_query_cachetrue hibernate.generate_statisticstrueuse_second_level_cache这个开关在新版本里默认就是开着的但如果不写Factory ClassHibernate找不到管理员开也没用。use_query_cache是给查询结果列表用的它和实体缓存是两个东西后面会专门说。实体上加注解Entity Cache(usage CacheConcurrencyStrategy.READ_WRITE) public class MyEntity { Id private Long id; // 其他字段和方法 }二级缓存只对实体、集合等特定加载路径生效。加上注解后通过session.get()或session.load()按ID加载这个实体时Hibernate会先去Provider里找找不到才发SQL查库并回填。如果不加这个注解配置再多也是白搭。这也解释了为什么很多人说“我开了二级缓存却没效果”——因为实体没告诉Hibernate“我可以被缓存”。4.3 给缓存容器立规矩Ehcache 3可以通过XML声明缓存模板。把下面的文件放在classpath下文件名例如ehcache.xmlconfig xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xmlnshttp://www.ehcache.org/v3 xmlns:jsr107http://www.ehcache.org/v3/jsr107 cache aliasdefault expiry ttl unitminutes10/ttl /expiry heap unitentries10000/heap /cache /config这里的default是兜底模板。Hibernate启动时会按实体的Region名字去创建缓存空间如果XML里没有对应的alias就落到default模板里。10分钟TTL是安全值生产环境不要贪心设成永不过期。实体类没有实现Serializable时单机纯堆内缓存还能凑合一旦要往堆外或分布式走序列化几乎是强制要求否则等真正交换数据时就会抛异常。4.4 怎么确认它真的生效了把hibernate.generate_statistics打开然后对同一个ID调用两次查询。第一次会看到一条select第二次什么都没有直接命中缓存。你还可以通过日志或统计接口观察Second level cache hit count看命中率有没有上来。如果第二次仍旧打印SQL就按后面第6节的链路去查。多数情况是实体没加注解或者查询根本没走实体缓存路径。5. 单实例没问题一上集群就翻车真的该换分布式Provider了吗本地Provider部署到多实例时会遇到一个典型症状用户刚在A实例提交修改下一次请求被负载均衡转发到B实例B实例还给出旧数据。因为B实例的本地缓存里存着旧版本A实例的更新动作它完全不知道。这时候很多人第一反应是换分布式缓存。但换之前可以先把问题排一排。如果项目刚上线并发其实不高最省事的方案是给本地缓存设置一个较短的过期时间比如30秒到1分钟。代价是每个实例可能短暂读到旧数据但很多业务根本感知不到。等真的到了命中率很高、缩短TTL都会让数据库有压力的时候再上分布式Provider也来得及。这个顺序可以避免过早引入复杂度。真正换Redis这类组件时角色变了Provider从“进程内管家”变成“远程服务客户端”。Hibernate还是同一套配置口子但数据要序列化后通过网络传输缓存读写时间会从微秒级跳到毫秒级。如果缓存的数据总是被频繁全量更新序列化和网络开销反而比查库更大那就是本末倒置。要真想上分布式先确认这些数据确实具备“读多写少”、可以被序列化、且能容忍网络故障这些前提。还有一个绕不开的问题数据库事务和缓存更新做不到原子。Hibernate在事务提交后会把受影响区域的缓存失效掉但这只对“通过Hibernate写入”的数据有效。如果有别的应用直接改数据库Hibernate根本不知道Region里的数据就成了脏数据。应对办法通常是接受一个短TTL或者在业务侧主动调用失效接口不要指望自动解决。分布式Provider能解决“多实例各有一份缓存”的问题但它解决不了“外部系统绕过ORM改数据”的问题。所以我的判断标准是单实例或少量实例先用本地Provider加短TTL压住确认有真实共享缓存和高命中率需求后再考虑分布式Provider。如果上了分布式Provider优先选JCache标准兼容的实现这能让Hibernate配置改动最小。6. 缓存像是没生效我按这个顺序排查这类问题几乎每周都有人在社区里问。我自己排查的顺序基本固定你可以照着走一遍。6.1 现象一SQL每次都打印缓存完全没命中先确认三件事。第一region.factory_class是否真的被加载。配置错误会直接启动失败启动能过说明配置路径基本没问题。第二实体上有没有Cache注解。第三你是不是在用HQL/JPQL查询而不是按ID加载。二级缓存默认缓存的是“按ID加载”这条路。你写session.get(User.class, 1)它会先查缓存你写from User返回的是查询结果集合默认不进实体二级缓存。要让查询结果也走缓存得把use_query_cache打开并在代码里对查询设置setCacheable(true)。这个细节是命中率低迷的头号原因。很多人只开了配置没开查询缓存也没给实体加注解等于管理员到位了但货架上根本放不了东西。6.2 现象二缓存命中率很低或者查询缓存区域爆炸查询缓存是按JPQL字符串和参数组合来生成key的。如果你动态拼接出几十种不同查询字符串缓存条目会迅速膨胀命中率反而低。解决办法是想办法减少查询变体让同一类查询尽量复用同一条语句。比如把可变部分作为参数传而不是拼进语句里。另外对一个实体做查询缓存通常还需要把关联的实体和集合关系一起缓存好。否则会出现经典的“缓存了ID列表每次又去单独查实体”的情况表面看起来开了缓存实际数据库查询一点没少。6.3 现象三更新数据后缓存还是旧值这时候先看并发策略。Cache注解里的usage如果写成READ_ONLY而你又允许修改实体那本质上是在赌没人会改。可在写实体上用READ_WRITE或NONSTRICT_READ_WRITE并保证缓存容器支持相应的锁机制。再检查一下是否有另一个程序绕过Hibernate直接改了数据库。这种旁路更新是任何ORM都不可能自动感知的最短TTL反而是最实用的兜底。如果数据更新频率本来就高频繁失效会让缓存命中率崩掉。那说明这个对象根本不适合做二级缓存把它从缓存策略里摘掉才是正解。不要为了用缓存而用缓存。6.4 现象四启动时报ClassNotFound或者Provider行为诡异绝大部分是版本冲突尤其是jcache桥接模块和底层缓存库的版本不一致。遇到这种问题先别急着改代码把依赖树打出来看一遍再对照Hibernate主版本去找匹配组合。我见过一个项目Hibernate是某个5.x版本却引入了针对新版本设计的jcache模块启动报错信息又很隐晦排查过程浪费了不少时间。现象可能原因验证手段SQL每次重新打印实体没加Cache / 查询没走查询缓存看日志中第二次相同查询是否还有select命中率很低查询参数组合太多 / 缓存空间被撑爆打开statistics看miss与hit数量更新后旧数据并发策略错误 / 旁路更新审查Cache usage确认是否有程序直写库启动时类找不到模块版本不匹配检查依赖树对照Hibernate版本最后分享我现在的习惯新项目默认不开二级缓存除非查库次数明显成为瓶颈。真要用先把本地Provider接上打开统计信息跑一段时间看命中率和失效原因再决定要不要往分布式组件迁移。多数项目最后停在“本地Provider加短TTL”就够了。少花时间调缓存多花时间看清热点数据长什么样这才是这个机制真正想帮我们做的事。
阅读完成 · 觉得有帮助?
咨询建站