返回文章列表

Redis 8 的扩展数据能力

系统梳理 Redis 8 Open Source 中 Search、JSON、Time Series、概率数据结构、Vector Set、Array 与 Go 调用边界,明确版本基线、成本和选型取舍。

第 10 章:Redis 8 的扩展数据能力

版本基线:Redis Open Source 8.8。 Redis 8.0 将原先分散在 Redis Stack 和独立模块中的 Search、JSON、Time Series、概率数据结构等能力合并到统一发行版;Vector Set 目前仍标注为 beta,Array 自 Redis 8.8 起提供,目前仍处于 preview。(Redis)

1. 本章定位

Redis 传统能力以 Key 定位和基础数据结构操作为核心。Redis 8 在此基础上进一步提供:

  • JSON 文档存储与局部更新。
  • HASH、JSON 上的二级索引和全文检索。
  • 时序数据存储、标签过滤和降采样。
  • Bloom Filter、Count-Min Sketch 等概率数据结构。
  • 向量检索。
  • 稀疏、按整数索引访问的 Array。

这些能力意味着 Redis 不再只能通过 Key 精确访问数据,但Redis 仍然不是关系数据库、搜索引擎、时序数据库和向量数据库的无条件替代品

本章的核心不是背诵新命令,而是回答三个问题:

  1. Redis 8 新增的能力解决什么问题?
  2. 它们相对于基础数据结构付出了什么 CPU、内存和运维代价?
  3. 什么时候应该使用 Redis,什么时候应该选择专用系统?

Redis 8.0 将 Redis Search、JSON、Time Series、五种概率数据结构和 Vector Set 统一放入 Redis Open Source 二进制发行物,不再要求用户分别匹配 Redis Server 与历史模块版本。升级时还必须注意 ACL 语义变化:原有 @read@write 等类别会覆盖更多新命令。(Redis)


2. 学习目标

完成本章后,应能够:

  1. 解释 Redis 8 与历史 Redis Stack、独立模块之间的关系。
  2. 根据对象结构、查询方式和更新模式,在 String、Hash、JSON 之间做出选择。
  3. 使用 Query Engine 为 HASH 或 JSON 建立索引,并解释索引对写入和内存的影响。
  4. 使用 Time Series 设计监控指标、设备采样或实时行情数据模型。
  5. 区分 Bloom、Cuckoo、Count-Min Sketch、Top-K 和 T-Digest 的不同语义。
  6. 区分 Vector Set 与 Query Engine 向量索引,并理解近似检索的召回率问题。
  7. 明确 Array 的 Redis 8.8+ 版本要求、preview 状态和使用边界。
  8. 在 Go 中安全调用这些命令,正确处理超时、redis.Nil、重试和并发。

2.1 本章边界与跳转

本章是 Redis 8 扩展数据能力的主章节。JSON、Hash 和 String 的对象存储选择会回扣[第 4 章](/blog/tech/Redis/04.String 的使用场景与底层实现/)与[第 5 章](/blog/tech/Redis/05.Hash 与 List/);Bloom Filter 在本章讲数据结构语义,在缓存穿透场景中的工程使用见第 18 章;Time Series、Query Engine 和 Vector Set 的性能、内存和上线风险,最终按第 20 章的方法做压测和观测。

3. 核心概念

3.1 Redis 8 与历史 Redis Stack

历史上,Redis Server 主要提供 String、Hash、List、Set、Sorted Set、Stream 等核心数据结构。JSON、全文检索、Time Series 和概率数据结构分别由 RedisJSON、RediSearch、RedisTimeSeries、RedisBloom 等模块提供。

Redis Stack 将 Redis Server 和这些模块打包在一起,但仍会带来:

  • Redis Server 与模块版本匹配问题。
  • Redis 与 Redis Stack 两套发行物的认知差异。
  • 部署、升级和安全配置分散。
  • 不同环境的功能集可能不一致。

Redis 8 的方向是 One Redis:上述能力直接成为 Redis Open Source 发行物的一部分。命令前缀仍然保留,例如 JSON.*FT.*TS.*BF.*,但使用官方 Redis 8 二进制时,不再需要单独安装这些历史模块。(Redis)

能力Redis 8 中的版本边界当前成熟度典型命令面试优先级
Query Engine / Redis SearchRedis 8.0 集成正式能力FT.CREATEFT.SEARCH重点加分
JSONRedis 8.0 集成正式能力JSON.SETJSON.GET重点加分
Time SeriesRedis 8.0 集成正式能力TS.ADDTS.RANGE加分
Bloom、Cuckoo、CMS、Top-K、T-DigestRedis 8.0 集成正式能力BF.*CF.*CMS.*Bloom 较重要,其余加分
Vector SetRedis 8.0+betaVADDVSIM前沿加分
ArrayRedis 8.8+previewARSETARGETARRING前沿加分
Hybrid SearchRedis 8.4+正式能力FT.HYBRID前沿加分
JSON 浮点数组格式、TS 多聚合器Redis 8.8+正式能力JSON.SET ... FPHA、多聚合 TS.RANGE版本加分

3.2 JSON:文档不是序列化字符串

使用普通 String 保存 JSON 时,Redis 只看到一段二进制数据:

product:42 -> {"name":"Headphone","price":699,"stock":20}

修改 stock 通常需要:

  1. GET 整个字符串。
  2. 客户端反序列化。
  3. 修改字段。
  4. 重新序列化。
  5. SET 整个字符串。

Redis JSON 则将其作为可寻址的层次化文档,可以通过 JSONPath 对局部路径读取和更新:

JSON.SET product:42 $ '{"name":"Headphone","price":699,"stock":20}'
JSON.GET product:42 $.price
JSON.NUMINCRBY product:42 $.stock -1

单条 JSON.NUMINCRBY 命令具有 Redis 单命令原子性,不会出现客户端读改写造成的并发覆盖。

JSON 适合:

  • 嵌套对象。
  • 数组和层次结构。
  • 只读取或更新文档局部字段。
  • 需要配合 Query Engine 建立索引的对象。

JSON 不自动提供:

  • 关系约束。
  • 外键。
  • Schema 校验。
  • 多文档 ACID 事务。
  • 复杂 Join。

JSON 会将反序列化后的文档保存在内存结构中,通常比原始序列化字符串占用更多内存;每个对象、数组、字符串和数值还会产生结构性开销。因此,不能根据 JSON 文本文件大小直接推算 Redis 内存。(Redis)

3.3 Search 与 Query Engine

Query Engine 在 HASH 或 JSON 之上建立二级索引,使客户端能够按内容而不是只按 Key 查询数据。

常见字段类型包括:

  • TEXT:分词、词干处理、模糊匹配和相关性评分。
  • TAG:精确标签匹配。
  • NUMERIC:数值范围查询。
  • GEOGEOSHAPE:地理位置和形状查询。
  • VECTOR:向量相似度查询。

例如:

FT.CREATE idx:product
  ON JSON
  PREFIX 1 product:
  SCHEMA
    $.name AS name TEXT
    $.price AS price NUMERIC SORTABLE
    $.tags[*] AS tags TAG

查询:

FT.SEARCH idx:product \
  '@tags:{audio} @price:[500 800]' \
  RETURN 2 $.name $.price \
  DIALECT 3

需要理解三个成本:

  1. 写放大:写入 HASH 或 JSON 时,还需要更新索引。
  2. 内存放大:原始文档和索引同时占用内存,SORTABLE 等选项还会增加额外结构。
  3. 查询放大:大结果集、深分页、排序和聚合可能消耗大量 CPU 和网络。

