如何基于 Redis + Protobuf 缓存天气查询结果并设置 TTL?
解读
在大模型应用落地过程中,天气数据是典型的高频、高延迟、低变化外部接口,若每次用户问“今天北京天气”都实时调用第三方服务,既增加大模型推理链路耗时,也极易触发QPS 上限或账单飙升。因此,国内主流做法是把城市+日期维度的查询结果缓存到 Redis,并给缓存加TTL(Time-To-Live)实现“准实时”更新;同时为了节省内存、降低网络包大小,使用Protobuf 序列化代替 JSON,可把体积再压缩 30 %~50 %。面试官想考察的是:
- 能否把缓存键设计得既唯一又可扫描;
- 能否在并发场景下防止缓存击穿、穿透、雪崩;
- 能否用Redis 原生 TTL 与Protobuf 高效序列化无缝配合;
- 能否给出可落地的 Java/Go/Python 代码骨架,并解释大模型侧如何透明调用。
知识点
- Redis 数据结构选择:String 最省内存,且 TTL 精度到秒;若需按城市前缀批量失效,可用 Hash + 后台定时扫描,但生产环境优先 String。
- 缓存键规范:国内统一用**“业务域:版本:城市码:日期”格式,例如
weather:v1:101010100:2024-07-16,方便按前缀统计与版本升级**。 - Protobuf 设计:定义
message WeatherReply,字段用int32存温度(扩大 100 倍去浮点)、enum存天气现象,禁止使用string存枚举值,可再省 20 % 空间。 - TTL 策略:天气数据在国内按小时更新,TTL 设为1.5 小时(5400 s)即可兼顾实时与命中;若对接中国气象局分钟级雷达数据,可缩短到600 s。
- 并发安全:使用Redis SETNX + EX 实现互斥锁,防止大模型并发推理线程同时回源;或直接用Redisson 分布式锁兜底。
- 大模型侧透明调用:把缓存封装成同步函数
get_weather(city: str, date: str) -> WeatherReply,内部先读 Redis,未命中再调第三方,对大模型 Prompt 完全透明,符合LLMOps 可观测要求。 - 内存监控:通过Redis INFO memory 与Prometheus exporter 采集
used_memory_rss,当内存占用超 80 % 触发LRU 淘汰或手动批量删除过期键。
答案
以 Python 为例,给出可直接落地的最小可运行骨架(生产需加异常重试、日志、指标):
- 定义 Protobuf
syntax = "proto3";
package weather;
option java_package = "com.xxx.weather.proto";
message WeatherReply {
int32 temp_x100 = 1; // 温度*100,省浮点
WeatherType type = 2;
int32 humidity = 3;
int64 update_time = 4; // 时间戳,秒
enum WeatherType {
SUNNY = 0;
RAIN = 1;
CLOUDY = 2;
}
}
- 编译生成
weather_pb2.py后,核心缓存类
import redis, time, weather_pb2
class WeatherCache:
def __init__(self, redis_host, ttl=5400):
self.r = redis.Redis(host=redis_host, decode_responses=False)
self.ttl = ttl
def _key(self, city_code, date):
return f"weather:v1:{city_code}:{date}".encode()
def get(self, city_code, date):
raw = self.r.get(self._key(city_code, date))
if raw:
wp = weather_pb2.WeatherReply()
wp.ParseFromString(raw)
return wp
return None
def set(self, city_code, date, wp: weather_pb2.WeatherReply):
key = self._key(city_code, date)
self.r.setex(key, self.ttl, wp.SerializeToString())
- 大模型侧调用
def query_weather(city: str, date: str) -> str:
city_code = city_to_code[city] # 映射表
cache = WeatherCache("127.0.0.1")
wp = cache.get(city_code, date)
if wp is None:
wp = call_cma_api(city_code, date) # 中国气象局接口
cache.set(city_code, date, wp)
return f"{city}{date}天气{WeatherType.Name(wp.type)},温度{wp.temp_x100/100}℃"
- 并发安全增强(可选)
with redis.lock.Lock(cache.r, f"lock:weather:{city_code}:{date}", timeout=5):
if cache.get(...) is None:
...
关键点:
- 使用
setex原子完成 set + TTL; - Protobuf 序列化后字节流直接存 Redis,无需 base64;
- 键里带版本号 v1,后续字段变更可平滑升级;
- TTL 取1.5 小时既覆盖国内气象局整点更新延迟,又避免雪崩。
拓展思考
- 大模型批量提问场景:若用户一次性问“北京、上海、广州未来三天天气”,可一次性 pipeline 批量查 Redis,减少RTT 三倍;未命中城市再并发回源,整体 P99 延迟从 900 ms 降到 180 ms。
- 冷热分级:把当天天气放 Redis,历史 30 天放SSD 上的 Redis on Flash,再老数据落Hive + ORC,通过大模型插件路由层自动切换,节省60 % 内存成本。
- 缓存穿透攻击:国内常出现伪造城市码 999999 刷接口,可在网关层用布隆过滤器拦截,或缓存空结果 300 s,防止大模型推理层被拖垮。
- TTL 动态调整:结合气象局更新推送(Kafka 队列),收到更新事件后主动删除对应键,使下次查询强制刷新,实现分钟级实时而不缩短 TTL,避免缓存雪崩。
- 跨云多活:若业务部署在阿里云华北 + 华南,可用Redis Global Database 做跨地域主从,Protobuf 字节流无状态,无需考虑 JSON 字段大小写差异,同步带宽节省 40 %。