Token刷新与缓存失效:一对被忽视的上下游
我是折哥,20年码农,专注出版社物流系统架构与Java实战。这个系列记录了我从单平台到多平台电子面单的重构全过程,关注我,第一时间获取后续更新。
上一篇:微信视频号电子面单完整对接实录
本文:Token刷新与缓存失效:一对被忽视的上下游
先说一个认知修正。
重构电子面单模块时,旧代码里有一个定时刷新Token的方法,每天定时跑,调用各平台API刷新Token。新架构里有一个带缓存和自动刷新能力的缓存组件。
我最初以为:后者是前者的替代品。
然后我被打脸了。它们不是替代关系,是上下游。
一、 三个组件,一条链路
先看三个组件各自干什么:
后台定时刷新:定时任务触发,调用各电商平台的API,用refresh_token换新的access_token,写入数据库。一个组织失败不影响后续,吞掉异常继续跑。
前台手动刷新:用户在前台点“刷新Token”按钮触发。逻辑和后台一模一样,但异常处理策略截然相反——失败直接抛业务异常,用户必须看到结果。
缓存查询:每次取号时,从内存缓存中读Token配置。缓存未命中才查数据库,避免每次取号都走DB。
三者的关系:
后台定时刷新 / 前台手动刷新 │ │ 写入新的Token值 ▼ 数据库 │ │ 缓存查询读取并缓存 ▼ 取号流程(使用缓存的Token配置)刷新负责写入,缓存负责读取。两者串联完成“Token值刷新 → 数据库更新 → 缓存失效 → 新值加载”的闭环,但各自解决完全不同的问题。
二、 同一个逻辑,两种异常策略
更有意思的是两个刷新方法的关系。
后台和前台的核心逻辑完全一样:查组织列表 → 构建签名 → 调API → 解析响应 → 更新数据库。唯一区别在最后一步:
后台定时刷新:
if(!isGetData){this.logger.error("更新"+org.getName()+"授权失败:"+responseVal);// 不抛异常,继续下一个组织}前台手动刷新:
if(!isGetData){thrownewBusinessException("更新"+org.getName()+"授权失败:"+responseVal);}这不是代码重复,是场景驱动的工程设计。
定时任务不能中断——一个组织失败就停掉整个循环,其他几十个组织的Token跟着过期,那就是生产事故。但手动刷新是运维人员主动操作,他需要立刻知道“刷没刷成功”。
同一个业务逻辑,吞异常和抛异常都是对的——取决于谁在调用。AI会统一处理,人知道区别对待。
三、 一个被忽视的问题:缓存什么时候失效?
后台刷新更新了数据库里的Token值,但缓存里还存着旧值。如果缓存没有及时失效,取号流程拿到的还是旧Token。
当前的设计是:缓存定时全量清空,默认1小时一次。这意味着Token刷新后,最多等1小时缓存才能感知到新值。
更理想的方式是:刷新成功后,主动清除对应缓存。
但改造方式有讲究。我的原则是:只加不改,最小侵入。
在一个普通电商平台和一个代发模式的刷新成功点,各插入一段缓存清除逻辑:
自动刷新(平台A普通模式):
if(isGetData){// === 新增:清除缓存 ===try{tokenCache.evictByOrgId(PlatFormType.PLAT_A_CODE,org.getId(),"normal");this.logger.info("已清除平台A普通Token缓存: "+org.getCode());}catch(ExceptioncacheException){this.logger.warn("清除缓存失败: "+cacheException.getMessage());}}else{this.logger.error("更新"+org.getName()+"授权失败:"+responseVal);}手动刷新(平台A普通模式):
if(isGetData){// === 新增:清除缓存 ===try{tokenCache.evictByOrgId(PlatFormType.PLAT_A_CODE,org.getId(),"normal");this.logger.info("已清除平台A普通Token缓存: "+org.getCode());}catch(ExceptioncacheException){this.logger.warn("清除缓存失败: "+cacheException.getMessage());}}else{thrownewBusinessException("更新"+org.getName()+"授权失败:"+responseVal);}关键设计:
- 缓存清除包裹在
try-catch中,失败只打日志,绝不中断主流程 - 和原有的错误处理合并为
if-else,一个分支成功+清缓存,另一个分支失败 - 原有代码一行不动,只在
if块里加了6行
四个平台通道(平台A普通、平台A代发、平台B、平台C)× 两个方法(自动+手动)=八处改造点,每处改动不超过10行代码。
四、还有一个惊喜
在梳理缓存Key设计时,发现了一个潜在问题:平台A的普通和代发模式共用缓存Key。
两者的Token存在不同字段,但缓存Key一样——这意味着先刷普通Token再刷代发Token,或者反过来,缓存会被互相覆盖。
给缓存Key加一个模式后缀:
// 普通模式:PLAT_A_123_normal// 代发模式:PLAT_A_123_daifapublicOrgTokenConfiggetTokenConfig(StringplatCode,OrgInfoorg,StringbizMode){Stringkey=platCode+"_"+org.getId();if(bizMode!=null&&!bizMode.isEmpty()){key=key+"_"+bizMode;}// ... 缓存未命中则查数据库}bizMode为 null 时退化为原有行为,完全向后兼容。新增的批量清除方法同样支持按模式清理,用迭代器替代旧版本JDK不支持的removeIf。
五 核心收获
1. 缓存和刷新是上下游,不是替代关系。这个认知修正花了我一个下午,但值——它让我看清了整个Token生命周期的完整链路。
2. 同一个逻辑,不同场景需要不同的异常策略。定时任务吞异常、手动操作抛异常,两者都对——取决于谁在调用、失败后果是什么。
3. 最小侵入式改造:只加不改。八个改造点,每处只在if (isGetData)里加了缓存清除,原有代码一字不动。上线风险为零。
4. 缓存Key的粒度设计很重要。加一个模式后缀,同一个平台的不同业务模式就不打架了。这个后缀不是一开始就想出来的,是在梳理代码时“顺便”发现的隐患。
六 后续
Token刷新的代码结构还没有动——方法里塞了四个平台的逻辑,代码重复率很高,日志级别还在用error打正常流程。这些都在优化清单上,但优先级排后。
原则不变:先上线、跑稳、观察,再迭代。这次只加缓存清除,等系统稳定运行一段时间后,再考虑代码层面的重构。
七、系列目录
💡如果你是第一次来,建议从这几篇开始:
开篇:从"能跑就行"到"整洁架构"——整体思路,适合先了解背景
12:两次架构升级完整复盘——最值钱的一篇,架构决策全记录
16:微信视频号电子面单完整对接实录(本文)
全部文章:
开篇:从"能跑就行"到"整洁架构"
01:奇门对接顺丰电子面单
02:抖音代发电子面单对接
03:抖音普通订单电子面单对接
04:多平台统一架构设计
05:策略工厂复合Key路由改造
06:快递公司前置校验改造
07:解析器职责分离改造
08:模板方法的组合与继承抉择
09:API调用调度层Handler分组设计
10:奇门 trade_order_list 排查实录
11:数据库查询优化让多包裹取号快一倍
12:两次架构升级完整复盘
13:常量与配置集中管控改造
14:京东物流电子面单对接
15:拼多多电子面单完整对接实录
16:微信视频号电子面单完整对接实录
17:Token刷新与缓存失效:一对被忽视的上下游(本文)
八、延伸阅读:Java 23种设计模式实战系列
本文中三步策略架构、异常策略的多重兜底设计、Handler的两步调用编排,背后体现了策略模式、模板方法模式和责任链模式。在《Java 23种设计模式:从踩坑到精通》系列中,这些模式有更体系化的拆解:
策略模式:如何定义算法族并保证异常分支的完整覆盖?
模板方法模式:两步API调用的固定流程与可变步骤如何分离?
责任链模式:错误提示的多重兜底是否可以用责任链实现更优雅?
📖《Java 23 种设计模式:从踩坑到精通》
系列开篇:从踩坑到精通 —— 总览与导航
策略模式 —— 从if-else到优雅替换
模板方法模式 —— 组合优于继承的实战验证
💡学习建议:电子面单系列侧重多平台工程实践,设计模式系列侧重理论体系与设计思维。两者搭配阅读,形成"实战→理论→反哺实战"的闭环。
九、一起交流,共同进步
两步API调用的顺序约束、Token存储位置的平台差异、错误提示的多重兜底——这些都是在多平台对接中容易被忽略的细节。十三次测试、五个踩坑点,微信视频号平台的对接过程完整展示了从API设计差异理解到全链路跑通的全过程。
📌 点击上方"关注",第一时间获取系列更新推送。
💬 你在做缓存设计时,遇到过“数据更新了但缓存没失效”的坑吗?是主动清除还是等过期?评论区聊聊。欢迎在评论区分享。
🔗 如果本文对您有帮助,请点赞、收藏、分享,让更多同行看到。