建立索引后,已有文档的首次扫描在后台完成,而之后新增或修改的文档通常同步更新索引,因此写命令返回时间包含索引维护成本。(Redis)

3.4 Time Series

Time Series 将数据建模为:

timestamp -> numeric value

并附加标签,例如:

service=payment
host=api-01
region=tokyo

它提供:

  • 保留时间 RETENTION
  • 压缩或非压缩 Chunk。
  • 重复时间戳策略。
  • 标签过滤。
  • 时间范围查询。
  • 聚合和降采样。
  • 多序列查询。
  • Compaction Rule。

例如保存 1 秒原始指标,同时生成 1 分钟平均值:

TS.CREATE metric:cpu:api-01 \
  RETENTION 86400000 \
  ENCODING COMPRESSED \
  DUPLICATE_POLICY LAST \
  LABELS service api host api-01

TS.CREATE metric:cpu:api-01:1m RETENTION 2592000000

TS.CREATERULE metric:cpu:api-01 metric:cpu:api-01:1m \
  AGGREGATION avg 60000

TS.ADD 在没有 Compaction Rule 时通常为 O(1);存在 M 条规则时,需要同步更新相关聚合,复杂度为 O(M)。范围查询成本与涉及的 Chunk 和返回样本数量有关。(Redis)

Time Series 解决的是实时工作集和近线分析问题,不等于替代所有监控或时序平台。大规模历史数据、复杂跨维度分析、长期低成本存储、告警规则管理和可视化生态,通常仍适合专用系统。

3.5 五种概率数据结构

概率数据结构通过牺牲部分精确性,换取更低的内存和更稳定的处理成本。

数据结构回答的问题主要误差是否支持删除典型场景
Bloom Filter某元素是否可能出现过可能假阳性不支持单元素删除缓存穿透、去重预判
Cuckoo Filter某元素是否可能存在可能假阳性支持,但有约束可撤销会员、优惠券集合
Count-Min Sketch某元素大约出现多少次碰撞导致高估不适合任意减计数流式频率统计
Top-K当前最频繁的 K 个元素是什么可能遗漏边界项不是精确集合热点商品、热门词
T-Digestp50、p95、p99 大约是多少分位数估计误差不用于删除单样本延迟分布、价格分布

Bloom Filter

Bloom Filter 使用位数组和多个哈希函数:

  • 返回“不存在”时,可以确定该元素没有被加入。
  • 返回“存在”时,只能表示“可能存在”。
  • 不应该把 Bloom Filter 的阳性结果直接当作业务事实。

容量估计过小会导致创建更多子过滤器,存在性检查需要依次检查多个子过滤器,从而增加延迟。(Redis)

Cuckoo Filter

Cuckoo Filter 保存元素指纹,并为指纹计算两个候选 Bucket。它通常比 Bloom 更适合需要删除的场景。

但删除有一个关键风险:如果删除了一个从未真正插入、只是因为假阳性而“看起来存在”的元素,可能破坏过滤器并产生假阴性。因此,只有确定元素曾经插入时才能执行 CF.DEL。(Redis)

Count-Min Sketch

Count-Min Sketch 使用多行计数器:

  1. 每行使用不同哈希函数。
  2. 每个事件更新每行对应的计数器。
  3. 查询时取多行计数器的最小值。

哈希碰撞只会使结果高估,不会低估。低频元素容易被碰撞噪声淹没,因此它更适合统计高频事件,而不是精确计费。(Redis)

Top-K

Redis Top-K 基于 HeavyKeepers 思路,将概率计数结构和维护 K 个候选元素的最小堆结合起来。它直接回答“最热的 K 个对象是什么”,而 CMS 更适合回答“指定对象大约出现了多少次”。(Redis)

T-Digest

T-Digest 不保存并排序全部样本,而是将相近样本压缩成若干质心,用较小空间估计:

  • 分位点。
  • 某个值对应的累计分布。
  • Rank。
  • 去除极端值后的均值。

COMPRESSION 越高,通常精度越高,但占用的内存和计算量也会增加。(Redis)

3.6 两种向量检索方式

Redis 8 同时存在两种向量检索模型。

Query Engine 向量索引

向量作为 HASH 或 JSON 文档中的一个字段,Query Engine 建立向量索引。

支持的索引类型包括:

  • FLAT:线性扫描,召回准确但成本随数据量增长。
  • HNSW:近似最近邻图索引,降低查询成本,但需要接受召回率和参数调优。
  • SVS-VAMANA:面向特定场景的向量索引类型。

它适合:

  • 文档具有多个结构化字段。
  • 需要全文、标签、数值条件和向量条件联合查询。
  • 需要一套文档 Schema。
  • 需要复杂的混合搜索。

(Redis)

Vector Set

Vector Set 类似“向量版本的 Sorted Set”:一个 Key 内保存多个成员,每个成员对应一个向量和可选 JSON 属性。

VADD vec:docs VALUES 3 0.1 0.2 0.3 doc:1 \
  SETATTR '{"year":2026}'

VSIM vec:docs VALUES 3 0.1 0.2 0.29 \
  COUNT 5 WITHSCORES \
  FILTER '.year >= 2025'

Vector Set 使用 HNSW,并支持 Q8、二值量化或不量化等模式。它的优点是模型简单、命令直接;缺点是功能和成熟度不如 Query Engine 文档索引,并且目前仍标注为 beta。(Redis)

Vector Set 是近似检索。召回率不足时可以提高 EF,检查量化方式,或者用 TRUTH 线性扫描结果作为验证基准,但验证成本很高。(Redis)

3.7 Array:Redis 8.8+ Preview

Array 是 Redis 8.8 新增的数据类型,目前仍处于 preview,API 和实现可能发生变化。

它将无符号整数索引映射到字符串:

index -> string

主要特点:

  • 索引范围为 02^64-1
  • 支持稀疏索引,不会为中间空洞分配全部元素。
  • 单索引访问通常为 O(1)
  • 支持连续或非连续批量设置。
  • 支持环形缓冲区。
  • 支持范围聚合、扫描和文本匹配。
  • ARLEN 表示最大索引加一,ARCOUNT 表示实际非空元素数量,两者在稀疏数组中可能相差很大。
ARSET events:1 0 login click purchase
ARGET events:1 1

ARMSET sparse:1 0 first 1000000 last
ARMGET sparse:1 0 1000000

ARRING recent:errors 100 "timeout"
ARLASTITEMS recent:errors 10

Array 不是 List 的全面替代:

  • 需要头尾 Push/Pop:优先考虑 List。
  • 需要字符串字段名:优先考虑 Hash。
  • 需要时间标签、Retention 和降采样:优先考虑 Time Series。
  • 需要长期稳定生产接口:当前应谨慎采用 Array。

Array 的范围操作复杂度取决于实际访问的槽位、切片或元素,并不都能因为“稀疏”而成为 O(1)。(Redis)

3.8 面试核心与加分项

层次应掌握内容
传统核心String、Hash、List、Set、Sorted Set、Stream、缓存、持久化、复制、Cluster
Redis 8 必要认知Redis 8 与 Stack 的关系;新增能力会增加索引、内存和运维成本
重点加分JSON 与 Hash/String 选型;Search 二级索引;Bloom Filter
中高级加分Time Series;CMS、Top-K、T-Digest;向量检索边界
前沿加分Vector Set beta;Array 8.8 preview;Hybrid Search
不合格表现把 Redis 8 新特性说成所有历史版本都支持;把近似结构说成精确结构;认为 Redis 能无条件替代 Elasticsearch 或专用数据库

