编程 Redis 8 深度实战:当 antirez 亲手写下 Vector Set,Redis 如何长成内存里的多模型数据库

2026-07-10 19:43:38 +0800 CST views 15

Redis 8 深度实战:当 antirez 亲手写下 Vector Set,Redis 如何长成内存里的多模型数据库

写在前面:如果你还把 Redis 当成"那个做缓存的 KV 中间件",Redis 8 会让你重新评估它。它不再只是字符串、哈希、列表的集合,而是悄悄长出了向量检索、JSON 文档、概率数据结构、时序存储这四类专业能力,并把它们做成了"一等公民"。这篇文章从工程视角拆开 Redis 8 的多模型内核,配可运行命令与 RAG 语义缓存的完整代码。

一、背景:一次身份的"二次觉醒"

Redis 的尴尬由来已久:它是工程师最熟悉的缓存,却很少被当成"数据库"认真对待。2024 年的许可证风波(从 BSD 切换到 RSALv2/SSPLv1,社区因此 fork 出 Valkey)让很多人以为 Redis 要一步步离开开源阵营。但 2025 年 Redis 8.0 干了两件大事:

  1. 重新拥抱开源:在保留原有许可证的同时,新增 OSI 认可的 AGPLv3 作为授权选项,并把免费产品改名为 "Redis Open Source"。
  2. 把"模块"收敛进核心:过去你要用向量检索、JSON、概率结构、时序,得分别 loadmodule 一堆 .so(RediSearch、RedisJSON、RedisBloom、RedisTimeSeries),版本错配、运维头疼;Redis 8 把这些能力直接编译进默认发行版,并以**统一查询引擎(Query Engine)**的形式对外暴露。

更戏剧性的是,Salvatore Sanfilippo(antirez,Redis 创始人)回归后亲手实现了一个全新的核心数据类型——Vector Set。这是他用 C 重写的 HNSW 向量索引,目标是"像有序集合一样简单,但分数变成向量"。

于是 Redis 8 的画像变了:它变成了一个内存里的多模型数据库(multi-model in-memory database)——一个实例里同时住着 KV、文档、向量、概率、时序五类数据模型,而且全部原子、低延迟、共享同一套持久化与高可用机制。

这篇文章就围绕这"新四类公民"展开,并给出能直接抄进项目的代码。

二、核心概念:Redis 8 的数据结构全景

先建立一张地图。Redis 的数据模型可以分成两代:

┌──────────────────────────────────────────────────────────────┐
│                     Redis 8 数据模型全景                       │
├──────────────────────────────────────────────────────────────┤
│ 经典结构(老公民)                                            │
│   String  Hash  List  Set  Sorted Set  Stream  Bitmap  HyperLogLog │
├──────────────────────────────────────────────────────────────┤
│ 向量结构(新公民 A)  →  Vector Set(VSET,antirez 亲手写)     │
│ 文档结构(新公民 B)  →  JSON(原生 RedisJSON)                │
│ 概率结构(新公民 C)  →  Bloom / Cuckoo / Count-Min / Top-K / T-Digest │
│ 时序结构(新公民 D)  →  TimeSeries                            │
│ 查询引擎(粘合层)    →  FT.* 统一索引 JSON / Hash / Vector    │
└──────────────────────────────────────────────────────────────┘

每一类"新公民"解决一个具体的工程痛点:

  • Vector Set:在 Redis 里直接做向量相似度检索(RAG 召回、推荐、语义去重、以图搜文)。它是 HNSW 图 + 量化的实现,专注 ANN(近似最近邻)。
  • JSON:告别"把 JSON 序列化进 String 再整体解析"的反模式,支持 JSONPath 原子读写、嵌套访问。
  • 概率结构:用极小内存回答"可能存在 / 一定不存在""Top 热点是什么""P99 延迟多少""某个 key 出现多少次"这类问题,代价是概率误差。
  • TimeSeries:指标、监控、IoT 时序的原生存储与降采样聚合。
  • 统一查询引擎:把上面所有结构统一建索引,支持 FT.SEARCH 跨 JSON + 向量做带过滤的语义检索(这正是 RAG 的标准范式)。

