欢迎光临
我们一直在努力

缓存组件设计复盘:从硬编码到带模式参数的缓存进化

缓存组件设计复盘:从硬编码到带模式参数的缓存进化

电子面单对接实录 · 技术深潜 | 前情:《Token刷新与缓存失效:一对被忽视的上下游》

新架构里有一个缓存组件,专门缓存各平台的Token配置,避免每次取号都查数据库。最初的设计很简单:平台编码加组织ID做Key,查不到就查库,查到就缓存。

跑了一段时间后,发现一个隐患:同一个平台的不同业务模式,缓存会互相覆盖。

这篇文章完整复盘这个缓存组件的设计演进过程——从最初的两层缓存结构,到新增六个方法、提取公共查询逻辑、处理JDK版本兼容,以及最重要的:给缓存Key加一个模式后缀。


01 初始设计:两层缓存,按平台加组织路由

缓存组件的最初版本,结构清晰:

两层缓存:

  • 平台应用配置缓存:Key = 平台编码,存平台基础配置
  • 平台Token配置缓存:Key = 平台编码 + 分隔符 + 组织ID,存Token的数据库记录

查询逻辑:先从缓存取,缓存未命中就查数据库,查到后放入缓存。支持手动清除指定缓存,支持定时全量刷新。

平台差异处理:大部分平台通过通用Token配置表查Token,但有一个社交电商平台的Token存在专门的授权表里,需要特殊处理——直接跳过通用查询,走白名单通道。

这个设计跑了一段时间,没问题。直到有一天,我在梳理Token刷新逻辑时,发现了一个隐患。


02 问题发现:两种业务模式在抢同一个缓存Key

系统里有一个短视频电商平台,它有两种业务模式:普通商家模式和代发模式。两种模式用同一个平台编码,但Token存在不同的字段——普通Token和代发Token是两套独立的凭证。

问题来了:缓存Key是 平台编码_组织ID,两种模式的Key完全一样。

普通取号:缓存Key = "PLAT_A_123" → 查到的是普通Token
代发取号:缓存Key = "PLAT_A_123" → 缓存命中,返回的还是普通Token(应该是代发Token!)

先刷新普通Token再刷代发Token,或者反过来,缓存会被互相覆盖。 当前没出问题,是因为两种模式的刷新在同一个定时任务里,几乎同时更新,缓存清空后重新加载的值恰好是正确的。但未来如果把普通和代发的刷新任务分离(比如普通每小时刷一次,代发每两小时刷一次),就会出现缓存加载到过期Token的情况。


03 解决方案:给缓存Key加一个模式后缀

最简单的方案:在缓存Key中增加业务模式维度。

// 原Key:平台编码 + "_" + 组织ID
String key = platCode + "_" + orgId;

// 新Key:平台编码 + "_" + 组织ID + "_" + 业务模式
String key = platCode + "_" + orgId;
if (bizMode != null && !bizMode.isEmpty()) {
key = key + "_" + bizMode;
}

不同模式存在不同的Key下,互不干扰:

普通取号:缓存Key = "PLAT_A_123_normal" → 缓存未命中 → 查库 → 拿到普通Token
代发取号:缓存Key = "PLAT_A_123_daifa" → 缓存未命中 → 查库 → 拿到代发Token

设计约束:bizMode 为 null 或空字符串时,退化为原有Key格式。完全向后兼容——现有调用方不需要任何修改,只有需要区分模式的场景才传入 bizMode。


04 方法扩展:新增六个方法,覆盖所有场景

加了一个参数,就需要配套的查询和清除方法。最终新增了六个方法:

方法用途
getTokenConfig(platCode, org, bizMode) 带模式参数的查询
evictToken(platCode, org, bizMode) 带模式参数的单个清除
evictTokenById(platCode, orgId, bizMode) 带模式参数的按ID清除
evictByPlatform(platCode, bizMode) 带模式参数的按平台批量清除
evictTokenById(platCode, orgId) 按组织ID清除(不需要完整对象)
evictByPlatform(platCode) 按平台批量清除(无模式参数)

按组织ID清除:原本的清除方法需要传入完整的组织对象。但有些调用场景(比如外部监控系统检测到Token过期)只知道组织ID,没有完整对象。新增一个重载方法,只传ID即可。