4. 命令与 Go 使用方法

4.1 redis-cli 示例

JSON.SET product:42 $ \
  '{"name":"Noise Cancelling Headphone","price":699,"stock":20,"tags":["audio","wireless"]}'

JSON.GET product:42 $.name $.price
JSON.NUMINCRBY product:42 $.stock -1

FT.CREATE idx:product \
  ON JSON \
  PREFIX 1 product: \
  SCHEMA \
    $.name AS name TEXT \
    $.price AS price NUMERIC SORTABLE \
    $.tags[*] AS tags TAG

FT.SEARCH idx:product \
  '@tags:{audio} @price:[500 800]' \
  RETURN 2 $.name $.price \
  DIALECT 3

Time Series

TS.CREATE metric:cpu:api-01 \
  RETENTION 86400000 \
  ENCODING COMPRESSED \
  DUPLICATE_POLICY LAST \
  LABELS service api host api-01

TS.ADD metric:cpu:api-01 * 42.7

TS.RANGE metric:cpu:api-01 - + \
  AGGREGATION avg 60000

TS.MRANGE - + \
  AGGREGATION avg 60000 \
  FILTER service=api

概率数据结构

BF.RESERVE seen:request 0.001 1000000
BF.ADD seen:request req-100
BF.EXISTS seen:request req-100

CF.RESERVE unused:coupon 100000
CF.ADD unused:coupon C-100
CF.EXISTS unused:coupon C-100
CF.DEL unused:coupon C-100

CMS.INITBYPROB sku:frequency 0.001 0.01
CMS.INCRBY sku:frequency sku-42 1
CMS.QUERY sku:frequency sku-42

TOPK.RESERVE hot:sku 100
TOPK.ADD hot:sku sku-42 sku-43 sku-42
TOPK.LIST hot:sku WITHCOUNT

TDIGEST.CREATE latency:payment COMPRESSION 200
TDIGEST.ADD latency:payment 8.2 10.5 27.4 91.3
TDIGEST.QUANTILE latency:payment 0.5 0.95 0.99

Vector Set

VADD vec:document \
  VALUES 3 0.1 0.2 0.3 \
  doc:42 \
  SETATTR '{"year":2026}'

VSIM vec:document \
  VALUES 3 0.1 0.2 0.29 \
  COUNT 5 \
  WITHSCORES \
  FILTER '.year >= 2025'

Array

ARSET events:42 0 login click purchase
ARGET events:42 1

ARMSET sparse:42 0 first 1000000 last
ARMGET sparse:42 0 1000000

ARRING recent:42 100 timeout
ARLASTITEMS recent:42 10

4.2 go-redis/v9 示例

下面统一使用 Do 调用扩展命令。较新的 go-redis 版本已经为大量命令提供类型化 API,但 Do 便于同一示例覆盖 Query Engine、概率结构以及仍在 preview 的 Array。

初始化索引以及 BF.RESERVECMS.INITBYPROBTOPK.RESERVETDIGEST.CREATE 等容量配置,通常应由部署或迁移流程完成,而不是每个业务请求临时执行。

package main

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"time"

	"github.com/redis/go-redis/v9"
)

var ErrNotFound = errors.New("redis value not found")

type Product struct {
	Name  string   `json:"name"`
	Price float64  `json:"price"`
	Stock int      `json:"stock"`
	Tags  []string `json:"tags"`
}

func runCommand(
	parent context.Context,
	rdb *redis.Client,
	args ...any,
) (any, error) {
	if len(args) == 0 {
		return nil, errors.New("redis command is empty")
	}

	ctx, cancel := context.WithTimeout(parent, 800*time.Millisecond)
	defer cancel()

	result, err := rdb.Do(ctx, args...).Result()
	switch {
	case errors.Is(err, redis.Nil):
		return nil, fmt.Errorf("%v: %w", args[0], ErrNotFound)
	case err != nil:
		return nil, fmt.Errorf("%v failed: %w", args[0], err)
	default:
		return result, nil
	}
}

func execAndPrint(
	ctx context.Context,
	rdb *redis.Client,
	label string,
	args ...any,
) error {
	result, err := runCommand(ctx, rdb, args...)
	if err != nil {
		return err
	}
	fmt.Printf("%s: %#v\n", label, result)
	return nil
}

func run(ctx context.Context, rdb *redis.Client) error {
	product := Product{
		Name:  "Noise Cancelling Headphone",
		Price: 699,
		Stock: 20,
		Tags:  []string{"audio", "wireless"},
	}

	raw, err := json.Marshal(product)
	if err != nil {
		return fmt.Errorf("marshal product: %w", err)
	}

	// JSON.SET 是覆盖式、幂等性相对较好的写入。
	if _, err := runCommand(
		ctx, rdb,
		"JSON.SET", "product:42", "$", raw,
	); err != nil {
		return err
	}

	value, err := runCommand(
		ctx, rdb,
		"JSON.GET", "product:42", "$.price",
	)
	if errors.Is(err, ErrNotFound) {
		fmt.Println("product or path does not exist")
	} else if err != nil {
		return err
	} else {
		fmt.Printf("JSON.GET: %#v\n", value)
	}

	// 假设 idx:product 已由部署流程创建。
	if err := execAndPrint(
		ctx, rdb, "FT.SEARCH",
		"FT.SEARCH", "idx:product",
		"@tags:{audio} @price:[500 800]",
		"RETURN", 2, "$.name", "$.price",
		"DIALECT", 3,
	); err != nil {
		return err
	}

	// TS.ADD 使用 * 生成时间戳。发生超时后盲目重试可能产生第二个样本。
	if err := execAndPrint(
		ctx, rdb, "TS.ADD",
		"TS.ADD", "metric:cpu:api-01", "*", 42.7,
	); err != nil {
		return err
	}

	if err := execAndPrint(
		ctx, rdb, "BF.EXISTS",
		"BF.EXISTS", "seen:request", "req-100",
	); err != nil {
		return err
	}

	// 以下增量类命令通常不是天然幂等操作。
	incrementCommands := [][]any{
		{"CF.ADD", "unused:coupon", "C-100"},
		{"CMS.INCRBY", "sku:frequency", "sku-42", 1},
		{"TOPK.ADD", "hot:sku", "sku-42"},
		{"TDIGEST.ADD", "latency:payment", 27.4},
	}

	for _, command := range incrementCommands {
		if _, err := runCommand(ctx, rdb, command...); err != nil {
			return err
		}
	}

	if err := execAndPrint(
		ctx, rdb, "VSIM",
		"VADD", "vec:document",
		"VALUES", 3, 0.1, 0.2, 0.3,
		"doc:42",
		"SETATTR", `{"year":2026}`,
	); err != nil {
		return err
	}

	if err := execAndPrint(
		ctx, rdb, "VSIM",
		"VSIM", "vec:document",
		"VALUES", 3, 0.1, 0.2, 0.29,
		"COUNT", 5,
		"WITHSCORES",
		"FILTER", ".year >= 2025",
	); err != nil {
		return err
	}

	// Array 要求 Redis 8.8+,当前仍是 preview。
	if _, err := runCommand(
		ctx, rdb,
		"ARSET", "events:42", 0, "login", "click", "purchase",
	); err != nil {
		return err
	}

	if err := execAndPrint(
		ctx, rdb, "ARGET",
		"ARGET", "events:42", 1,
	); err != nil {
		return err
	}

	return nil
}