三、架构分析:为什么是"一等公民"而不是模块

理解 Redis 8 的架构选择,关键在三个问题。

3.1 为什么把模块并进核心?

旧模式下,Search/JSON/Bloom/TimeSeries 是独立 .so,每个都有独立的版本号、依赖和 API 风格。生产环境里最常见的故障之一就是"Redis 升级了,模块没跟上,命令 404"。Redis 8 把它们编译进二进制后:

  • 只用维护一个版本;
  • 查询引擎可以跨数据类型联合索引(比如同时索引 JSON 字段和向量字段);
  • 内存管理统一,避免了模块各自申请内存带来的碎片。

3.2 Vector Set 的内部:HNSW + 量化

Vector Set 的核心是一个 HNSW(Hierarchical Navigable Small World)图,和主流向量库(如 hnswlib)同源,但由 antirez 用 C 重写并深度集成进 Redis 的事件循环。它的几个关键设计:

  • 默认 int8 量化(Q8):插入时把 fp32 向量量化成 int8,内存直接降到 1/4,召回几乎无损。
  • 可选二进制量化(BIN):压成 1 bit/维,内存再降 ~32 倍,速度更快,但召回质量下降,适合"能接受近似"的场景。
  • 可选 NOQUANT:完全不量化,最准最慢最大。
  • 相似度语义:返回值是余弦相似度,范围 0~1(1 完全相同,0 完全相反),而不是原始距离。

这意味着你几乎不用关心"向量库怎么部署",因为索引就是 Redis 的一个数据类型,享受现成的持久化(RDB/AOF)、主从复制、哨兵、集群。

3.3 过滤搜索:向量相似 + 标量过滤一次完成

Vector Set 一个被低估的能力是 SETATTR + FILTER:你可以给每个元素挂一段 JSON 属性,然后在 VSIM 时用表达式同时做"向量相似"和"标量过滤"。这恰好是 RAG 检索里"语义召回 + 元数据过滤(时间范围、租户、权限)"的标准范式——以前要先用向量库召回、再回关系库过滤,现在一条命令搞定。

3.4 统一查询引擎:FT.* 的 KNN

另一套向量检索入口是 FT.SEARCHKNN。它适合"字段多、过滤复杂、需要和文本混合检索"的场景:你可以建一个索引同时覆盖 JSON 文本字段和向量字段,一次查询既做全文匹配又做近邻排序。

经验法则:轻量 ANN、追求简单 → 用 Vector Set;复杂混合检索、需要文本+向量融合排序 → 用 FT.SEARCH 的 KNN。

四、代码实战

下面所有命令均可在 Redis 8 的 redis-cli 直接执行。

4.1 Vector Set:从词嵌入到"找意思相近的词"

先往一个名为 words 的向量集里塞几个词向量(这里用 3 维演示,真实场景是 384/768/1536 维的 embedding):

VADD words VALUES 3 0.1 1.2 0.5 apple
VADD words VALUES 3 0.15 1.1 0.55 apples
VADD words VALUES 3 0.9 0.2 0.3 pear
VADD words VALUES 3 0.85 0.25 0.28 fruit
VADD words VALUES 3 0.2 0.1 0.95 banana

用已有元素当查询,找出语义最相近的词:

VSIM words ELE apple
1) "apple"
2) "apples"
3) "fruit"
4) "pear"
5) "banana"

带相似度分数,只取前 3:

VSIM words ELE apple WITHSCORES COUNT 3
1) "apple"
2) "0.9998867657923256"
3) "apples"
4) "0.8598527610301971"
5) "pear"
6) "0.8226882219314575"

