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

📅 2026/7/21 22:52:32 👁️ 阅读次数 📝 编程学习
从 `int` 到 `Duration`:一个缓存 API 的三次演进教会我的事

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

image

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 小时。
  • 类型不同(long vs TimeUnit),顺序写反会编译报错,不会留到运行时。
  • 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,禁止使用 intlong

回头看,从 intlong+TimeUnit 再到 Duration,不仅仅是 API 签名变了,更是对代码质量理解的深化——高质量代码不是靠“约定”和“自觉”,而是靠类型系统和编译器来保障。

下一次你写一个接收时间参数的方法时,不妨问问自己:我能让调用方犯错的可能性降到零吗?