func main() {
	rdb := redis.NewClient(&redis.Options{
		Addr:                  "127.0.0.1:6379",
		DialTimeout:           500 * time.Millisecond,
		ReadTimeout:           800 * time.Millisecond,
		WriteTimeout:          800 * time.Millisecond,
		PoolTimeout:           time.Second,
		ContextTimeoutEnabled: true,

		// 示例包含计数、采样等非幂等操作,因此关闭隐式命令重试,
		// 由业务层根据命令语义决定是否重试。
		MaxRetries: -1,
	})
	defer func() {
		_ = rdb.Close()
	}()

	pingCtx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
	defer cancel()

	if err := rdb.Ping(pingCtx).Err(); err != nil {
		log.Fatalf("ping redis: %v", err)
	}

	if err := run(context.Background(), rdb); err != nil {
		log.Fatal(err)
	}
}

4.3 并发和错误处理说明

  • redis.Client 内部管理连接池,可以被多个 goroutine 并发使用。(GitHub)
  • 每个请求应使用自己的 context.Context,不要让所有请求共享同一个即将超时的 Context。
  • redis.Nil 表示目标 Key、字段或元素不存在,不等于 Redis 服务故障。(Redis)
  • Pipeline 对象本身不能被多个 goroutine 并发写入;每个 goroutine 应创建自己的 Pipeline,或使用受控的 Pipelined 回调。(GitHub)
  • go-redis 默认可能自动重试命令。对于 CMS.INCRBYTOPK.ADDTDIGEST.ADDTS.ADD * 等非幂等操作,超时并不代表服务端一定没有执行,因此盲目重试可能造成重复计数。MaxRetries: -1 可以关闭普通客户端的自动重试。(Redis)
  • DialTimeoutReadTimeoutWriteTimeout 是连接和 Socket 层防护;Context Deadline 是单次业务操作预算,两者应同时设置。(Redis)

5. 典型业务场景

能力适用场景不适用场景数据量要求一致性要求主要性能风险替代方案
JSONSession、商品快照、嵌套配置、实时用户画像复杂关系、跨文档事务、强 Schema工作集能放入内存单文档原子更新文档与结构开销、Big JSONHash、关系数据库、文档数据库
Query Engine实时商品过滤、内容检索、二级索引、混合搜索超大离线搜索、复杂分析型查询索引和文档均需容量规划接受 Redis 复制语义写放大、索引内存、排序和大结果集Elasticsearch/OpenSearch、数据库索引
Time Series秒级指标、IoT 最近数据、行情窗口多年历史、超高基数标签、复杂分析重点是热数据和近线数据接受异步复制风险标签基数、过长 Retention、聚合规则过多Prometheus、InfluxDB、TimescaleDB、ClickHouse
Bloom缓存穿透预判、粗粒度去重金融扣款、唯一约束最终判定超大集合且允许误差阳性结果必须回源确认容量不足、假阳性上升Set、数据库唯一索引
Cuckoo需要删除的近似成员集合无法确认元素是否真实插入预估容量较明确不能容忍假阴性时慎用错误删除导致过滤器损坏Set、数据库状态表
CMS高频事件、流量频次估计精确计费、库存、余额大流且只关心高频项允许高估低频噪声、重复消费Hash 计数、流处理系统
Top-K热门商品、热词、异常 IP必须得到完整精确排行K 较小、元素空间很大允许边界误差参数不当导致候选抖动Sorted Set、离线计算
T-Digestp95/p99 延迟、价格分布逐条审计、精确排序样本量巨大允许近似Compression 过低或窗口无界Histogram、原始样本、分析数据库
Vector Set单 Key 小中型语义检索、语义缓存大规模多租户复杂检索内存足以容纳向量和图接受近似召回维度、图索引、删除延迟Query Engine 向量、专用向量数据库
Array稀疏数值索引、环形窗口、最近事件需要稳定长期接口或字段名访问Redis 8.8+,谨慎生产继承 Redis 一致性范围扫描、preview 兼容性List、Hash、Time Series

6. 底层实现

6.1 总体成本模型

Redis 8 的扩展能力可以抽象为四类结构:

  1. 结构化值:JSON、Array。
  2. 原始数据加二级索引:Query Engine。
  3. 专用压缩数据结构:Time Series。
  4. 近似 Sketch 或图索引:概率结构、向量结构。

它们共同的特点是:减少应用层往返和计算,但将更多计算、内存和状态维护放到 Redis 服务端。

6.2 实现与复杂度

能力核心结构或算法典型复杂度空间成本设计原因
JSON反序列化后的层次化对象和数组取决于路径遍历和返回值大小通常高于序列化字符串支持局部访问和原子局部更新
TEXT/TAG 索引词项到文档集合的倒排索引单词查询通常与结果数相关文档之外增加索引避免全量扫描文档
NUMERIC/SORTABLE数值范围和排序辅助结构与候选数量、排序方式相关SORTABLE 增加内存降低范围查询和排序延迟
Time Series分块 Chunk、压缩样本、标签索引TS.ADD 无规则时通常 O(1)Retention、Chunk 和标签决定顺序样本可压缩并按时间高效查询
Bloom位数组、多哈希函数O(k),k 为哈希函数数由容量和误判率决定不保存原始元素,节省内存
CuckooBucket、指纹、双候选位置、搬迁与子过滤器和搬迁次数有关保存短指纹支持删除并保持较低空间成本
CMSdepth × width 计数矩阵每个元素约 O(depth)固定矩阵碰撞只造成高估,适合高频估算
Top-KHeavyKeepers 计数结构和最小堆更新与深度、增量有关显著小于保存所有元素只保留重频候选
T-Digest压缩质心添加 N 项约 O(N)由 Compression 和质心数量决定不保存全部样本即可估计分位数
Vector SetHNSW 图、量化向量、属性VADD 典型 O(log N)向量、图边和属性以近似图搜索降低全量扫描成本
Array稀疏、索引寻址的切片组织单点 O(1),范围操作与访问槽位有关与实际元素和切片组织相关支持高索引和稀疏访问,不分配全部空洞

Query Engine 的简单单词查询复杂度通常与结果集合大小有关;复杂查询还取决于查询词、集合交集以及最终返回结果数。即使词项定位较快,加载完整文档和网络返回仍然需要线性处理结果。(Redis)

6.3 为什么索引会同时提高读性能和降低写性能

没有索引时,按价格查询商品可能需要扫描所有文档:

O(文档总数)

有数值索引后,可以直接定位候选范围。但每次修改 price 时,Redis 必须:

  1. 修改原始 JSON 或 Hash。
  2. 从旧的索引位置移除文档。
  3. 将文档加入新的索引位置。
  4. 更新可能的排序、统计或向量结构。

因此,索引的本质是:

用写入成本和额外内存,换取查询成本下降。

索引字段越多、文档更新越频繁、向量维度越高,写放大越明显。

6.4 Vector Set 与向量索引的空间成本

以 768 维向量为例:

  • FP32 原始向量约为 768 × 4 = 3072 字节。
  • 还需要成员名称、对象头、HNSW 图边和分配器开销。
  • Q8 可以显著减少向量本身的存储,但会引入量化误差。
  • 二值量化进一步降低空间,但通常会牺牲更多召回率。

因此,“一百万条 3 KB 向量约 3 GB”只是向量裸数据估算,不能直接作为 Redis 实例容量;实际还必须加入图索引、Key、元数据、复制缓冲和内存碎片。

6.5 Array 的稀疏不等于所有操作都便宜

设置索引 110^12 不要求分配中间所有元素,因此单点访问仍然高效。

但下面的范围读取意味着用户要求返回大量位置:

