如何基于 Redis + Protobuf 缓存天气查询结果并设置 TTL?

解读

在大模型应用落地过程中,天气数据是典型的高频、高延迟、低变化外部接口,若每次用户问“今天北京天气”都实时调用第三方服务,既增加大模型推理链路耗时,也极易触发QPS 上限账单飙升。因此,国内主流做法是把城市+日期维度的查询结果缓存到 Redis,并给缓存加TTL(Time-To-Live)实现“准实时”更新;同时为了节省内存、降低网络包大小,使用Protobuf 序列化代替 JSON,可把体积再压缩 30 %~50 %。面试官想考察的是:

  1. 能否把缓存键设计得既唯一又可扫描;
  2. 能否在并发场景下防止缓存击穿、穿透、雪崩;
  3. 能否用Redis 原生 TTLProtobuf 高效序列化无缝配合;
  4. 能否给出可落地的 Java/Go/Python 代码骨架,并解释大模型侧如何透明调用

知识点

  1. Redis 数据结构选择:String 最省内存,且 TTL 精度到秒;若需按城市前缀批量失效,可用 Hash + 后台定时扫描,但生产环境优先 String
  2. 缓存键规范:国内统一用**“业务域:版本:城市码:日期”格式,例如 weather:v1:101010100:2024-07-16,方便按前缀统计版本升级**。
  3. Protobuf 设计:定义 message WeatherReply,字段用 int32 存温度(扩大 100 倍去浮点)、enum 存天气现象,禁止使用 string 存枚举值,可再省 20 % 空间。
  4. TTL 策略:天气数据在国内按小时更新,TTL 设为1.5 小时(5400 s)即可兼顾实时与命中;若对接中国气象局分钟级雷达数据,可缩短到600 s
  5. 并发安全:使用Redis SETNX + EX 实现互斥锁,防止大模型并发推理线程同时回源;或直接用Redisson 分布式锁兜底。
  6. 大模型侧透明调用:把缓存封装成同步函数 get_weather(city: str, date: str) -> WeatherReply,内部先读 Redis,未命中再调第三方,对大模型 Prompt 完全透明,符合LLMOps 可观测要求。
  7. 内存监控:通过Redis INFO memoryPrometheus exporter 采集 used_memory_rss,当内存占用超 80 % 触发LRU 淘汰手动批量删除过期键

答案

以 Python 为例,给出可直接落地的最小可运行骨架(生产需加异常重试、日志、指标):

  1. 定义 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;
  }
}
  1. 编译生成 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())
  1. 大模型侧调用
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}℃"
  1. 并发安全增强(可选)
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 小时既覆盖国内气象局整点更新延迟,又避免雪崩

拓展思考

  1. 大模型批量提问场景:若用户一次性问“北京、上海、广州未来三天天气”,可一次性 pipeline 批量查 Redis,减少RTT 三倍;未命中城市再并发回源,整体 P99 延迟从 900 ms 降到 180 ms。
  2. 冷热分级:把当天天气放 Redis,历史 30 天SSD 上的 Redis on Flash,再老数据落Hive + ORC,通过大模型插件路由层自动切换,节省60 % 内存成本
  3. 缓存穿透攻击:国内常出现伪造城市码 999999 刷接口,可在网关层布隆过滤器拦截,或缓存空结果 300 s,防止大模型推理层被拖垮。
  4. TTL 动态调整:结合气象局更新推送(Kafka 队列),收到更新事件主动删除对应键,使下次查询强制刷新,实现分钟级实时而不缩短 TTL,避免缓存雪崩
  5. 跨云多活:若业务部署在阿里云华北 + 华南,可用Redis Global Database跨地域主从,Protobuf 字节流无状态无需考虑 JSON 字段大小写差异,同步带宽节省 40 %。