面试考场上,我见过太多人在JCache这道题上翻车了。前阵子帮团队面一个高级Java候选人,简历上写着“精通分布式缓存”,我问他:“JCache(JSR-107)定义了哪两种主要的缓存存储模式?”他很流畅地说:“LOCAL本地缓存和PARTITION分区缓存,一个存在本机,一个分散到多个节点。”听起来没毛病,但我接着问了一句:“那分区模式下,一个key请求进来,客户端怎么知道该找哪个节点?”他卡住了,支支吾吾半天说不出一致性哈希。
这就是典型的分层:背过概念,没理解机制。这道题表面上考的是两个英文名词,实际上考的是你对“缓存数据在分布式环境下怎么存、怎么找、怎么容错”这一整套逻辑的掌握程度。这篇文章我打算把LOCAL和PARTITIONED从规范定义到实现原理、从选型逻辑到面试回答框架,完整拆一遍,不管你是在准备面试还是正在设计缓存方案,都能直接拿去用。
1. 从面试翻车现场说起:Topology到底在考什么
1.1 JCache(JSR-107)解决的是Java缓存生态的碎片化问题
在JCache出现之前,Java里的缓存方案是各占山头的。Ehcache、Guava Cache、Caffeine、Hazelcast、Redis的客户端库,每个都有自己的API,用法千差万别。今天你基于Ehcache写了一堆缓存存取代码,明天项目组说要换Caffeine,你会发现缓存工具类的接口全得改,业务代码跟着遭殃。这种感觉就像换了数据库,DAO层全部重写一样痛苦。
JSR-107就是日本JCP组织在2014年发布的一套统一缓存规范,小名叫JCache。它的核心目标是把缓存API标准化:CachingProvider负责提供缓存实现,CacheManager管理缓存的生命周期,Cache就是你业务里最常用的那个键值对存储接口,再加上过期策略ExpiryPolicy、缓存加载器CacheLoader、缓存写入器CacheWriter和事件监听器CacheEntryListener,构成了一套完整的能力矩阵。
这套规范落地之后,你写业务代码时只需要依赖javax.cache的标准接口,至于底层是Ehcache 3还是Hazelcast还是Infinispan,对代码来说是透明的。Spring Cache的注解抽象之所以能和JCache互通,也正是因为底层接了这套标准API。理解了这层背景,你才能明白Topology这个话题不是孤立存在的,它是Cache配置体系中描述“数据如何分布”的一个关键维度。
1.2 Topology是Cache配置体系中的一个维度
JCache里的Cache不是一个裸的Map,它要回答的问题很多:数据存哪里、存多久、满了怎么办、要不要回写外部存储、条目变化要不要通知业务方。这一堆问题汇总起来就是Cache的Configuration,主要包含CacheLoader、CacheWriter、ExpiryPolicy、CacheEntryListener等配置项,其中还有一个容易被忽略的议题——Topology(拓扑)。
Topology在规范里描述的是一个核心问题:当一个应用部署成多节点集群时,你创建的缓存数据,到底在节点之间怎么摆?是每个节点各存各的,还是大家把数据切开来分着存?这两种摆法,就是规范里定义的LOCAL和PARTITIONED两种拓扑模式。
这里有个细节我建议你记住:规范原文里写的第二种模式叫PARTITIONED,但很多面试题、二手资料里会简写成PARTITION,甚至你在面试时口误说成PARTITIONED,面试官也不会纠正你。但如果你能主动说一句“题目里的PARTITION,对应规范里完整写法应该是PARTITIONED”,这在面试官那里的加分效果非常明显,说明你读的是原始规范,不是考前背的八股。
1.3 两种模式的规范定义和第一印象
规范里对两种拓扑的原文描述,翻译过来很好懂:
- LOCAL(本地模式):缓存条目只存储在当前JVM节点内部,不做任何跨节点的数据分发和共享。节点A写入的数据,节点B是看不到的,因为B根本没有这份数据。
- PARTITIONED(分区模式):缓存条目按照key的哈希结果,分散存储到集群里的多个节点上。每个节点只持有全量数据的一个子集,没有任何一个节点能独立拥有全部数据。
这俩定义看着就两句话,但往深了挖,它们背后的数据访问路径、一致性模型、故障恢复机制是完全不同的。面试官考这道题,真正想听的就是这一层。接下来我分别把两种模式拆开,讲清楚它们到底是怎么运作的。
2. LOCAL模式拆解:单机缓存为什么也能撑起生产环境
2.1 LOCAL模式的本质:数据边界就在当前JVM
LOCAL模式,从实现上看,就是在使用缓存的JVM内部维护一个键值对存储区,数据不会发送到网络,也不会被其他节点感知。你在这个节点执行cache.put("order:1001", orderData),这条数据就安安稳稳地躺在这个节点的内存里,其他节点的cache.get("order:1001")永远返回null,除非它们自己也执行了一次put。
正是因为这个特性,LOCAL模式的Cache和数据源、业务进程是强绑定的。JVM一重启,缓存数据全部清空,然后通过CacheLoader从数据库等外部存储里重新加载。这种“简单粗暴”的模式,恰恰是很多生产环境的主力缓存形态。
很多初学者会低估LOCAL模式,觉得它就是个高级点的HashMap。这么理解会吃大亏。LOCAL模式的价值在于访问路径极短:一个get请求从代码发起,到JVM内部定位到缓存条目,全程是纯内存操作,没有序列化,没有网络IO,没有跨进程通信。对响应时间极度敏感的场景,比如商品详情页热点数据、用户会话信息,LOCAL模式能给你稳定到纳秒到微秒级的访问延迟。
2.2 LOCAL模式的数据一致性模型
LOCAL模式的一致性,只能在“单节点内部”保证。同一个JVM内的缓存,put之后立即get,肯定是你刚写入的数据。但放在集群视角看,LOCAL模式天然是“各节点各说各话”的。节点A更新了一条数据,节点B的缓存里还是旧值,直到B的过期策略触发,或者B自己重新加载。
这个特性决定了它的适用边界:适合一致性要求不高的读多写少场景,或者数据本身就是节点内私有、不需要共享的场景。比如你一个后台管理系统,每个服务实例只处理自己分到的那部分任务,任务状态放LOCAL缓存里完全合理。再比如一个计算型服务,中间结果本来就是节点内临时用的,没必要同步到集群。
反过来,如果你用LOCAL模式缓存了一份全局配置,而且大部分节点都依赖这份配置做实时判断,那就要小心了:一个节点更新了配置,其他节点短时间拿不到最新值,在配置切换的窗口期里可能出现行为不一致。这种情况我见过不止一次,解决方案倒也不复杂,要么缩短本地缓存的过期时间,要么引入一个分布式缓存作为配置的统一来源。
2.3 LOCAL模式的生产级配置要点
LOCAL模式虽然简单,但真要在生产环境里用好,细节还挺多。
- 过期策略别乱拍脑袋:
ExpiryPolicy有三种典型选项,Created从创建时间算过期,Modified从最后修改时间算,Accessed从最后访问时间算。如果你缓存的是热点商品数据,Accessed策略能让热数据一直在缓存里活着,冷数据慢慢被淘汰,命中率会高很多。 - 容量上限要提前定:JCache标准API里有
CacheConfiguration的容量设置项,具体到实现,Ehcache、Hazelcast都有自己的eviction策略配置。最简单常用的淘汰算法是LRU(最近最少使用),优先淘汰最久没被访问的条目。容量设太小,缓存会频繁驱逐;设太大,内存风险就上来了,要做压测。 - 加载器别做慢操作:
CacheLoader是在缓存miss时被调用的。如果在load方法里做慢SQL查询,一次缓存穿透就可能拖垮接口。生产经验是:CacheLoader里只放毫秒级的快查询,慢查询放到异步任务里预加载,或者等业务层做多级兜底。
可以对比一下,市面上常见的本地缓存组件,在LOCAL语义上其实都类似:
| 维度 | 标准JCache(LOCAL) | Caffeine/Guava Cache(对比参考) |
|---|---|---|
| 数据分布 | 当前JVM节点内部 | 当前JVM节点内部 |
| 访问延迟 | 纯内存,极快 | 纯内存,极快 |
| 集群感知 | 无 | 无 |
| 淘汰机制 | 可通过实现配置 | 自带LRU/LFU等 |
| 持久化 | 通常不支持 | 通常不支持 |
从使用角度讲,LOCAL模式的价值不在于“跟别人比谁强”,而在于它是一种确定性的快速通道。它不需要考虑“数据在哪个节点”“网络超时怎么办”这些复杂度,把缓存当成进程内的私有空间来用就好。
2.4 面试官追问LOCAL时的高频陷阱
面试官考LOCAL模式,最爱往下追的坑基本集中在两个点上。
第一个是“那各节点的数据不一致怎么办”,这是分布式架构里绕不开的问题。你要能给出对应的答案逻辑:先从场景角度判断是否需要强一致,大多数本地缓存场景允许秒级延迟,比如商品信息、配置信息,这些都是最终一致就行。如果业务真的要求强一致,那压根不应该用LOCAL模式,该用分布式缓存或者加版本号校验。
第二个是“缓存穿透和缓存击穿”,面试官问你LOCAL模式会不会遇到缓存穿透,你要能说清楚:缓存穿透是指查询一个不存在的key,每次都要打到数据库;缓存击穿是指某热点key在过期瞬间被大量请求同时打到DB。解决方案也成体系:穿透可以用空值缓存,击穿可以用互斥锁防止并发重建缓存,热点数据可以设置永不淘汰加定时刷新。能说到这里,面试官对你在LOCAL模式上的理解就已经很满意了。
3. PARTITIONED模式拆解:缓存数据是怎么被拆到多个节点的
3.1 PARTITIONED模式的本质:全量数据被切片存放
PARTITIONED模式和LOCAL模式最大的不同,在于数据的所有权分配。在PARTITIONED模式下,集群中没有任何一个节点保存全量缓存数据,每份数据归属于某个特定节点。你可以类比银行的分行制度:总行数据分散存储在各分行,你办业务不需要跑遍所有分行,只要去管你那个账户的分行就行。
这样做的好处非常直观:总容量可以水平扩展。单节点内存撑不住大数据量,那就加节点,每个节点分担一部分数据。100GB的缓存数据,10个节点,每个节点只需要10GB,存储压力被摊开了。这就是为什么大数据量缓存场景通常不会用LOCAL模式的原因——你的单机内存再大也有限,而且性价比很低。
3.2 分区路由算法:key是怎么找到它的归属节点的
PARTITIONED模式最核心的机制,就是key到节点的路由算法。这个算法决定了get、put操作到底该发往哪个节点,是分布式缓存的灵魂。
绝大多数的实现(包括Hazelcast的JCache分区机制),都采用了一致性哈希(Consistent Hashing)的变种。一致性哈希的基本思路是:把key计算hash值,映射到一个固定范围的哈希环上;同时每个节点(物理节点)在环上占据一个或多个位置(虚拟节点)。查找key归属时,从key的哈希位置出发,在环上顺时针找到第一个节点,数据就归那个节点管。
为什么用一致性哈希而不是简单取模?简单取模hash(key) % N也能分数据,但节点数量从N变成N+1时,绝大多数key的归属都会变化,意味着几乎全量数据要迁移,生产上这是灾难。一致性哈希则不同,新增或下线一个节点,只有环上邻近的一小部分key需要迁移到新节点,大部分数据原地不动。这种最小化rehash的特性,让集群扩缩容变成了一个轻量操作。
你可以在回答里补充一句虚拟节点的意义:物理节点少的时候,哈希分布可能不均匀,某些节点数据特别多,给每个物理节点配置多个虚拟节点,让它们在哈希环上分散开,可以让数据分布更均衡。这一点一说出来,面试官就知道你真的懂分区。
3.3 一次get请求在PARTITIONED模式下的完整旅程
理解PARTITIONED模式,最好的方法是走一遍读写链路。
假设一个3节点的集群,客户端发起cache.get("order:1001"):
- 客户端对key计算哈希,比如
hash("order:1001") = 19528437。 - 客户端通过一致性哈希环,定位到这个哈希值归属于节点B。
- 如果客户端本身就在节点B上,直接本地内存读取;如果不在,客户端通过网络向节点B发送get请求。
- 节点B收到请求后,在其本地存储中查找对应条目,返回给客户端。
- 如果节点B上没有该条目,则缓存miss,接下来按照配置的
CacheLoader去数据源加载,或者返回null给业务方。
这整个过程里有两个关键点值得在面试中主动说出来。
第一,读写都需要网络跳转(除非key恰好归属本地节点),因此PARTITIONED模式的访问延迟比LOCAL模式高一个数量级,这是分布式存储的固有代价。第二,客户端是感知数据分布的,也就是常说的“智能客户端”。它知道每个key该找哪个节点,所以不需要中间代理层转发。如果面试官继续追问,你还可以补充:这种机制和Redis Cluster的客户端路由如出一辙,底层都是“客户端算位置—直接连接目标节点—返回结果”的路径,只不过Redis Cluster拿出slot槽位的概念,每个槽位映射到具体节点,逻辑上更工程化。
3.4 节点故障和数据安全
PARTITIONED模式带来的一个绕不开的问题:节点挂了,它管的那部分数据就没了。怎么解决?答案是冗余副本。
JCache规范本身没有强制规定PARTITIONED必须配副本,但主流实现(如Hazelcast的JCache、Infinispan)都支持配置备份数量backup-count。默认情况下,一份主数据可以有1个或多个备份副本,分散存放在其他节点上。当主节点宕机,集群会从备份中提升一个新的主节点出来,客户端的路由表也随之更新,整个服务不受影响。
你在面试时如果能把这个容错机制讲出来,就比单纯说“PARTITIONED是分片”的人高一个段位。要记住的要点是:副本数越多,容错能力越强,但写放大和存储成本也越高。一个备份副本意味着每份数据写两次、空间占用翻倍。生产上通常设置1个备份副本就够,除非你的缓存数据像交易流水一样完全不能丢。
3.5 PARTITIONED模式和Redis Cluster的关系
很多候选人以为PARTITIONED是JCache独创的概念,其实它和Redis Cluster的分片思想高度相似。你完全可以在面试中做个类比:Redis Cluster把整个键空间划分成16384个哈希槽,每个节点负责一部分槽位,key通过CRC16计算落到某个槽,再由槽定位到节点;JCache的PARTITIONED模式也是把key集合切分成多个分区,每个节点负责一部分分区。差别在于,Redis Cluster是一个具体产品,JCache是一个标准抽象,标准本身不规定实现细节,所以不同Provider的PARTITIONED性能差异会很大。
这个类比的好处在于,它能把一个JCache的小概念,连接到你已经熟悉的Redis知识体系里,让你回答问题时有更多延伸空间。面试官听你提到Redis Cluster,大概率会顺着问你对Redis Cluster分片的理解,这是一个很好的引导性话题,前提是你真的能把两边讲清楚。
4. LOCAL和PARTITIONED怎么选:从架构视角看取舍
4.1 先看数据量与单机容量
最直接的判断标准就是数据量。你的缓存数据总规模如果只有几百MB到1GB级别,单机内存完全放得下,LOCAL模式是最优解。它没有网络开销,没有分布式的复杂度,出现问题的概率最小。一旦数据量到了几十GB甚至上百GB,单机内存放不下,或者放得下但成本太高,PARTITIONED模式就变成刚需了。
很多项目的问题在于,明明数据量只有几百MB,却强行上了分布式缓存。结果缓存服务集群加了一堆,网络开销和运维成本全上去了,收益却微乎其微。这就是典型的架构过度设计。
4.2 再看一致性与性能要求的矛盾
LOCAL模式访问快,但集群内数据的一致性差;PARTITIONED模式能享受大容量和水平扩展,但每次访问多一次网络跳转。这是个绕不开的权衡。
- 如果业务允许缓存数据在不同节点上短暂不一致,比如商品详情、用户昵称、资讯列表,LOCAL很合适。
- 如果是分布式锁、库存计数、全局唯一数据这类强一致场景,LOCAL模式压根不能用,PARTITIONED模式配上写后失效或同步更新机制才有机会满足。
这里我要多说一句:即使是PARTITIONED模式,也很难做到跨节点强一致,它更像是一个性能驱动的最终一致系统。真正要求强一致的缓存,应该在业务层面加版本号、校验和或者直接用数据库本身的机制来兜底。缓存的定位从来都是“加速”,不是“记账本”。
4.3 组合使用:多级缓存才是生产常态
实际生产里,LOCAL和PARTITIONED不是互斥的,它们经常组合成多级缓存架构。这个组合逻辑很清晰:
- 请求进来,先查本节点的LOCAL缓存。命中就直接返回,这个路径最快。
- LOCAL缓存miss了,再查分布式PARTITIONED缓存(比如JCache的分区模式或者Redis Cluster)。
- 分布式缓存也miss,才走
CacheLoader去查数据库,查完之后回填两级缓存。
这种架构下,LOCAL缓存通常只放热点数据,命中率可以做得非常高。MIT的论文里有过一个经典数据:哪怕只有很短的本地过期时间,热点数据的本地命中率都能达到80%以上。剩下的20%落到分布式缓存,数据库的压力就非常小了。
我在做电商促销活动的时候就是这么干的。秒杀时段,大量请求都集中在几十个SKU上,本地缓存把这几十个SKU的热度发挥到极致,分布式缓存兜底剩余的非热点查询。数据库几乎无感,整个系统稳如老狗。
4.4 两种模式的完整对比
| 对比维度 | LOCAL模式 | PARTITIONED模式 |
|---|---|---|
| 数据存储位置 | 当前JVM节点内 | 按key哈希分布到集群各节点 |
| 数据总容量 | 受单节点内存限制 | 可随节点数水平扩展 |
| 访问延迟 | 纯内存,极快 | 可能跨节点网络访问,延迟较高 |
| 数据一致性 | 集群内无共享,天然不一致 | 有副本机制,仍需考虑一致策略 |
| 节点故障影响 | 只影响本节点缓存 | 需要副本提升,仍可能短暂不可用 |
| 复杂度 | 低,简单易用 | 高,需处理路由、迁移、容错 |
| 适用场景 | 数据量小、性能敏感、一致性要求低 | 数据量大、集群部署、需要水平扩展 |
这个表格建议你收藏。面试时如果被问到选型,能流利地把这几个维度讲出来,比单纯背定义有说服力得多。
5. 面试官问这道题时,到底想听到什么样的回答
5.1 这道题背后的三个考察层次
我作为面试官,问JCache拓扑这道题,从来不是想听候选人背出两个名词,而是在看三个层次:
- 第一层:知不知道。听说过LOCAL和PARTITIONED,知道一个是本地、一个是分区。这一层只能说明你有基本的概念面。
- 第二层:懂不懂原理。知道PARTITIONED的key是怎么路由的,理解一致性和容量之间的关系,能说清楚两种模式各自适合什么场景。这一层的人,至少是对分布式缓存有实践或深入研究过的。
- 第三层:能不能落地。能主动说出多级缓存组合、分区副本容错、本地缓存的和分布式缓存的一致性代价。这一层的人,是真的设计过生产级缓存方案。
你可以自己对照一下,现在能到第几层。这篇文章讲完,我希望你能摸到第三层的边缘。
5.2 一个能扛住追问的现场回答框架
面试时如果真的被问到这道题,建议按这个顺序组织回答,既简洁又有层次:
- 一句话定义:JCache定义了LOCAL和PARTITIONED两种缓存存储模式,前者数据只存当前JVM,后者数据按key哈希分布到多个节点。
- 展开对比:LOCAL访问快、实现简单,但容量受单机限制,集群内数据各自独立;PARTITIONED容量可水平扩展,但访问可能需要网络跳转,需要处理节点容错和数据迁移。
- 补一个关键细节:PARTITIONED通常用一致性哈希做路由,节点增减时rehash成本很小,配合副本来保证节点故障后数据不丢。
- 联系项目场景:说一个你实际用过的组合,比如“我当时先在本地点缓存放热点,再配合分区缓存兜底冷数据”。
这个回答框架最大的优点,是给了面试官至少三个可以追问的方向,而每个方向你自己都已经准备好了。当你能掌控追问节奏的时候,面试的主导权就在你手上了。
5.3 高频追问清单和应对思路
面试官听完你的回答后,大概率会顺着往下追。我列几个高频追问,你们可以提前准备好:
- 追问1:你说PARTITIONED用了一致性哈希,那节点扩容时具体会发生什么?应对思路:说清楚先加虚拟节点、迁移数据、最终一致地对外提供服务。强调只有邻近一小部分key需要迁移,大部分数据不动。
- 追问2:LOCAL缓存和数据库之间的数据一致性怎么保证?应对思路:配合过期时间和主动失效,允许短时间不一致。如果业务对一致性敏感,就用版本号或者干脆不用本地缓存。
- 追问3:PARTITIONED模式下,单点写入了数据,但还没同步到副本,这时候主节点挂了怎么办?应对思路:这就是数据丢失的窗口。生产上通常用同步复制来缩小这个窗口,或者接受极小的概率,在业务上做兜底。能说出“同步复制 vs 异步复制”的权衡,就是一次漂亮的加分回答。
- 追问4:JCache标准API怎么配置这两种模式?应对思路:标准API本身不直接暴露Topology配置项,它是通过具体Provider的配置来定义的。比如Hazelcast的JCache在配置分布式Cache时,默认就是分区存储,而本地Cache也可以通过配置关闭分布式特性。能说出“规范定义语义,Provider提供配置”这句话,说明你是真懂。
6. 聊点我的真实感受
在分布式缓存这个领域,LOCAL和PARTITIONED是个绕不开的话题,但它绝不是两个名词那么简单。LOCAL模式教会我的是“能用本地解决的,别引入网络”,很多系统性能问题不是缓存不够好,而是用错了缓存形态;PARTITIONED模式教会我的是“没有免费的午餐”,数据分散了,容量上去了,但你得为路由、迁移、容错付出运维代价。
我常跟团队里的人说,考面试题不是目的,借一道题把一个技术领域吃透才是目的。这道JCache的题目背后,站着整个分布式缓存的知识体系。你把LOCAL和PARTITIONED搞明白,再看Redis Cluster、一致性哈希、多级缓存这些概念,就会有一种“原来它们都是通的”的感觉。
如果你正在准备面试,建议别只背答案,自己动手把这两种模式写在代码里,跑起来看看:在本地缓存里put几个key,再在分区模式下put同样的key,观察数据落在哪个节点、访问跨不跨网络。纸上得来终觉浅,这句话在技术上真的适用。毕竟面试官也只能从你的话里判断你有没有动手经验,而你若真有动手经验,说出来的每句话都会自带一种笃定感。