按平台批量清除:原本只能单个清除或全部清除。如果某个平台的AppKey全局变更,需要失效该平台下所有组织的缓存,只能遍历组织逐个清除。新增按平台批量清除,一次操作搞定。

带模式参数的批量清除:更进一步——如果只是代发模式的AppKey变更,只清除代发模式的缓存,不影响普通模式。批量清除时按模式后缀匹配。


05 JDK兼容:removeIf 不能用

批量清除的核心逻辑是:遍历缓存Map,删除匹配前缀(和后缀)的Key。

Java 8 有现成的方法:

// Java 8:简洁优雅
cache.keySet().removeIf(key -> key.startsWith(prefix));

但项目的运行环境是旧版本JDK,不支持 removeIf。只能改为迭代器模式:

// 旧版本JDK兼容:迭代器遍历
Iterator<String> it = cache.keySet().iterator();
while (it.hasNext()) {
String key = it.next();
if (key.startsWith(prefix)) {
it.remove();
}
}

带模式参数的批量清除,需要在匹配前缀的基础上再加后缀判断:

// 匹配以 PLAT_A_ 开头、以 _daifa 结尾的Key
Iterator<String> it = cache.keySet().iterator();
while (it.hasNext()) {
String key = it.next();
if (key.startsWith(prefix) && key.endsWith(suffix)) {
it.remove();
}
}

代码量多了几行,但逻辑完全一致。 遗留系统的技术栈约束就是这样——新版本JDK的流式API、Lambda表达式都用不了,设计方案时必须考虑向下兼容。


06 代码重构:提取公共查询方法

加带模式参数的查询方法时,发现整个数据库查询逻辑在两个重载方法中完全重复——包括白名单判断、通用表查询、各平台分支的判断。

提取一个公共方法 loadTokenFromDB:

private OrgTokenConfig loadTokenFromDB(String platCode, OrgInfo org) {
// 白名单:社交电商平台直接查授权表
if (isWhiteListPlatform(platCode)) {
return queryFromAuthTable();
}
// 其他平台:查通用Token表
TokenRecord record = queryFromTokenTable(org.getId());
if (record == null) {
return null;
}
// 根据平台编码获取对应的配置对象
if (isPlatformA(platCode)) {
return record.getConfigA();
} else if (isPlatformB(platCode)) {
return record.getConfigB();
}
// … 其他平台分支
}

关键细节:每个分支保留了原有的日志输出(包括平台中文名)。这确保了回归测试时日志一字不差,不会因为日志格式变化触发监控告警。

重构后两个查询方法只剩缓存Key构建方式的差异——一个带模式后缀,一个不带。数据库查询完全委托给公共方法。


07 向后兼容:旧代码一行不动

所有新增方法都是可选的扩展,原有方法的行为完全不变:

  • 原有查询方法:两个参数,行为不变
  • 原有清除方法:按对象清除,行为不变
  • 原有全部清除方法:行为不变

新方法是额外的工具,旧代码不需要任何修改。当需要区分模式时,调用方传入 bizMode 参数即可。

回归测试验证通过,所有平台分支的日志输出与原有代码完全一致。


08 核心收获

1. 缓存Key的粒度设计,在系统演进中会被重新审视。 最初设计时只有普通模式,代发是后来加的。一个后缀解决两个模式抢Key的问题,改动量很小——但前提是Key的构建逻辑是收敛在一处的,而不是散落在各调用方。

2. 公共方法提取的价值不在当下,在未来。 提取数据库查询公共方法后,后续新增平台只需要在判断分支中加一个条件,两个查询方法自动生效。如果没提取,新增平台要改两个地方,忘了一个就是Bug。

3. 向后兼容是遗留系统改造的底线。 bizMode 为 null 时退化为原有行为——这行判断保证了新老代码可以并存。新增方法、提取公共逻辑,都不影响已有调用方。

4. 技术栈约束影响设计决策。 removeIf 一行代码能解决的问题,在旧版本JDK上要多写四行。这不是设计缺陷,是工程现实。

讨论话题:你做缓存设计时,遇到过Key粒度不够导致数据互相覆盖的情况吗?是加后缀还是重新设计了Key结构?评论区聊聊。

赞(0)
未经允许不得转载:171主机测评 » 缓存组件设计复盘:从硬编码到带模式参数的缓存进化
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址