ARGETRANGE key 0 1000000000000

即使实际元素很少,命令仍可能访问大量位置或生成巨大结果。因此:

  • 稀疏存储解决的是空间问题。
  • 范围命令仍需要控制扫描跨度和返回数量。
  • ARSCAN 更适合只遍历实际存在的元素。

Array 当前文档明确将单点操作和范围、扫描、聚合操作划分为不同复杂度类别。(Redis)


7. 高性能、高并发、高可用分析

7.1 高性能

CPU

主要 CPU 风险包括:

  • 全文分词和相关性排序。
  • 多条件交集、聚合和深分页。
  • Time Series 大时间范围聚合。
  • T-Digest、CMS、Top-K 的批量更新。
  • 高维向量检索和 HNSW 图维护。
  • Array 的大范围扫描或正则匹配。

即使 Query Engine 或部分扩展能力能够使用额外线程,它们仍与缓存命令共享机器的 CPU、内存带宽和网络资源。不要把低延迟缓存与重型检索无限制地部署在同一个实例。

内存

重点关注:

  • JSON 文档解析后内存。
  • Search 索引内存。
  • SORTABLE 字段。
  • Time Series 标签索引和长期 Retention。
  • 高维向量与 HNSW 图。
  • Bloom 容量和扩展子过滤器。
  • Redis 复制、持久化和内存碎片预留。

网络

扩展数据能力可以减少网络:

  • JSON.GET 只返回局部路径。
  • TS.RANGE ... AGGREGATION 在服务端降采样。
  • Query Engine 在服务端过滤候选。
  • T-Digest 只返回若干分位值。

但也可能扩大网络:

  • FT.SEARCH 返回大量完整文档。
  • JSON.GET key $ 返回 Big JSON。
  • 向量查询返回大量成员和属性。
  • Array 大范围读取返回大量 Nil 与字符串。

批处理

优先使用:

  • TS.MADD
  • BF.MADDBF.MEXISTS
  • CF.INSERT
  • CMS.INCRBY 多元素形式。
  • Pipeline。

批次不能无限增大。过大的批次会增加:

  • 单次事件循环占用时间。
  • 请求和响应内存。
  • 客户端超时风险。
  • 故障后的重试放大。

7.2 高并发

原子性边界

单条扩展命令仍然按照 Redis 命令语义原子执行,例如:

JSON.NUMINCRBY product:42 $.stock -1

但以下流程不是天然原子操作:

BF.EXISTS
数据库查询
BF.ADD

多个客户端可能同时看到“不存在”并重复执行数据库查询。Bloom Filter 只能减少无效请求,不能替代唯一约束和并发控制。

热点

常见 HotKey:

  • 所有请求都写同一个 TOPK
  • 所有延迟样本都写同一个 T-Digest。
  • 所有文档都放入一个 Vector Set。
  • 所有设备共用一个 Time Series。
  • 所有去重请求共用一个 Bloom Filter。

拆分方法包括:

  • 按租户、日期或业务维度分片。
  • 周期性合并 CMS 或 T-Digest。
  • 按日期建立 Bloom Filter。
  • 将向量分片后并行查询,再由客户端合并 Top-N。

Vector Set 横向扩展通常需要将数据拆到多个 Key 或实例,对每个分片执行 VSIM,再在客户端合并结果,因此读请求具有 Fan-out 成本。(Redis)

重试和重复执行

以下命令超时后不能简单重试:

  • CMS.INCRBY
  • TOPK.ADD
  • TDIGEST.ADD
  • TS.ADD key * value
  • ARRING
  • 使用自增语义的 JSON 命令。

客户端收到超时,只能证明没有在截止时间内收到结果,不能证明服务端没有执行。

常见改进:

  • 使用稳定时间戳代替 TS.ADD *
  • 在业务层加入事件 ID 和幂等记录。
  • 通过 Lua 或 Function 将幂等检查与更新合并。
  • 对只读操作和覆盖式写入采用有限、带退避的重试。
  • 对增量操作先确定幂等策略,再配置 go-redis 重试。

7.3 高可用

扩展数据结构不会改变 Redis 的基础一致性模型:

  • 主从复制通常是异步的。
  • 故障切换可能丢失尚未复制的最近写入。
  • 从副本读取可能看到旧数据。
  • Sentinel 或 Cluster 提供故障切换,不自动提供强一致。
  • 持久化不等于备份。

因此:

  • Bloom、CMS、Top-K 等最好能够从数据库、日志或消息流重建。
  • 不能只依赖 T-Digest 保存审计级原始数据。
  • 索引应被视为可重建派生状态,而不是唯一事实来源。
  • 重要 JSON 文档仍需评估 AOF、RDB、备份和数据恢复。
  • 升级前需要验证 RDB、AOF、复制和客户端对目标 Redis 版本及新数据类型的兼容性。
  • Array 仍是 preview,不应在无法快速回滚或迁移的核心链路中直接承担唯一数据存储。

8. 常见错误与生产事故

8.1 Redis 8 升级后普通账号突然可以执行 Search 或 JSON 写入

  • 现象:原本只有 +@read +@write 的账号可以执行 FT.SEARCHJSON.SET 等新命令。
  • 根因:Redis 8 扩展了已有 ACL 类别,新能力被纳入 @read@write 等分类。
  • 排查方法:执行 ACL GETUSER,对比升级前后的 ACL 分类和实际命令集合。
  • 修复方案:改用明确的命令白名单或增加 -@search-@json 等限制。
  • 如何预防:每次大版本升级都执行 ACL 回归测试,不把分类名称当作永久不变的权限集合。(Redis)

8.2 从 String JSON 迁移到 Redis JSON 后内存突然翻倍

  • 现象:数据量基本不变,但 used_memory 明显上涨,甚至触发淘汰。
  • 根因:Redis JSON 保存反序列化结构,嵌套对象、小字符串、数组和容器都会产生额外内存。
  • 排查方法:抽样比较原始 JSON 长度、MEMORY USAGE、文档节点数量和索引内存。
  • 修复方案:缩小文档、拆分冷热字段、删除无用路径、减少重复嵌套,并重新评估容量。
  • 如何预防:使用真实文档和真实 Redis 版本压测,不能按磁盘 JSON 文件大小等比例估算。(Redis)

8.3 创建 Search 索引后,查询暂时缺少历史文档

  • 现象:索引刚创建时只能查到部分数据,稍后又逐步正常。
  • 根因:已有文档需要后台初始扫描,索引建立不是瞬间完成。
  • 排查方法:查看 FT.INFO 的索引状态、文档数和索引进度。
  • 修复方案:迁移流程等待索引就绪后再切换流量;必要时先建立新索引、验证后再切换。
  • 如何预防:把索引建设作为数据迁移流程,而不是在业务高峰临时执行。(Redis)

8.4 增加多个 Search 字段后写延迟和内存同时上升

  • 现象JSON.SETHSET 的 p99 上升,Redis 内存快速增长。
  • 根因:每次写入都需要同步维护多个索引;大量 SORTABLE、TEXT 和 VECTOR 字段增加索引成本。
  • 排查方法:比较 FT.INFO 索引大小、写命令延迟、CPU 和字段更新频率。
  • 修复方案:只索引真实查询字段;去掉不需要的 SORTABLE;冷热索引分离;降低高频文档更新。
  • 如何预防:为每个索引字段记录业务查询依据,禁止“先全部索引以后再说”。