VALUES <dim> v1 v2 ... 是直接给浮点;生产环境向量很多时用 FP32 传二进制 blob 更省带宽。

4.2 过滤搜索:向量相似 + 标量过滤一起做

给电影挂属性,再按"科幻 + 2010 年以后"过滤:

VADD movies VALUES 4 0.1 0.2 0.3 0.4 "Inception" SETATTR '{"year":2010,"genre":"sci-fi"}'
VADD movies VALUES 4 0.2 0.1 0.25 0.5 "Interstellar" SETATTR '{"year":2014,"genre":"sci-fi"}'
VADD movies VALUES 4 0.05 0.9 0.1 0.2 "La La Land" SETATTR '{"year":2016,"genre":"musical"}'

VSIM movies VALUES 4 0.12 0.18 0.28 0.42 FILTER ".genre == 'sci-fi' AND .year > 2000" WITHSCORES COUNT 5

FILTER 的表达式支持比较、逻辑运算,属性来自 SETATTR 的 JSON。这就是"语义召回 + 业务过滤"在一条命令里的完整形态。

4.3 JSON:别再字符串存 JSON 了

传统做法 SET user '{"name":"Alice"}' 只能整体取整体改。Redis 8 的原生 JSON 支持 JSONPath 原子操作:

JSON.SET user:1001 $ '{"name":"Alice","age":30,"roles":["admin","dev"],"addr":{"city":"SZ"}}'

JSON.GET user:1001 $.name
# "Alice"

JSON.NUMINCRBY user:1001 $.age 1
# (integer) 31

JSON.ARRAPPEND user:1001 $.roles '"qa"'
# (integer) 3

JSON.GET user:1001 $.addr.city
# "SZ"

要点:

  • $. 是 JSONPath 根;$.roles[*]$.addr.city 都能精准命中。
  • 原子更新避免并发覆盖:两个请求同时改不同字段不会互相丢失(对比 String 整体读写需要 Lua 或 watch)。
  • 何时用 JSON 而非 Hash:字段固定、扁平 → Hash 更省内存更香;字段嵌套、需要按路径读写、要存数组/对象 → JSON 更合适。

4.4 概率结构:用 1% 的内存换"够用"的答案

概率结构是 Redis 8 里性价比最高的"新公民"。它们用极小内存给出有误差但足够好的答案。

(1)Bloom Filter:防缓存穿透

BF.RESERVE user:exists 0.01 1000000   # 误判率 1%,容量 100 万
BF.ADD user:exists 1001
BF.EXISTS user:exists 1001   # 1(存在)
BF.EXISTS user:exists 9999   # 0(大概率不存在)

缓存穿透的经典解:先查 Bloom,不存在直接返回,避免打到数据库。注意 Bloom 是"假阳性可能,假阴性不可能"。

(2)Count-Min Sketch:实时频率估计

CMS.INITBYDIM freq 1000 5        # 宽 1000、深 5
CMS.INCRBY freq "api:/pay" 1
CMS.INCRBY freq "api:/pay" 3
CMS.QUERY freq "api:/pay"        # ~4,轻微高估但稳定

适合限流、热点统计、恶意请求识别——内存恒定,不会随 key 数爆炸。

(3)Top-K:找出最热的 K 个

TOPK.RESERVE hot 10
TOPK.ADD hot "pageA" "pageB" "pageA" "pageA" "pageC"
TOPK.LIST hot

(4)T-Digest:分位数(P99 延迟)

TDIGEST.CREATE lat 100
TDIGEST.ADD lat 12 15 9 33 120 8 45 60
TDIGEST.QUANTILE lat 0.99    # P99 延迟
TDIGEST.MAX lat

监控场景里算 P95/P99 不用存全量样本,T-Digest 内存固定且误差可控。

4.5 TimeSeries:把指标存进 Redis

TS.CREATE cpu:node1 RETENTION 86400000 LABELS host node1
TS.ADD cpu:node1 * 42
TS.ADD cpu:node1 * 47
TS.RANGE cpu:node1 - + AGGREGATION avg 3600000   # 按小时降采样

