今日油价调用量规划:缓存粒度与 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)