8.5 把 Bloom Filter 阳性结果当作“数据库中一定存在”

  • 现象:用户访问不存在的对象时,系统跳过数据库确认并返回错误业务状态。
  • 根因:Bloom Filter 存在假阳性;阳性只能表示可能存在。
  • 排查方法:对比 Bloom 结果与权威数据库,统计假阳性率和子过滤器数量。
  • 修复方案:阳性必须继续查询权威存储;只有阴性可以用于提前拒绝或快速返回。
  • 如何预防:在接口名称中明确使用 MayExist,避免命名成 Exists 后被业务代码误用。

8.6 Cuckoo Filter 删除不存在的元素后出现假阴性

  • 现象:某些真实存在的元素被 CF.EXISTS 判断为不存在。
  • 根因:业务根据假阳性结果对从未插入的元素执行 CF.DEL,破坏了与其他元素共享的指纹状态。
  • 排查方法:检查删除调用链,确认删除前是否有权威插入记录。
  • 修复方案:从权威数据重新构建过滤器;删除只允许由成功插入记录驱动。
  • 如何预防:不要以 CF.EXISTS == 1 作为允许删除的充分条件。(Redis)

8.7 go-redis 超时重试导致指标或频率被重复计算

  • 现象:流量没有增长,但 CMS 计数、Top-K 频率或 T-Digest 样本数偏高。
  • 根因:服务端已经执行命令,但响应在网络中超时;客户端随后再次执行增量命令。
  • 排查方法:对比客户端重试指标、服务端命令次数和上游唯一事件数。
  • 修复方案:关闭该类命令的透明重试;加入事件 ID;使用稳定时间戳或幂等脚本。
  • 如何预防:按命令语义配置重试,而不是为所有 Redis 命令使用统一策略。

8.8 Vector Set 召回率下降或删除时出现延迟尖峰

  • 现象:预期文档没有进入 Top-K;删除大型 Vector Set 时 Redis 延迟突然升高。
  • 根因:HNSW 是近似算法;EF 或量化配置偏低。大规模删除还需要处理图节点和链接。
  • 排查方法:使用 TRUTH 对比线性扫描结果;检查 EF、Q8/BIN 配置、向量归一化和删除规模。
  • 修复方案:提高查询 EF、减少量化损失、分批删除,或对超大集合采用分片及异步迁移。
  • 如何预防:上线前使用业务数据评估 Recall@K;避免直接 DEL 数百万元素的大 Vector Set。(Redis)

8.9 在核心业务中直接依赖 Array Preview

  • 现象:升级小版本后命令行为、客户端方法或兼容矩阵发生变化。
  • 根因:Array 仍是 preview,API 和实现可能调整。
  • 排查方法:检查 Redis Server、go-redis、云服务和备份恢复环境是否均支持同一版本。
  • 修复方案:建立 List、Hash 或 Time Series 的可回退模型;通过适配层封装 Array 命令。
  • 如何预防:preview 能力先用于非核心场景,不直接暴露到不可迁移的数据契约中。(Redis)

9. 方案选型与权衡

9.1 数据建模选型

需求推荐起点选择理由主要代价
简单对象、字段较少Hash结构简单、字段访问直接不适合深层嵌套和数组
整体缓存对象String + JSON/MessagePack内存和网络模型清晰局部更新需要读改写
嵌套对象和局部更新Redis JSONJSONPath、单命令局部更新内存结构成本较高
按内容检索 HASH/JSONQuery Engine二级索引、全文和范围查询写放大和索引内存
时间戳数值序列Time SeriesRetention、压缩、聚合、标签不适合通用事件载荷
精确成员集合Set没有概率误差内存随元素数量增长
超大集合存在性预判Bloom/Cuckoo极低内存假阳性及删除约束
精确排行榜Sorted Set精确分数和排序必须保存全部候选
流式热门项Top-K只保存热点候选结果近似
文档与向量混合查询Query Engine VECTOR结构化、全文、向量统一查询Schema 和索引成本
简单成员向量检索Vector Set命令模型简单beta、近似召回、分片复杂
稀疏整数索引Array直接索引和环形窗口Redis 8.8+ preview

9.2 Redis Search 与 Elasticsearch/OpenSearch

维度Redis Query EngineElasticsearch/OpenSearch
主要优势内存实时性、数据与索引接近、低延迟服务搜索生态、复杂全文检索、大规模磁盘索引
数据模型HASH、JSON 及其二级索引文档和分片索引
典型工作集热数据、实时服务路径大规模搜索语料、日志和分析
成本大量内存,写入与索引共享 Redis 资源更多组件、磁盘和集群运维
适合实时商品过滤、会话搜索、RAG 热索引大规模站内搜索、日志检索、复杂分析
不宜强行使用长期海量冷数据、重型聚合、极深分页极低延迟 Key-Value 缓存

不能只根据“谁更快”选型,应比较:

  • 数据能否长期放在内存。
  • 查询复杂度。
  • 全文相关性要求。
  • 写入更新频率。
  • 索引规模。
  • 团队运维能力。

9.3 Redis Time Series 与专用 TSDB

Redis Time Series 更适合:

  • 最近几小时或几天的实时指标。
  • 业务请求路径中的即时判断。
  • 低延迟读取最近窗口。
  • Redis 中已有标签和业务状态。

专用 TSDB 更适合:

  • 数月或数年的历史数据。
  • 高压缩比磁盘存储。
  • 大规模标签分析。
  • 复杂告警、查询和可视化生态。
  • 对象存储分层和离线分析。

常见组合是:

应用 -> Redis Time Series 作为实时窗口
     -> 消息流/采集器 -> 专用 TSDB 作为长期历史

9.4 Vector Set、Query Engine 与专用向量数据库

条件Vector SetQuery Engine VECTOR专用向量数据库
模型单 Key 成员集合HASH/JSON 文档字段向量集合及专用 Schema
过滤简单属性过滤TEXT、TAG、NUMERIC、GEO 等混合过滤通常支持丰富元数据过滤
成熟度betaRedis 正式查询能力取决于具体产品
扩展方式分 Key/分实例并由客户端合并Query Engine 集群能力专用分片与查询协调
适合规模小中型热向量集合实时文档和向量联合检索超大规模向量、复杂生命周期
主要风险召回、单 Key 热点、API 变化索引内存和写放大增加独立系统和数据同步

9.5 不能突破的选型红线

以下场景不应因为 Redis 8“已经支持”就强行使用:

  1. 要求精确唯一性或金融正确性:不能把 Bloom、CMS 或 Top-K 作为最终判定。
  2. 数据规模远超可接受内存成本:应考虑磁盘型或分层存储系统。
  3. 需要复杂 Join 和跨实体事务:优先使用关系数据库。
  4. 需要长期海量日志和重型聚合:优先考虑搜索或分析系统。
  5. 需要十亿级向量并具备复杂生命周期管理:应做专门容量、召回和成本评估。
  6. 无法接受接口变化:不要在核心数据模型中直接采用 preview Array。
  7. 无法从权威数据重建索引或 Sketch:不要把派生结构当作唯一事实来源。

10. 高频面试题

10.1 Redis 8 与 Redis Stack 是什么关系?

问题

Redis 8 已经有 JSON 和 Search,是否还要安装 Redis Stack?

推荐回答

现场回答:

Redis 8 将过去 Redis Stack 中的 Search、JSON、Time Series、概率数据结构等能力合并进统一的 Redis Open Source 发行物,使用官方完整二进制时不再需要单独安装对应模块。命令前缀仍然保留。Vector Set 是 beta,Redis 8.8 的 Array 仍是 preview,不能和成熟能力混为一谈。

展开:

