今日油价调用量规划:缓存粒度与 TTL 的确定方式

约 12 分钟 高级今日油价缓存设计Redis调用量规划
# 今日油价调用量规划:缓存粒度与 TTL 的确定方式 接口/接入点:今日油价(apiCode=138)· 查询油价(138-46)/ 查询行情(138-49) · 免费 · 适用人群:中高级开发者、架构师 · 阅读时间:约 6 分钟 · 最后实测核对:2026-09-18 今日油价是免费服务,注册后按平台档位限制调用量。油价数据一天只变一次,页面却可能被反复刷新,加一层缓存是典型的工程动作。这篇讲清缓存该按什么粒度存、TTL 该设多久。 ## 核心要点 - 缓存粒度按 `prov` 或按全量快照都可以,2026-09-18 对照实测确认两种粒度的取值与结果一一对应。 - 一次不传 `prov` 的调用就能覆盖 31 个省级行政区,计费次数为 1,适合做全量快照。 - TTL 不建议硬编码到某个整点。2026-09-18 实测返回的 `ct` 是 `2026-09-18 12:00:05.916`,与产品说明的每日凌晨 7 点不是同一时点。 ## 免费档位与调用量 档位说明取自免费接口页面(https://www.showapi.com/free-api),注册后默认为 L0: | 档位 | 每日调用量 | 并发限制 | 积分要求 | |---|---|---|---| | L0 基础版 | 100 次/天 | 1 QPS | 免费 | | L1 进阶版 | 500 次/天 | 5 QPS | 5,000 积分/年 | | L2 高级版 | 2,000 次/天 | 10 QPS | 15,000 积分/年 | | L3 专业版 | 10,000 次/天 | 30 QPS | 50,000 积分/年 | | L4 旗舰版 | 50,000 次/天 | 50 QPS | 100,000 积分/年 | 按调用次数累计消耗。用户在页面上刷新一次就打一次接口的话,L0 的 100 次/天很容易被一个热门页面用完。 ## 缓存粒度 2026-09-18 做的对照实测: | 请求 | 返回条数 | 返回的省份 | |---|---|---| | `prov=北京` | 1 | 北京 | | `prov=广西` | 1 | 广西 | | 不传 `prov` | 31 | 全部 31 个省级行政区 | 传 `prov` 时返回且只返回该省,所以以 `prov` 作为缓存 key 是一一对应的。不传时一次覆盖全部省份,所以也可以把整批结果存成一个快照。 两种用法的取舍: - **单省缓存**:适合用户选定一个省再查的场景。key 用 `oil:138:46:{prov}`,命中率高,缓存体量小。 - **全量快照**:适合油价地图、全国排行榜这类一次要展示多省的场景。一次回源拿全 31 省,存一个 JSON,取用时按省名索引。 ## 实现示例 Python(requests + Redis): ```python import json import requests import redis rds = redis.Redis(host="localhost", port=6379, db=0) API = "https://route.showapi.com/138-46" def fetch_snapshot(): """一次调用取回全国油价,返回 {省名: 记录} 字典。""" resp = requests.post(API, data={"appKey": "YOUR_APPKEY"}, timeout=5).json() if resp.get("showapi_res_code") != 0: raise RuntimeError(resp.get("showapi_res_error")) body = resp["showapi_res_body"] if body.get("ret_code") != 0: raise RuntimeError(body.get("remark")) return {item["prov"]: item for item in body["list"]}, body["list"][0]["ct"] def get_snapshot(ttl_seconds=3600): """命中缓存直接返回;未命中回源并写回,同时记录 ct。""" cached = rds.get("oil:138:46:snapshot") if cached: data = json.loads(cached) return data["prices"], data["ct"] prices, ct = fetch_snapshot() rds.setex( "oil:138:46:snapshot", ttl_seconds, json.dumps({"prices": prices, "ct": ct}, ensure_ascii=False), ) return prices, ct def get_province(prov, ttl_seconds=3600): """单省缓存,未命中时从快照里取,避免额外回源。""" key = f"oil:138:46:{prov}" cached = rds.get(key) if cached: return json.loads(cached) prices, _ = get_snapshot(ttl_seconds) item = prices.get(prov) if item: rds.setex(key, ttl_seconds, json.dumps(item, ensure_ascii=False)) return item ``` cURL(回源动作由服务端触发): ```bash curl -X POST "https://route.showapi.com/138-46?appKey=YOUR_APPKEY" \ -H "content-type: application/x-www-form-urlencoded" -d "prov=%E5%8C%97%E4%BA%AC" ``` Node.js(fetch + 内存缓存示意): ```javascript const API = "https://route.showapi.com/138-46"; let cache = { prices: null, ct: null, expireAt: 0 }; async function getSnapshot(ttlMs = 3600_000) { if (cache.prices && Date.now() < cache.expireAt) { return cache.prices; } const resp = await fetch(API, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ appKey: "YOUR_APPKEY" }), // 不传 prov 取全量 signal: AbortSignal.timeout(5000), }); const body = (await resp.json()).showapi_res_body; if (body.ret_code !== 0) throw new Error(body.remark); cache = { prices: Object.fromEntries(body.list.map((x) => [x.prov, x])), ct: body.list[0].ct, expireAt: Date.now() + ttlMs, }; return cache.prices; } const prices = await getSnapshot(); console.log(`缓存 ${Object.keys(prices).length} 个省份,数据时间 ${cache.ct}`); ``` ## TTL 怎么定 旧的做法是把 TTL 设成「距下一个整点 7 点的秒数」,前提是数据在 7 点刷新。2026-09-18 实测拿到的时间戳不支持这个前提:`ct` 是 `2026-09-18 12:00:05.916`,不是 07:00 附近。 两种可行做法: - **按 `ct` 变化失效**:缓存里一并存下 `ct`,回源后先比较新旧 `ct`,没变就刷新 TTL 不更新数据,变了就整体替换。这种方式不依赖具体几点刷新。 - **按固定周期失效**:TTL 设 1 小时或 2 小时。数据一天只变一次,这个频率下的多余回源很少,实现也简单。 给出调用量预算的例子:页面日均活跃用户 5,000、每人看 2 次油价,不缓存需要 10,000 次调用;按小时缓存后每天最多 24 次回源,L0 的 100 次/天完全够用。 ## 适用条件与注意事项 - 档位按调用次数累计,一次全量调用与一次单省调用的计费次数相同。 - 缓存 key 里的接入点编号要带上,`138-46` 与 `138-49` 返回结构不同,混用会解析出错。 - `ct` 只代表这批数据的生成时间,不等同于调价公告时间。 - 并发限制按档位不同,L0 是 1 QPS。定时任务与用户实时请求共用额度时,把回源动作串行化。 - 缓存写入前把空字符串字段原样保留,判空逻辑放在展示层统一处理。 ## FAQ **Q1:免费接口为什么还需要缓存?** 档位按调用次数累计,页面刷新频次远高于数据更新频次。缓存把回源次数压到每天个位数,同时让页面响应更快。 **Q2:缓存该按省还是按全国?** 两种都行。2026-09-18 实测 `prov=北京` 只返回北京、不传 `prov` 返回 31 条,粒度与结果都是一一对应的。一次要展示多省时用全量快照更省调用。 **Q3:TTL 设多久合适?** 一小时或两小时足够。数据一天只变一次,不必精确到某个整点。 **Q4:怎么确认缓存里的数据已经是新的?** 比较 `ct`。缓存与回源结果的 `ct` 不同就说明数据换批了,替换即可。2026-09-18 实测同一批 31 条记录的 `ct` 有两个毫秒值,属同批写入。 **Q5:定时任务和实时请求会不会互相挤额度?** 会共用同一档位额度。把回源动作收敛到一个定时任务里,实时请求只读缓存,额度消耗最稳。 ## 相关能力 / 下一步阅读 - [今日油价数据什么时候刷新?用 ct 字段判断缓存是否过期](https://www.showapi.com/guides/oilprice-schedule-138) - [今日油价:一次调用取回全国 31 省油价的做法](https://www.showapi.com/guides/oilprice-province-list-138) - [在车主服务 APP 里接入今日油价的完整流程](https://www.showapi.com/guides/oilprice-app-integration-138) - **本系列共 13 篇**:查看[今日油价指南总目录](https://www.showapi.com/guides/oilprice-guides-138)
加载中...