AI 大模型评测平台按月或按调用计量成本,单价来自各家供应商且会调整。把价格硬编码在配置里会迅速过期,每次调用都远程拉取又慢又脆弱。正确做法是把价格表集中存库,启动时加载进 Redis 并设置小时级 TTL,定时刷新与事件失效双管齐下,评测扣费时从缓存读取最新单价。下文给出缓存键设计、刷新策略与 Spring Boot 代码。
说明:本文基于通用工程实践对平台价格层做合理推演,非平台真实源码。方案适用于任意 Spring Boot + Redis 技术栈,读者应按供应商真实计费单位(每千 token / 每百万 token)对齐字段精度。
一、价格为什么不能写死
模型供应商按 token 计费,单价随版本迭代频繁变动。评测平台对接多家模型,每家的 input/output 单价、缓存命中价、最小计费单位都不同。若把价格写进 Java 常量,供应商调价当天计费就失真,月底对账与真实账单对不上。价格必须作为”会变化的数据”来管理,而非”不变的代码”。
把价格表放进数据库做单一事实源,应用层通过缓存读它,既保证可运营人员后台修改,又避免每次评测都查库拖慢链路。
二、缓存分层与键设计
2.1 单模型单价键
每个模型一条价格记录,键形如 price:{vendor}:{model},Hash 字段存 input_per_1k、output_per_1k、cache_hit_per_1k,单位为”每千 token 多少元”。
| 字段 | 含义 | 单位 |
|---|---|---|
| inputper1k | 输入单价 | 元 / 千 token |
| outputper1k | 输出单价 | 元 / 千 token |
| cachehitper_1k | 缓存命中单价 | 元 / 千 token |
2.2 全量价格快照键
price:all 存整张价格表的 JSON 快照,供后台看板一次性读取,TTL 设 1 小时,到点自动失效触发重载。
三、动态获取与刷新策略
3.1 读时回源
扣费前先查 Redis,命中直接用;未命中从数据库加载并写回,避免缓存穿透把压力打到数据库。
- 读取
price:{vendor}:{model}; - 命中则返回,用于本次 token 成本估算;
- 未命中则从价格表数据库按 vendor+model 查询;
- 查到则写回 Redis 并设 TTL,返回单价;
- 查不到则抛配置缺失异常,阻断该模型评测扣费。
@Service
public class PriceService {
private final StringRedisTemplate redis;
private final PriceMapper priceMapper;
public PriceService(StringRedisTemplate redis, PriceMapper priceMapper) {
this.redis = redis;
this.priceMapper = priceMapper;
}
public ModelPrice getPrice(String vendor, String model) {
String key = "price:" + vendor + ":" + model;
var cached = redis.opsForHash().entries(key);
if (!cached.isEmpty()) {
return toPrice(cached);
}
ModelPrice db = priceMapper.selectByVendorModel(vendor, model);
if (db == null) throw new IllegalStateException("价格未配置: " + vendor + "/" + model);
redis.opsForHash().putAll(key, fromPrice(db));
redis.expire(key, Duration.ofHours(1)); // 小时级失效
return db;
}
}
3.2 定时刷新与事件失效
仅靠 TTL 会有”旧价残留一小时”的窗口。补两条刷新路径:定时任务每小时全量重载 price:all;运营后台改价时主动 DEL price:{vendor}:{model},下次读取即回源最新值,做到改完立刻生效。
@Scheduled(fixedDelay = 3600_000)
public void reloadAll() {
List<ModelPrice> all = priceMapper.selectAll();
redis.opsForValue().set("price:all",
JsonUtils.toJson(all), Duration.ofHours(1));
}
/** 后台改价后调用,使缓存即时失效 */
public void invalidate(String vendor, String model) {
redis.delete("price:" + vendor + ":" + model);
}
四、和成本计量的衔接
评测任务结束拿到 usage 的 input/output token 后,调 getPrice 取当前单价算出金额,再交给成本计量模块累加(见 Redis 实时成本监控一文)。价格缓存与成本计数共用同一个 Redis 实例,读取链路零额外网络跳数。
五、生产注意点
Redis 连接失败时不能让整次评测失败:价格读取包一层 try-catch,连不上就回退到数据库直查或本地兜底配置,并打告警日志。价格属敏感计费数据,审计日志应落持久库(MySQL / 审计表)而非仅存 Redis,避免缓存清空丢失变更历史。缓存键用 vendor+model 组合,防止不同供应商同名模型串价。货币单位统一以”元”为基准、精度保留四位小数,避免浮点误差在海量调用后累积放大,造成对账偏差。
五、价格来源与供应商对接
价格单一事实源建议放在平台自有数据库,由运营或定时爬虫从供应商官网同步。直接在生产链路调供应商定价接口风险高:外部依赖会让每次评测扣费都多一次网络往返,且供应商接口不稳定会拖垮计费。正确做法是”离线同步入库、线上只读缓存”,供应商接口只在校验环节偶尔比对,不进入热路径。同步任务记录每次拉取的成功时间与来源,便于排查”价格为何没更新”这类运营疑问,也让计费口径有迹可循。
六、缓存一致性边界
价格这类”改了要尽快看到,但短暂旧值可接受”的数据,用”TTL + 事件失效”已足够,不必上写穿(write-through)增加写延迟。真正的边界在计费精度:扣费用的是读取瞬间的单价快照,若供应商在评测中途调价,本批次沿用旧价是合理的——它对应已发生调用的计费合约,强行中途换价反而对账混乱。平台应在价格表加 effective_time 字段,新价生效后新批次自动采用,旧批次不受影响。
| 失效方式 | 触发 | 时效 | 适用 |
|---|---|---|---|
| TTL 过期 | 到点自动 | 小时级 | 日常兜底 |
| 事件失效 | 后台改价 DEL | 秒级 | 运营改价 |
| 定时重载 | 调度任务 | 小时级 | 全量校正 |
七、与成本展示的联动
后台看板读 price:all 快照渲染”各模型单价对比表”,运营改价后看板下次刷新即更新。评测报告里每条子任务的成本,由”实际 token × 当时单价”算出,可在报告页回放任意历史批次的花费。把价格快照版本号随评测任务一起落库,即便后续价格大改,历史报告仍能精确还原当时的计费口径,审计无忧。
常见问题(FAQ)
Q1:价格改了多久能生效?
后台改价主动删缓存键,下次读取即回源最新单价,秒级生效。
Q2:Redis 挂了会影响评测吗?
价格读取加兜底,连不上就回退数据库直查,评测不中断。
Q3:为什么还要定时刷新?
TTL 防止脏数据常驻,定时重载兜底修正未被事件失效的陈旧键。