天气预报国际版:免费档位下的缓存策略设计(用 Redis 省调用量)
# 天气预报国际版:免费档位下的缓存策略设计(用 Redis 省调用量)
> 接口:天气预报国际版(apiCode=3540)全部接入点 · 免费接口 · POST/GET · JSON · 适用人群:后端/中高级开发者 · 阅读时间:约 7 分钟
## 核心要点
- 免费接口设有防滥用档次限制(具体档位数以官方说明为准),上量后第一件该做的事就是加缓存:同一城市同一接入点短时间内的重复请求,完全没有必要每次都打到接口。
- 实测事实:失败调用 `showapi_fee_num=0` 不扣次——调试阶段放心试错,但生产代码仍要把成功调用的次数当成本来管。
- 本文给出可落地的 Redis 缓存设计(key 结构、TTL 分层、雪崩防护)与完整 Python 代码。
## Why
天气数据天然"不需要秒级新鲜":当前天气几分钟内的变化对用户几乎无感,预报更是按小时/按天粒度。但典型的流量模式偏偏是反过来的——首页天气卡片每个 PV 都触发一次查询,热点城市的调用量远高于长尾城市。
在免费档位的频次约束下,这套流量结构意味着:不做缓存,很快就会碰到限流;做了缓存,同样的用户体验可以只用原来零头级别的调用量。这是天气接口接入后性价比最高的一个工程动作。
## What
设计目标:
| 目标 | 手段 |
|------|------|
| 减少重复调用 | 以「接入点 + 定位参数」为 key 缓存返回 JSON |
| 数据新鲜度可控 | 按数据类型分层 TTL(当前天气短、14 天预报长) |
| 防缓存雪崩 | TTL 加随机抖动 |
| 失败不污染缓存 | 只缓存 `ret_code=0` 的成功结果 |
TTL 分层建议(按数据粒度自行调整,非官方要求):
| 接入点 | 数据粒度 | 建议 TTL |
|--------|---------|---------|
| 3540-1 当前天气 | 实时 | 5~10 分钟 |
| 3540-2 24小时预报 | 小时 | 30~60 分钟 |
| 3540-3 14天预报 | 天 | 2~4 小时 |
## How
### 1. Redis 缓存封装(Python)
```python
# pip install requests redis
import json
import random
import requests
import redis
APPKEY = "YOUR_APPKEY"
rds = redis.Redis(host="localhost", port=6379, decode_responses=True)
TTL = {"3540-1": 600, "3540-2": 3600, "3540-3": 10800} # 秒
def get_weather_cached(endpoint: str, name=None, lon=None, lat=None) -> dict:
"""带 Redis 缓存的天气查询:key = 接入点 + 定位参数。"""
if lon and lat:
loc = f"lon:{lon},lat:{lat}"
elif name:
loc = f"name:{name}"
else:
raise ValueError("必须提供定位参数")
key = f"weather:{endpoint}:{loc}"
cached = rds.get(key)
if cached:
return json.loads(cached) # 缓存命中,不打接口
# 缓存未命中 -> 回源
params = {"appKey": APPKEY}
if lon and lat:
params.update({"lon": lon, "lat": lat})
else:
params["name"] = name
resp = requests.post(f"https://route.showapi.com/{endpoint}",
params=params, timeout=10)
data = resp.json()
if data["showapi_res_code"] != 0:
raise RuntimeError(f"系统级错误: {data.get('showapi_res_error')}")
body = data["showapi_res_body"]
if body.get("ret_code") != 0:
raise RuntimeError(f"业务错误: {body.get('remark')}") # 失败不缓存(实测失败不扣次)
ttl = TTL[endpoint] + random.randint(-60, 60) # 抖动防雪崩
rds.setex(key, ttl, json.dumps(body, ensure_ascii=False))
return body
# 连续两次调用同一城市,第二次直接走缓存
b1 = get_weather_cached("3540-1", name="北京")
b2 = get_weather_cached("3540-1", name="北京")
print(b1["cityInfo"]["city"], b2["cityInfo"]["city"]) # 第二次来自 Redis
```
cURL(回源行为对照):
```bash
curl -X POST "https://route.showapi.com/3540-1?appKey=YOUR_APPKEY&name=北京"
```
Node.js(ioredis 版要点):
```js
const Redis = require("ioredis");
const rds = new Redis();
async function getWeatherCached(endpoint, name) {
const key = `weather:${endpoint}:name:${name}`;
const cached = await rds.get(key);
if (cached) return JSON.parse(cached);
const res = await fetch(
`https://route.showapi.com/${endpoint}?appKey=${process.env.APPKEY}&name=${encodeURIComponent(name)}`,
{ method: "POST" }
);
const body = (await res.json()).showapi_res_body;
if (body.ret_code !== 0) throw new Error(body.remark);
const ttl = 600 + Math.floor(Math.random() * 120) - 60; // 抖动
await rds.setex(key, ttl, JSON.stringify(body));
return body;
}
```
### 2. 更进一步:热点城市预热
对确定的热点城市(你的用户集中地),用定时任务按 TTL 节奏提前刷新缓存,把用户请求全部挡在 Redis 层,回源调用量变成常数级别。
## 返回示例与解析
缓存的对象就是接口原样返回的 `showapi_res_body`(成功时),示例:
```json
{
"remark": "查询成功",
"ret_code": 0,
"cityInfo": { "city": "北京", "city_en": "Beijing", "time_zone": "Asia/Shanghai" },
"now": { "temperature": 33.7, "weather": "晴天", "rain_prop": 0 }
}
```
缓存命中时 `showapi_res_id`/`showapi_fee_num` 不会出现在你解析的对象里(它们是每次真实调用才有的系统级字段)——如需请求 ID 做对账,只在回源那次记录即可。
## 进阶与边界
- **失败不缓存**:`ret_code != 0` 的结果(如"经纬度不能为空")绝不能写缓存,否则一个坏参数会毒化整个 TTL 周期。
- **失败不扣次(实测)**:失败调用返回 `showapi_fee_num=0`。这降低了试错成本,但不要因此省略参数校验——参数校验应该在进缓存逻辑之前就拦下。
- **TTL 不是官方规定**:上表 TTL 是按数据粒度给出的工程建议,业务对新鲜度的要求不同请自行调整。
- **缓存与限速配合**:缓存解决重复请求,回源请求仍建议加限速与重试,见[多城市批量管理](https://www.showapi.com/guides/global-weather-multicity-3540)。
- **免费档位具体次数**:文档未给出,以[官方档位说明](https://www.showapi.com/free-api)为准,本文不编造数字。
## FAQ
**Q1:缓存 TTL 设多长合适?**
按数据粒度:当前天气 5~10 分钟、24 小时预报 30~60 分钟、14 天预报 2~4 小时是常见起点,按业务对新鲜度的敏感度调整。
**Q2:天气数据更新频率是多少,缓存会不会展示旧数据?**
接口文档未标注更新频率,本文不做无依据断言。工程上按"分钟级实况、小时级预报"的常识设 TTL 即可满足绝大多数产品场景。
**Q3:失败的结果要不要缓存一段短时间(负缓存)?**
可以,但要区分失败原因:参数类失败(实测 `ret_code=-1` "经纬度不能为空")建议在业务层直接拦截而不回源,无需负缓存;网络类失败不应缓存。
**Q4:不用 Redis,用本地内存缓存行不行?**
单实例小流量可以(`cachetools` 之类即可);多实例部署时本地缓存会导致各节点重复回源,此时用集中式 Redis 才能真正合并调用量。
**Q5:`showapi_fee_num` 能用来做计费监控吗?**
可以,它是每次调用实际扣费次数的系统级字段(实测成功为 1、失败为 0),把回源调用的该值打点统计,即可对账调用量。
## 下一步阅读
- [天气预报国际版:多城市天气批量管理(无批量接口下的循环与并发控制)](https://www.showapi.com/guides/global-weather-multicity-3540)
- [天气预报国际版:当前天气接入实战(气温、体感、风、降水概率全字段)](https://www.showapi.com/guides/global-weather-current-weather-3540)
- [天气预报国际版:免费接口档位说明(官方)](https://www.showapi.com/free-api)
- **本系列共 12 篇**:查看[天气预报国际版指南总目录](https://www.showapi.com/guides/global-weather-guides-3540)