历史 Stack 解决了模块打包问题,但仍形成了 Redis 与 Redis Stack 的发行物差异。Redis 8 的 One Redis 方向统一了部署和版本管理。升级时还要检查 ACL,因为 @read@write 所包含的命令范围发生了扩展。(Redis)

面试官追问

  • 从 Redis Stack 7.x 升级时应检查什么?
  • 为什么命令仍然使用 FT.*JSON.* 前缀?
  • ACL 可能出现什么破坏性变化?

常见错误回答

“Redis 8 完全重写了这些能力,和 Redis Stack 没有关系。”

评分点

  • 初级:知道 Redis 8 自带 JSON 和 Search。
  • 中级:能解释历史模块和统一发行物。
  • 高级:能补充升级、ACL、客户端和数据兼容验证。

10.2 JSON、Hash 和 String 应该如何选择?

问题

保存一个商品对象应该使用 String、Hash 还是 Redis JSON?

推荐回答

现场回答:

整体读写、结构简单且追求较低内存时,可以使用 String;字段扁平、需要单字段访问或计数时适合 Hash;存在嵌套对象、数组、局部路径更新或需要 JSON 索引时适合 Redis JSON。最终要根据访问模式,而不是根据“对象本来是 JSON”做选择。

展开:

Redis JSON 的优势是 JSONPath 和局部原子更新,代价是结构化内存通常高于序列化字符串。Hash 不适合天然嵌套结构;String 的局部更新能力弱,但序列化格式选择更自由。(Redis)

面试官追问

  • Big JSON 会产生什么问题?
  • 将对象拆成很多 String Key 是否更好?
  • 如何处理 Schema 变更?

常见错误回答

“JSON 一定比 String 更省网络和内存。”

评分点

  • 初级:能说出三种方式。
  • 中级:能根据局部更新、嵌套和内存选型。
  • 高级:能考虑热字段拆分、Schema 版本和索引写放大。

10.3 Query Engine 为什么会让读取更快、写入更慢?

问题

为 JSON 建立索引后,为什么 JSON.SET 延迟可能上升?

推荐回答

现场回答:

索引用额外内存保存字段到文档的映射,从而避免查询时扫描全部文档。但文档写入后必须同步维护相关 TEXT、TAG、NUMERIC 或 VECTOR 索引,因此索引越多、字段更新越频繁,写放大越明显。

展开:

已有数据的初始扫描可在后台完成,但新增或修改文档通常在写入路径上完成索引更新。SORTABLE、全文索引和向量索引都会产生额外成本。(Redis)

面试官追问

  • 哪些字段不应该建立索引?
  • 如何进行索引迁移?
  • 为什么深分页危险?

常见错误回答

“索引只占内存,不影响写入。”

评分点

  • 初级:知道索引加速查询。
  • 中级:能说出写放大和内存放大。
  • 高级:能讨论索引迁移、字段基数、分页和容量隔离。

10.4 Bloom Filter 和 Cuckoo Filter 有什么区别?

问题

需要判断优惠券是否可能未使用,应该选 Bloom 还是 Cuckoo?

推荐回答

现场回答:

两者都可能产生假阳性。Bloom 结构简单、插入性能和扩展性通常较好,但不支持安全的单元素删除;Cuckoo 保存指纹和 Bucket,支持删除,更适合状态可撤销的集合。但删除前必须确定元素确实插入过,否则可能造成假阴性。

面试官追问

  • Bloom 为什么不能直接清除某几个 Bit?
  • 假阳性和假阴性分别是什么?
  • 容量估计过小会怎样?

常见错误回答

“Cuckoo 支持删除,所以可以看到存在后直接删除。”

评分点

  • 初级:知道两者都是近似成员判断。
  • 中级:知道删除和假阳性差异。
  • 高级:能说明子过滤器、指纹碰撞及错误删除风险。

10.5 Count-Min Sketch 与 Top-K 如何选择?

问题

统计热门商品为什么不直接全部使用 CMS?

推荐回答

现场回答:

CMS 适合查询指定商品大约出现多少次,但它本身不直接保存完整的热门商品候选集合。Top-K 在概率计数基础上维护 K 个重频候选,适合直接得到热门元素。若需要精确排行榜,应使用 Sorted Set 或离线计算。

面试官追问

  • CMS 为什么只会高估?
  • Top-K 能否返回所有元素?
  • 重复消费会造成什么影响?

常见错误回答

“CMS 和 Top-K 是同一个结构,只是命令名不同。”

评分点

  • 初级:能区分频率查询和热门列表。
  • 中级:能说明近似误差。
  • 高级:能结合事件幂等、窗口化和精确排行设计。

10.6 T-Digest 能否用于精确计算 p99?

问题

监控接口延迟时,T-Digest 的 p99 是否就是精确值?

推荐回答

现场回答:

不是。T-Digest 通过压缩质心估计分位点,适合大量样本下的低内存近似统计。它适合监控和趋势判断,不适合审计级精确计算。需要根据误差要求选择 Compression,并保留必要的原始数据或分层存储。

面试官追问

  • p99 和平均值分别反映什么?
  • Compression 提高后有什么代价?
  • 如何按分钟或租户划分 T-Digest?

常见错误回答

“T-Digest 保存了排序后的所有原始值。”

评分点

  • 初级:知道用于分位数。
  • 中级:知道结果近似。
  • 高级:能设计窗口、合并、重建和精度验证。

10.7 Time Series 与 Sorted Set 有什么区别?

问题

时间戳可以作为 Sorted Set Score,为什么还需要 Time Series?

推荐回答

现场回答:

Sorted Set 是通用的分数排序结构,适合任务调度、时间线和排行榜。Time Series 针对时间戳数值样本提供压缩、Retention、重复时间戳策略、标签查询、聚合和降采样。只是保存少量时间点时 ZSet 足够;大量连续数值采样更适合 Time Series。

面试官追问

  • Time Series 能保存复杂事件吗?
  • Retention 是如何触发的?
  • Compaction Rule 对写入复杂度有什么影响?

常见错误回答

“Time Series 就是内部使用 Sorted Set。”

评分点

  • 初级:知道两者都能按时间查询。
  • 中级:能说明压缩、标签和降采样。
  • 高级:能讨论乱序样本、重复策略和长期存储边界。

10.8 Vector Set 与 Query Engine 向量索引有什么区别?

问题

Redis 已经支持 VECTOR 字段,为什么又提供 Vector Set?

推荐回答

现场回答:

Query Engine 的向量索引附着在 HASH 或 JSON 文档上,适合向量与全文、标签、数值等条件联合查询。Vector Set 是一个更直接的成员到向量集合,命令类似 Sorted Set,适合简单语义相似检索,但目前仍是 beta,过滤、Schema 和扩展能力更受限。

面试官追问

  • 哪种方式适合 RAG 文档?
  • Vector Set 如何跨分片查询?
  • 两者是否都负责生成 Embedding?

常见错误回答

“Vector Set 已经完全替代了 Search 向量索引。”

评分点

  • 初级:知道二者都支持向量。
  • 中级:能区分文档索引和单集合模型。
  • 高级:能讨论过滤、分片、召回和生产成熟度。

10.9 向量检索为什么会漏掉预期结果?

问题

某文档与查询向量很相似,但没有出现在 VSIM Top-10 中,如何排查?

推荐回答

现场回答:

HNSW 是近似检索,结果受 EF、图构建参数、量化方式、向量归一化和过滤条件影响。应先确认向量维度和编码,再提高 EF,并用 TRUTH 线性扫描对比 Recall@K。如果只有量化模式下遗漏,说明需要重新平衡内存、延迟和召回率。

