从 `int` 到 `Duration`:一个缓存 API 的三次演进教会我的事

1) 一个让我熬夜排查的 Bug
某天凌晨两点,线上告警:用户 Token 频繁过期,大量请求被踢回登录页。
查了一圈,发现 Redis 里 Token 的 TTL 设置有问题——本该存活 1 小时的 Token,实际只活了 1 分钟。顺着调用链找到罪魁祸首:
CacheUtils.set("token:" + userId, tokenJson, 60); // 调用方以为是60秒
再看方法签名:
public static String set(String key, String value, int cacheSeconds)
调用方传的 60 确实是 60 秒,但问题出在另一个地方——有人传了 TimeUnit.HOURS.toMillis(1)(结果是 3600000),被当作秒存进去了,导致 TTL 变成 3600000 秒 ≈ 41 天,而其他人传的正常值反而显得异常。
排查过程极其痛苦,因为 int 参数无法区分单位。那一刻我意识到:程序设计不注意细节的话,也许会成为整个团队的隐患。
2) 第一代:int cacheSeconds——简单,但脆弱
public static String set(String key, String value, int cacheSeconds)
优点: 参数少,调用简单,靠参数名来约定调用方。
缺陷:
- 单位全靠参数名约定,编译器不帮忙,IDE 不提醒。
- 魔法数字泛滥:
set("key", val, 7200)谁知道 7200 是两小时还是两毫秒? - 容易误传:
TimeUnit.HOURS.toMillis(1)这种错误,只要团队里有一个人犯,就够所有人喝一壶。
这个版本的代码就像“手写 SQL 拼接”——能跑,但随时可能炸。
3) 第二代:long + TimeUnit——类型安全,但调用繁琐
痛定思痛,我们加了 TimeUnit 参数:
public static String set(String key, String value, long cacheTTL, TimeUnit timeUnit)
进步之处:
- 单位显式指定,
set(k, v, 1, TimeUnit.HOURS)一眼可知是 1 小时。 - 类型不同(
longvsTimeUnit),顺序写反会编译报错,不会留到运行时。 long避免了int溢出的问题(虽然 Redis TTL 很少超过 int 范围,但更严谨)。
依然存在的问题:
- 调用方每次都要写两个参数,略显啰嗦。
long cacheTTL这个数值本身没有语义——1代表 1 个单位,但单位是TimeUnit决定的,调用方需要理解“TTL 数值”的含义。- 与主流框架不一致:Spring 的
RedisTemplate早已用Duration,我们的自定义工具类却还在用“数值+枚举”的组合。
这个版本像是“用安全带代替了徒手攀岩”——安全了,但还不够优雅。
4) 第三代:Duration——优雅且安全
在我们的 Java 8 版本中,有更好的方案:
public static String set(String key, String value, Duration cacheDuration)
这才是正确的姿态:
// 调用方代码即文档
CacheUtils.set("token", token, Duration.ofHours(1));
CacheUtils.set("code", code, Duration.ofMinutes(5));
CacheUtils.set("temp", temp, Duration.ofSeconds(30));
CacheUtils.set("config", config, Duration.ZERO); // 永不过期
相比前两代的碾压性优势:
| 维度 | 第一代 int |
第二代 long+TimeUnit |
第三代 Duration |
|---|---|---|---|
| 单位明确性 | 靠参数名约定 | 显式指定,但数值与单位分离 | 类型自带单位,语义合一 |
| 编译期检查 | 无 | 顺序写反会报错,但数值本身无约束 | 类型安全,传错类型直接编译失败 |
| 可读性 | 魔法数字,需换算 | set(k,v,1,HOURS) 可读,但略繁琐 |
Duration.ofHours(1) 自然语言 |
| 与生态集成 | 手动转换 | 手动转换 | 与 Spring/JDK 原生 API 无缝对接 |
| 扩展性 | 只能秒 | 支持多种单位,但需额外枚举 | 纳秒到天,任意精度,且支持运算 |
内部实现同样简洁:
public static String set(String key, String value, Duration cacheDuration) {if (cacheDuration.isNegative()) {throw new IllegalArgumentException("TTL must not be negative");}long seconds = cacheDuration.getSeconds(); // 底层 Redis 需要秒// ... 执行 Redis SETEX 命令
}
5) 三次演进教会我的事
教训0️⃣:定义清晰的参数名,仅仅是一个基础
int cacheSeconds指明让调用者传“秒”。
教训一:类型是最好的文档
int cacheSeconds 写了一百遍“单位是秒”,不如 Duration 一个类型来得可靠。编译器能替你检查的,就不要留给人类去记。
教训二:API 设计要考虑调用方的犯错成本
第一代 API 的设计者可能觉得“传个 int 多简单”,但他没想过调用方可能会传毫秒、传分钟、传魔法数字。一个好的 API 应该让正确用法显而易见,让错误用法难以编译通过。
6) 结语:高质量代码是从每一个参数开始的
经过这次 Bug,不妨定义如下这条团队规约:
所有表示“时间段”的参数,一律使用
java.time.Duration,禁止使用int或long。
回头看,从 int 到 long+TimeUnit 再到 Duration,不仅仅是 API 签名变了,更是对代码质量理解的深化——高质量代码不是靠“约定”和“自觉”,而是靠类型系统和编译器来保障。
下一次你写一个接收时间参数的方法时,不妨问问自己:我能让调用方犯错的可能性降到零吗?
当看到一些不好的代码时,会发现我还算优秀;当看到优秀的代码时,也才意识到持续学习的重要!--buguge
本文来自博客园,转载请注明原文链接:https://www.cnblogs.com/buguge/p/21752776