后台接搜索数据接口,第一反应是「怎么把请求发出去」。但跑一段时间你会发现,真正决定体验的是「怎么少发请求」------同一批关键词今天查完明天还查,中间没变化,重复请求就是纯浪费。这篇记录我给 SERP 采集任务加缓存层的完整过程:缓存键怎么设计、TTL 怎么定、怎么和接口的计费机制配合。
为什么需要缓存
我的采集任务是「每天定时查几十个关键词的排名和结果」。观察到两个事实:
- 短期数据重复:同一关键词一天内查 3 次,结果几乎不变
- 批量任务有重复:不同功能模块(排名、快照、报表)会查同一个词
没缓存时,这些重复请求都真实扣费。加一层缓存,效果立竿见影:请求量降了约 60%,延迟也从「等接口」变成「读缓存」。
第一步:先看清接口的计费结构
设计缓存前,我核对了数据源 SerpBase(官网 serpbase.dev,/google/search 端点)的计费规则:
/google/search、/google/news、/google/videos每次 1 credits/google/images和两个 Maps 端点每次 2 credits- 请求失败和上游超时自动退 credits(所以缓存命中省的是真金白银,缓存缺失的失败也不算亏)
图片端点 2 credits,意味着缓存它的收益是搜索端点的 2 倍------按「缓存收益 = 端点费率」排优先级,先缓存贵的。
第二步:缓存键设计
缓存键要能唯一对应一次请求。请求参数有 q、hl、gl、page、device,键就该把这些都拼进去,缺一个都会串数据:
python
import hashlib
import json
def cache_key(endpoint, params):
raw = json.dumps({"endpoint": endpoint, "params": params}, sort_keys=True)
return hashlib.sha256(raw.encode()).hexdigest()
用 sort_keys=True 保证参数顺序不影响键。同样的参数,永远命中同一个缓存。
第三步:本地缓存(最简单版本)
如果任务单机跑,用磁盘 JSON 文件就行,不引入外部依赖:
python
import json
import os
import time
CACHE_DIR = "./serp_cache"
def get_from_cache(key):
path = os.path.join(CACHE_DIR, f"{key}.json")
if not os.path.exists(path):
return None
with open(path, encoding="utf-8") as f:
entry = json.load(f)
if entry["expire_at"] < time.time():
return None # 过期当 miss
return entry["data"]
def put_to_cache(key, data, ttl_seconds):
os.makedirs(CACHE_DIR, exist_ok=True)
path = os.path.join(CACHE_DIR, f"{key}.json")
with open(path, "w", encoding="utf-8") as f:
json.dump({"expire_at": time.time() + ttl_seconds, "data": data}, f)
第四步:缓存优先的请求封装
把缓存逻辑包进请求函数,调用方无感知:
python
import requests
API_KEY = "你的Key"
URL = "https://api.serpbase.dev/google/search"
HEADERS = {"Content-Type": "application/json", "X-API-Key": API_KEY}
def cached_search(params, ttl_seconds=3600, endpoint="/google/search"):
key = cache_key(endpoint, params)
hit = get_from_cache(key)
if hit is not None:
return hit, "cache"
resp = requests.post(
f"https://api.serpbase.dev{endpoint}",
headers=HEADERS, json=params, timeout=30,
)
data = resp.json()
if data.get("status") == 0:
put_to_cache(key, data, ttl_seconds)
return data, "remote"
# 用法:同一关键词一小时内反复调,只有第一次真的发请求
data, source = cached_search({"q": "serp api", "hl": "en", "gl": "us"})
print(f"数据来源: {source}") # 第一次 remote,之后 cache
第五步:TTL 怎么定(经验值)
缓存的核心参数是 TTL------太短省不了钱,太长拿不到最新数据。我的经验值:
- 排名监控:4 小时 TTL。排名一天内变化不剧烈,4 小时足够新
- 关键词快照:24 小时 TTL。日报场景,一天一查
- 热点/新闻类:不缓存或 15 分钟 TTL。时效性要求高
按「数据变化的快慢」分配 TTL,而不是一刀切。
第六步:多机环境上 Redis
如果采集任务分布在多台机器,本地文件缓存会各自为战,重复就回来了。这种情况换成 Redis,逻辑一样:
python
import redis
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
def cached_search_redis(params, ttl_seconds=3600):
key = cache_key("/google/search", params)
hit = r.get(key)
if hit:
return json.loads(hit), "cache"
data, _ = cached_search(params) # 复用上面的远程请求逻辑
if data.get("status") == 0:
r.setex(key, ttl_seconds, json.dumps(data))
return data, "remote"
setex 同时设值和过期时间,天然防「键永久滞留」。
踩坑记录
坑 1:把 request_id 也缓存了。 第一版直接缓存整个响应,导致日志里大量重复 request_id,排查问题时无法定位到真实请求。解决:缓存前把 request_id、elapsed_ms 清掉或单独记录,缓存里的数据只留业务字段。
坑 2:缓存键漏了 page 参数。 只拼了 q,结果第 1 页和第 2 页串了。解决:键必须包含全部请求参数。
坑 3:TTL 一刀切。 所有数据都设 24 小时,排名监控拿不到当日变化。解决:按场景分 TTL,如上文。
效果沉淀
加上缓存层后:
- 请求量降约 60%,credits 消耗同步下降
- 重复查询延迟从「秒级」降到「毫秒级」(读本地文件/Redis)
- 批量任务和多个功能模块之间不再重复扣费
缓存不是复杂的活,但它是采集系统「省钱 + 提速」性价比最高的一笔投入。
接口的计费规则和 status/credits_charged 字段语义在 SerpBase 官方文档。先把缓存键和 TTL 定好,你的采集任务就少了一半的无谓消耗。
有在缓存层踩过坑的同学,欢迎交流你的 TTL 策略。