## 缓存与 Redis 使用说明 本仓库中 Redis 主要用于**性能型缓存**,当前落地的业务缓存包括: - **文本向量缓存**(embedding 缓存) - **翻译结果缓存**(Qwen-MT 等机器翻译) 底层连接配置统一来自 `config/env_config.py` 的 `REDIS_CONFIG`: - **Host/Port**:`REDIS_HOST` / `REDIS_PORT`(默认 `localhost:6479`) - **Password**:`REDIS_PASSWORD` - **Socket & 超时**:`REDIS_SOCKET_TIMEOUT` / `REDIS_SOCKET_CONNECT_TIMEOUT` / `REDIS_RETRY_ON_TIMEOUT` - **通用缓存 TTL**:`REDIS_CACHE_EXPIRE_DAYS`(默认 `360*2` 天,代码注释为 “6 months”) --- ## 1. 缓存总览表 | 模块 / 场景 | Key 模板 | Value 内容示例 | 过期策略 | 备注 | |------------|----------|----------------|----------|------| | 向量缓存(text/image embedding) | 文本:`{EMBEDDING_CACHE_PREFIX}:embed:norm{0|1}:{text}`;图片:`{EMBEDDING_CACHE_PREFIX}:image:embed:norm{0|1}:{url_or_path}` | **BF16 bytes**(每维 2 字节大端存储),读取后恢复为 `np.float32` | TTL=`REDIS_CONFIG["cache_expire_days"]` 天;访问时滑动过期 | 见 `embeddings/text_encoder.py`、`embeddings/image_encoder.py`、`embeddings/server.py`;前缀由 `REDIS_CONFIG["embedding_cache_prefix"]` 控制 | | 翻译结果缓存(translator service) | `trans:{model}:{target_lang}:{source_text[:4]}{sha256(source_text)}` | 机翻后的单条字符串 | TTL=`services.translation.cache.ttl_seconds` 秒;可配置滑动过期 | 见 `translation/service.py` + `config/config.yaml` | 下面按模块详细说明。 --- ## 2. 向量缓存(embeddings/text_encoder.py / embeddings/image_encoder.py / embeddings/server.py) - **代码位置**: - 调用侧:`embeddings/text_encoder.py`、`embeddings/image_encoder.py` - 服务侧:`embeddings/server.py` - 公共 Redis/BF16 编解码:`embeddings/redis_embedding_cache.py`、`embeddings/bf16.py` - **用途**:缓存文本/图片向量结果,避免重复推理、重复下载图片、重复占用模型并发槽位。 ### 2.0 当前缓存链路总览 - 现在是**双层缓存**: - **调用侧缓存**:`TextEmbeddingEncoder` / `CLIPImageEncoder` 在发 HTTP 请求前先查 Redis; - **服务侧缓存**:`/embed/text` / `/embed/image` 在进入后端推理前再查 Redis。 - 这次改动里,**BF16 存储格式本身没有变化**: - 写入:`float32 -> BF16 -> bytes -> Redis` - 读取:`Redis bytes -> BF16 -> float32(np.float32)` - 这次新增的核心变化是: - **缓存 key 加入 normalize 维度**,避免 `normalize=true` 与 `normalize=false` 命中同一条缓存; - **服务端也启用同一套 Redis 向量缓存**,而不是只有 client 侧缓存; - **拆分 text / image 服务** 后,图片压力不会再拖慢文本服务; - **服务侧 full-cache-hit 可直接返回**,不会再进入模型限流槽位。 ### 2.1 Key 设计 - 统一 helper:`embeddings/cache_keys.py` - 文本主 key(TEI/BGE):`build_text_cache_key(text, normalize=...)` - 多模态图片(CN-CLIP `/embed/image`):`build_image_cache_key(url, normalize=..., model_name=...)`,其中 `model_name` 来自 `services.embedding.image_backends.*.model_name`(与 `embeddings.config.CONFIG.MULTIMODAL_MODEL_NAME` 一致) - 多模态文本(CN-CLIP `/embed/clip_text`):`build_clip_text_cache_key(text, normalize=..., model_name=...)` - 模板: ```text TEI 文本: {EMBEDDING_CACHE_PREFIX}:embed:norm{0|1}:{text} CN-CLIP 图片: {EMBEDDING_CACHE_PREFIX}:image:embed:{model_name}:txt:norm{0|1}:{url_or_path} CN-CLIP 文本塔: {EMBEDDING_CACHE_PREFIX}:clip_text:embed:{model_name}:img:norm{0|1}:{text} ``` - 字段说明: - `EMBEDDING_CACHE_PREFIX`:来自 `REDIS_CONFIG["embedding_cache_prefix"]`,默认值为 `"embedding"`,可通过环境变量 `REDIS_EMBEDDING_CACHE_PREFIX` 覆盖; - `model_name`:如 `CN-CLIP/ViT-H-14`;切换模型时自动使用新 key 空间,避免混用旧维度向量; - `norm1` / `norm0`:分别表示 `normalize=true` / `normalize=false`; - `text` / `url_or_path`:经 `strip` 后,若 Unicode 长度 **≤** ``CACHE_KEY_RAW_BODY_MAX_CHARS``(默认 256,见 ``embeddings/cache_keys.py``)则原样写入键尾;**更长**则改为 ``h:sha256:<64 hex>``(对 UTF-8 字节做 SHA-256)。TEI 文本与多模态共用 ``_stable_body_for_cache_key``。 补充说明: - TEI 文本 raw key 仍为 `embed:norm{0|1}:...`(尾部负载规则同上)。 - 多模态键名中 `txt` / `img` 段为项目内约定(与 `embeddings/cache_keys.py` 一致),用于区分图片 lane 与 clip 文本 lane。 ### 2.2 Value 与类型 - 类型:**BF16 bytes**(bfloat16),每一维用 2 字节无符号整数表示,按**大端**序列化。 - 写入流程:FP32 向量 → BF16 → bytes → Redis - 读取流程:Redis bytes → BF16 → FP32(`np.float32`)向量 - 典型示例:BGE-M3 1024 维向量在 Redis value 大小约为 \(1024*2=2048\) bytes(不含 Redis 元数据开销)。 ### 2.3 过期策略 - 初始化: - `self.expire_time = timedelta(days=REDIS_CONFIG.get("cache_expire_days", 180))` - `.env` 中可通过 `REDIS_CACHE_EXPIRE_DAYS` 配置,默认 `360*2`(代码注释标注为 6 个月)。 - 写入: - `redis.setex(cache_key, self.expire_time, serialized_data)` - 访问(滑动过期): - 命中缓存后,会调用 `redis.expire(cache_key, self.expire_time)` 延长 TTL。 ### 2.4 特殊处理 - 若缓存中的向量 **为空 / shape 异常 / 含 NaN/Inf**,会: - 直接丢弃该缓存(并尝试 `delete` key); - 回退为重新调用向量服务。 ### 2.5 服务拆分与缓存/限流关系 - 当前默认部署形态: - 文本 embedding 服务:`6005` - 图片 embedding 服务:`6008` - 调用方配置: - `EMBEDDING_TEXT_SERVICE_URL` - `EMBEDDING_IMAGE_SERVICE_URL` - 服务端配置: - `TEXT_MAX_INFLIGHT` - `IMAGE_MAX_INFLIGHT` - `EMBEDDING_SERVICE_KIND=all|text|image` - 关键行为: - **full-cache-hit** 请求在服务端会直接返回,不占用 `TEXT_MAX_INFLIGHT` / `IMAGE_MAX_INFLIGHT`; - **cache miss** 才会进入对应模型 lane; - 因此高频重复图片请求不会再因为严格的图片限流而大量阻塞。 --- ## 3. 翻译结果缓存(translation/service.py) - **代码位置**:`translation/service.py` - **用途**:统一缓存所有 translation capability 的翻译结果。 - **配置入口**: - `config/config.yaml -> services.translation.cache` - `config/config.yaml -> services.translation.capabilities.*.use_cache` ### 3.1 Key 设计 - 内部构造函数:`TranslationCache.build_key(...)` - 模板: ```text trans:{model}:{target_lang}:{source_text[:4]}{sha256(source_text)} ``` 其中: - `model`:capability 名称,如 `qwen-mt`、`llm`、`opus-mt-zh-en` - `target_lang`:目标语言,如 `en` / `zh` - `source_text[:4]`:原文前 4 个字符 - `sha256(source_text)`:对完整原文做 SHA-256 ### 3.2 Value 与类型 - 类型:**UTF-8 字符串**,即翻译后的文本结果。 - 存取逻辑: - 读取:`redis.get(key)` 返回 `str` 或 `None`; - 写入:`redis.setex(key, expire_seconds, translation)`。 ### 3.3 过期策略 - 配置来源:`config/config.yaml -> services.translation.cache`,经 `get_translation_cache_config()` 解析: ```yaml services: translation: cache: ttl_seconds: 62208000 # 默认约 720 天 sliding_expiration: true capabilities: qwen-mt: use_cache: true llm: use_cache: true deepl: use_cache: true nllb-200-distilled-600m: use_cache: true opus-mt-zh-en: use_cache: true opus-mt-en-zh: use_cache: true ``` - 运行时行为: - translator service 启动时初始化共享 Redis cache; - **读缓存**: - 命中后,若 `sliding_expiration=True`,会调用 `redis.expire(key, expire_seconds)`; - **写缓存**: - 使用 `redis.setex(key, expire_seconds, translation)`。 ### 3.4 关联模块 - `api/translator_app.py` 通过 `TranslationService` 统一复用同一套缓存逻辑; - 所有翻译后端都通过 `TranslationService` 接入缓存。 --- ## 4. 商品内容理解缓存(已迁出) 本仓库原先存在一套用于 `qanchors` / `enriched_*` 生成的 Redis 缓存实现,但对应内容理解服务已经迁移到独立项目,当前仓库代码中不再读写这类缓存,也不再把它作为运行时能力的一部分维护。 --- ## 5. Redis 运维脚本工具 `scripts/redis/` 下提供三个脚本,用于查看缓存数量、内存占用与健康状态。连接配置均来自 `config/env_config.py` 的 `REDIS_CONFIG`,运行前需在项目根目录执行(或保证 `PYTHONPATH` 含项目根),以便加载配置。 ### 5.1 redis_cache_health_check.py(缓存健康巡检) **功能**:按**业务缓存类型**(embedding / translation)做健康巡检,不扫全库。 - 对每类缓存:SCAN 匹配对应 key 前缀,统计**匹配 key 数量**(受 `--max-scan` 上限约束); - **TTL 分布**:对采样 key 统计 `no-expire-or-expired` / `0-1h` / `1h-1d` / `1d-30d` / `>30d`; - **近期活跃 key**:从采样中选出 `OBJECT IDLETIME <= 600s` 的 key,用于判断是否有新写入; - **样本 key 与 value 预览**:对 embedding 显示 ndarray 信息,对 translation 显示译文片段。 **适用场景**:日常查看两类缓存是否在增长、TTL 是否合理、是否有近期写入;与「缓存总览表」中的 key 设计一一对应。 **用法示例**: ```bash # 默认:检查 embedding / translation 两类 python scripts/redis/redis_cache_health_check.py # 只检查某一类 python scripts/redis/redis_cache_health_check.py --type embedding python scripts/redis/redis_cache_health_check.py --type translation # 按自定义 pattern 检查(不按业务类型) python scripts/redis/redis_cache_health_check.py --pattern "mycache:*" # 调整采样与扫描规模 python scripts/redis/redis_cache_health_check.py --sample-size 100 --max-scan 50000 --db 0 ``` **常用参数**: | 参数 | 说明 | 默认 | |------|------|------| | `--type` | 缓存类型:`embedding` / `translation`,可多选 | 两类都检查 | | `--pattern` | 自定义 key pattern(如 `mycache:*`),指定后忽略 `--type` | - | | `--db` | Redis 数据库编号 | 0 | | `--sample-size` | 每类采样的 key 数量 | 50 | | `--max-scan` | 每类最多 SCAN 的 key 数量上限 | 20000 | --- ### 5.2 redis_cache_prefix_stats.py(按前缀统计条数与内存) **功能**:**全局视角**,扫描当前 DB 下所有 key,按 key 的**前缀**(第一个冒号前)分类,统计每类 key 的**条数**与**内存占用量**(含占比),并输出每类示例 key 与 Redis 总内存信息。 - 使用 `SCAN` 扫全库,按前缀聚合; - 内存优先用 Redis `MEMORY USAGE`,不可用时用 key+value 长度估算;key 过多时按 `--sample-size` 采样后按均值推算总内存; - 输出:前缀、条数、内存及计算方式、占比、简要说明(如「翻译缓存」「向量化缓存」);末尾附 Redis 总内存与 maxmemory(若配置)。 **适用场景**:了解「哪一类前缀占了多少条、多少内存」,做容量规划或清理决策;支持多 DB(`--all-db`)或指定前缀(`--prefix`)缩小范围。 **用法示例**: ```bash # 默认 DB 0,全库按前缀统计 python scripts/redis/redis_cache_prefix_stats.py # 统计所有有数据的 DB python scripts/redis/redis_cache_prefix_stats.py --all-db # 指定 DB python scripts/redis/redis_cache_prefix_stats.py --db 1 # 只统计指定前缀(可多个) python scripts/redis/redis_cache_prefix_stats.py --prefix trans embedding # 全 DB + 指定前缀 python scripts/redis/redis_cache_prefix_stats.py --all-db --prefix trans embedding ``` **常用参数**: | 参数 | 说明 | 默认 | |------|------|------| | `--prefix` | 只统计这些前缀下的 key(如 `trans` `embedding`) | 全库按前缀统计 | | `--db` | 数据库编号(0–15) | 0 | | `--all-db` | 对所有有数据的 DB 分别执行 | 否 | | `--sample-size` | 单前缀 key 过多时,用于内存采样的数量 | 100 | | `--real` | 对单前缀 key 数 ≤10000 时计算全部 key 真实内存(较慢) | 否 | --- ### 5.3 redis_memory_heavy_keys.py(大 key / 内存占用排查) **功能**:找出当前 DB 中**占用内存最多的 key**,并分析「按 key 估算的总内存」与「Redis 实际使用内存」的差异原因。 - 全库 `SCAN` 获取 key 列表,对 key 采样(默认最多 1000)调用 `MEMORY USAGE`(或估算)得到单 key 内存; - 按内存排序,输出**占用最高的 N 个 key**(`--top`,默认 50); - 输出:前缀分布、采样统计、估算总内存 vs 实际内存、差异说明(碎片、内部结构等);并检测超大 value(>1MB)、key 类型分布。 **适用场景**:内存异常升高时定位大 key;理解为何「按 key 加总」与 `used_memory` 不一致。 **用法示例**: ```bash # 显示占用内存最多的 50 个 key(默认) python scripts/redis/redis_memory_heavy_keys.py # 显示前 100 个 python scripts/redis/redis_memory_heavy_keys.py --top 100 ``` **常用参数**: | 参数 | 说明 | 默认 | |------|------|------| | `--top` | 显示内存占用最高的 N 个 key | 50 | --- ### 5.4 三个脚本的选用建议 | 需求 | 推荐脚本 | |------|----------| | 看两类业务缓存(embedding/translation)的数量、TTL、近期写入、样本 value | `redis_cache_health_check.py` | | 看全库或某前缀的 key 条数与内存占比 | `redis_cache_prefix_stats.py` | | 找占用内存最多的大 key、分析内存差异 | `redis_memory_heavy_keys.py` | --- ## 6. 其他 Redis 相关代码(测试与替身) 除上述运维脚本外,以下代码使用 Redis 或其替身,但不定义新的业务缓存 key 协议: - **单元测试**:`tests/test_embedding_pipeline.py` 中的 `_FakeRedis` 用于 mock embedding 缓存;`tests/test_translator_failure_semantics.py` 中的 `_RecordingRedis` 用于验证翻译失败时不写缓存。 --- ## 7. 添加新缓存时的建议 新增 Redis 缓存时,建议遵循以下约定: - **配置集中**: - key 前缀、TTL 优先放在 `config/env_config.py`(通用)或 `config/config.yaml -> services..cache`; - 避免在业务代码中硬编码 TTL。 - **命名规范**: - 统一使用 `prefix:维度1:维度2:...` 的扁平 key 结构; - 对长文本/value 使用 `md5`/`sha256` 做哈希,避免过长 key。 - **文档同步**: - 新增缓存后,应在本文件中补充一行总览表 + 详细小节; - 若缓存与外部系统/历史实现兼容(如 Java 侧翻译缓存),需在说明中显式标注。