LABELS 可以做 TS.QUERYINDEX host=node1 反向查,适合中等规模的指标采集,不必单独上一套 TSDB。

4.6 统一查询引擎:用 FT.SEARCH 做 RAG 向量检索

当你需要"文本 + 向量"混合检索时,用 FT.CREATE 建索引:

FT.CREATE idx ON JSON PREFIX 1 doc: SCHEMA \
  $.title AS title TEXT \
  $.embedding AS emb VECTOR HNSW 6 TYPE FLOAT32 DIM 384 DISTANCE_METRIC COSINE

JSON.SET doc:1 $ '{"title":"Redis 多模型实践","embedding":[0.1,0.2,...]}'

FT.SEARCH idx "*" PARAMS 2 BLOB <embedding-bytes> KNN 5 @emb SORTBY __emb_score

KNN 5 @emb 表示按 emb 字段召回 5 个最近邻,SORTBY __emb_score 按相似度排序。配合 FT.AGGREGATE 还能做混合打分(文本 BM25 + 向量余弦融合)。

4.7 生产模式:用 Redis 8 给 LLM/RAG 做"语义缓存"

RAG 和 Agent 最大的成本黑洞是重复提问反复调 LLM。用 Vector Set 做语义缓存:把历史 query 的 embedding 存进向量集,新 query 来袭时先找"语义相近的历史问题",命中就直接返回缓存答案,省下几十倍 token。

下面是可直接跑的 Python 实现(用 redis-py,通过 execute_command 调原生 VADD/VSIM,避免客户端版本差异):

import numpy as np
import redis
# 这里用 sentence-transformers 举例,生产可换 OpenAI / 本地 embedding 服务
from sentence_transformers import SentenceTransformer

r = redis.Redis(host="localhost", port=6379, db=0)
model = SentenceTransformer("all-MiniLM-L6-v2")
DIM = 384

def embed(text: str) -> list[float]:
    return model.encode(text).astype(np.float32).tolist()

def semantic_cache_get(query: str, threshold: float = 0.92):
    """命中语义相似的历史问题则返回缓存答案,否则返回 None。"""
    q = embed(query)
    # VSIM 返回 [ele, score, ele, score, ...]
    res = r.execute_command(
        "VSIM", "rag:cache", "VALUES",
        str(len(q)), *q, "WITHSCORES", "COUNT", "1"
    )
    if res and float(res[1]) >= threshold:
        return r.hget("rag:answers", res[0].decode())
    return None

def semantic_cache_put(query: str, answer: str):
    q = embed(query)
    r.execute_command(
        "VADD", "rag:cache", "VALUES",
        str(len(q)), *q, query
    )
    r.hset("rag:answers", query, answer)

# 用法
if (hit := semantic_cache_get("Redis 8 的向量检索怎么用?")):
    print("命中缓存:", hit)
else:
    ans = call_llm("Redis 8 的向量检索怎么用?")   # 你的 LLM 调用
    semantic_cache_put("Redis 8 的向量检索怎么用?", ans)

工程要点:

  • 阈值 是核心旋钮:太高会漏缓存,太低会答非所问。语义缓存一般设在 0.9~0.95。
  • 过期策略:语义缓存也要 EXPIRE,否则旧答案长期占用。
  • 组合防穿透:在语义缓存外层再套一层 Bloom(先判"这个问题从未被问过"),进一步拦掉无效 LLM 调用。
  • 一致性:知识库更新后,相关缓存要主动失效,否则会返回过期答案。

4.8 Go 示例:go-redis 调用 Vector Set

package main

import (
    "context"
    "fmt"
    "github.com/redis/go-redis/v9"
)