面试官追问

  • FLAT 和 HNSW 如何选择?
  • Q8 与 BIN 的代价是什么?
  • Filter 很稀有时为什么更难找够结果?

常见错误回答

“Redis 向量查询一定返回数学上最接近的 K 个向量。”

评分点

  • 初级:知道向量检索可能近似。
  • 中级:知道 EF 和量化。
  • 高级:能建立 Recall@K、p99 和内存的联合基准。

10.10 Array 能否替代 List 和 Hash?

问题

Redis 8.8 有 Array 后,List 和 Hash 是否过时了?

推荐回答

现场回答:

不能。Array 适合数值索引、稀疏索引、环形窗口和范围聚合;List 适合头尾 Push/Pop 和队列;Hash 适合字段名到值的映射。Array 当前还是 preview,因此更不能作为所有列表和对象场景的默认替代。

面试官追问

  • ARLENARCOUNT 有什么区别?
  • 稀疏 Array 为什么不会为中间空洞分配全部空间?
  • 什么情况下应该选择 Time Series 而不是 Array?

常见错误回答

“Array 的所有操作都是 O(1)。”

评分点

  • 初级:知道 Array 自 8.8 起提供。
  • 中级:能区分数值索引、序列位置和字段名。
  • 高级:能说明 preview 风险、范围复杂度和迁移适配层。

10.11 Go 客户端如何重试这些扩展命令?

问题

TS.ADDCMS.INCRBY 超时后,可以直接重试吗?

推荐回答

现场回答:

不能统一直接重试。客户端超时不代表服务端没有执行。查询和覆盖式幂等写通常可以有限重试;自增、采样、Top-K、T-Digest 等命令可能重复生效。应关闭透明重试或通过事件 ID、稳定时间戳、Lua 幂等检查等方式控制。

面试官追问

  • go-redis 如何关闭自动重试?
  • TS.ADD key * value 如何提高幂等性?
  • Context Timeout 和 ReadTimeout 有什么区别?

常见错误回答

“Redis 单命令原子,所以重试一定安全。”

评分点

  • 初级:知道设置 Context 和超时。
  • 中级:能区分幂等与非幂等命令。
  • 高级:能设计事件去重、退避、指标和未知执行状态处理。

10.12 上线 Search 后 Redis 内存和延迟同时恶化,如何排查?

问题

建立新索引后,内存上涨、写 p99 上升、查询也偶尔超时,你会如何排查?

推荐回答

现场回答:

先确认索引是否仍在初始扫描,再使用 FT.INFO 查看文档数、索引大小和字段配置;同时检查 CPU、内存、慢查询、返回结果数及写入频率。重点排查过多 TEXT/VECTOR/SORTABLE 字段、Big JSON、深分页、大范围排序和写热点。

展开:

索引本身增加内存;同步索引维护增加写延迟;返回完整文档或进行大排序又会增加 CPU 和网络。修复通常不是简单扩容,而是减少索引字段、限制查询、拆分实例或将重型搜索迁移到专用集群。

面试官追问

  • 如何做到索引无损切换?
  • 为什么 LIMIT 很小仍可能查询很重?
  • 缓存实例和搜索实例是否应该隔离?

常见错误回答

“Redis 是内存数据库,加内存就一定能解决。”

评分点

  • 初级:会检查内存和慢查询。
  • 中级:能关联索引写放大和结果集。
  • 高级:能提出容量隔离、双索引迁移、限流和降级方案。

11. 一分钟面试回答

Redis 8 将历史 Redis Stack 中的 Search、JSON、Time Series 和 Bloom 等能力统一到 Redis Open Source 发行版。JSON 解决嵌套文档和局部原子更新;Query Engine 为 HASH 和 JSON 提供全文、标签、数值和向量二级索引,但会产生索引内存和写放大;Time Series 针对时间戳数值提供压缩、Retention、标签和降采样;Bloom、Cuckoo、CMS、Top-K、T-Digest 用近似结果换取低内存,不能承担精确业务判定。向量方面,Query Engine 适合文档、结构化条件和向量混合查询,Vector Set 模型更简单但仍是 beta。Array 自 Redis 8.8 起提供,目前仍是 preview,适合稀疏数值索引和环形窗口。生产选型时要重点评估内存、写放大、召回或误差、重试幂等、高可用语义以及是否应该改用 Elasticsearch、TSDB 或专用向量数据库。


12. 本章总结

  1. Redis 8 的核心变化不是增加几个命令,而是将原 Redis Stack 的扩展能力统一进 Redis Open Source。
  2. JSON 适合层次化文档和局部更新,但其内存通常高于序列化 String。
  3. Query Engine 用写放大和索引内存换取内容查询能力,索引字段不能无节制增加。
  4. Time Series 适合实时数值窗口、Retention 和降采样,不等于长期海量时序平台。
  5. Bloom、Cuckoo、CMS、Top-K 和 T-Digest 都必须先明确误差语义,再决定是否可用于业务。
  6. Vector Set 与 Query Engine 向量索引是两种不同的数据模型;向量检索必须评估召回率,而不是只看平均延迟。
  7. Array 要求 Redis 8.8+,当前仍是 preview,应保留替代模型和迁移能力。
  8. 扩展数据类型继承 Redis 的复制、故障切换和持久化边界,不会自动获得强一致。
  9. Go 客户端重试必须区分只读、覆盖式写入和非幂等增量命令。
  10. Redis 8 能减少系统组件,但不代表应该把所有搜索、分析、时序和向量数据都集中到 Redis。

13. 自测清单

  1. Redis 8 与 Redis Stack 的关系是什么,为什么升级时必须重新审查 ACL?
  2. 一个嵌套商品对象分别使用 String、Hash、JSON 时,各有什么优缺点?
  3. 为什么建立 Search 索引后,原始 JSON 写入延迟可能增加?
  4. Bloom Filter 返回存在和不存在时,分别能得出什么结论?
  5. 为什么不能根据 CF.EXISTS 的阳性结果直接执行 CF.DEL
  6. CMS、Top-K 和 Sorted Set 分别适合哪种频率或排名问题?
  7. Time Series 的 Retention、Duplicate Policy 和 Compaction Rule 分别解决什么问题?
  8. Vector Set 和 Query Engine VECTOR 索引在数据模型、过滤和扩展方式上有什么区别?
  9. TS.ADD *CMS.INCRBY 超时后,为什么重试可能造成数据偏差?
  10. Array 的 ARLENARCOUNT 为什么可能相差巨大,哪些场景仍应选择 List、Hash 或 Time Series?

14. 官方资料

  1. Redis Open Source 8.0 Release Notes。(Redis)
  2. Redis 8.0 新特性与 One Redis 说明。(Redis)
  3. Redis Open Source 8.8 发布说明。(Redis)
  4. Redis JSON 内存使用说明。(Redis)
  5. FT.CREATE 命令与索引字段说明。(Redis)
  6. FT.SEARCH 命令、复杂度和分页说明。(Redis)
  7. JSON 文档索引流程。(Redis)
  8. Redis Time Series 文档。(Redis)
  9. Bloom Filter 与 Cuckoo Filter 文档。(Redis)
  10. Count-Min Sketch、Top-K 和 T-Digest 文档。(Redis)
  11. Query Engine 向量索引说明。(Redis)
  12. Redis Vector Set 文档。(Redis)
  13. Redis Array 数据类型文档。(Redis)
  14. go-redis 生产超时和重试指南。(Redis)
  15. go-redis 错误处理与并发安全说明。(GitHub)