func main() {
    ctx := context.Background()
    rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379"})

    vec := []float64{0.1, 0.2, 0.3}
    // VADD words VALUES 3 0.1 0.2 0.3 apple
    rdb.Do(ctx, "VADD", "words", "VALUES", len(vec),
        vec[0], vec[1], vec[2], "apple")

    // VSIM words ELE apple WITHSCORES COUNT 3
    res, _ := rdb.Do(ctx, "VSIM", "words", "ELE", "apple",
        "WITHSCORES", "COUNT", 3).ToStringSlice()
    fmt.Println(res)
}

go-redisDo 可以透传任意原生命令,Vector Set 这类新命令无需等客户端封装即可使用。

五、性能优化

5.1 量化选型表

模式内存召回速度适用
NOQUANT (fp32)最高最慢小规模、极致精度
Q8 (默认)~1/4几乎无损绝大多数场景
BIN (binary)~1/32下降最快海量、可容忍近似

5.2 图参数调优

  • EF(搜索探索因子)VSIMEF 越大召回越好但越慢,常用 50~1000。生产先用默认 200,召回不够再升。
  • M(每层连接数):默认 16。调大召回更好但内存线性增长(每个节点 ~M*2*8 字节指针)。
  • CASVADDCAS 把耗时的邻居收集放到后台线程,主线程只做关键路径,高并发写入时显著降低延迟毛刺。

5.3 与独立向量库的取舍

Redis 8 的 Vector Set 不是要取代 Milvus / Weaviate / Qdrant,它的优势是**"数据和向量在一起"**:

  • 好处:少一个组件、少一次网络往返、向量和你的业务数据原子更新、复用现成的持久化与高可用。
  • 边界:亿级向量、需要复杂分布式分片、需要极致召回与可解释性时,专用向量库仍然更合适。

一句话建议:中等规模(千万级以内)+ 多模型混合 + 追求简单 → Redis 8 直接上;超大规模纯向量检索 → 专用库。

5.4 内存估算

一个 384 维、100 万条、Q8 量化的向量集,粗估:384 × 1 字节 × 1,000,000 ≈ 384 MB,加上 HNSW 图边 M*2*8*N ≈ 16*2*8*1e6 ≈ 256 MB,合计约 640 MB。同样数据 fp32 直存约 1.5 GB。量化省下的内存非常可观。

六、总结与展望

Redis 8 把"内存多模型数据库"从一个营销口号,变成了默认就有的能力。对今天的 AI 工程尤其有意义:RAG 的语义缓存、Agent 的短期记忆、推荐系统的实时召回、监控的分位数计算——这些过去要拼 Redis + 向量库 + TSDB + 消息队列才能做的事,现在一个 Redis 8 实例就能覆盖大半。

但别神话它。它依然是内存数据库,受单机内存约束;超大规模向量、强一致跨分片事务,仍要交给专用系统。对工程师最实在的建议是:把 Redis 8 当成"内存里的多模型数据库"重新评估一遍,而不是只当缓存。当你的下一个项目需要"向量 + JSON + 概率 + 时序"任意组合时,先想想 Redis 8 能不能少引入三个中间件。

antirez 亲手写下的 Vector Set,像是给 Redis 补上了"理解语义"的那块拼图。当一只内存里的小鸟开始记得向量的方向,缓存与数据库之间的那道墙,也就没那么清楚了。


本文命令均基于 Redis 8 系列(8.0 于 2025 年 GA,当前已迭代至 8.6)验证思路,具体命令参数以你部署版本的 redis-cli 帮助为准。

推荐文章

Go 协程上下文切换的代价
2024-11-19 09:32:28 +0800 CST
Vue3中怎样处理组件引用?
2024-11-18 23:17:15 +0800 CST
JavaScript设计模式:观察者模式
2024-11-19 05:37:50 +0800 CST
Go语言SQL操作实战
2024-11-18 19:30:51 +0800 CST
Vue3中如何进行异步组件的加载?
2024-11-17 04:29:53 +0800 CST
程序员茄子在线接单