如果你曾经为一个很简单的需求发过愁——想在 Redis 里存一个 JSON 对象,按字段查一查、改一改,却发现在原版 Redis 里只能把整个 JSON 序列化成字符串塞进去,要改其中一个字段还得整串读出来、反序列化、改完再写回去,并发一高就提心吊胆。我当时就是被这个痛点逼着去了解 Redis Stack 的。简单说,Redis Stack 就是把官方维护的几个扩展模块和 Redis 本身打包在一起,装好之后你立刻能用的不止是 SET/GET,还有 JSON、Search、Time Series、Bloom 这些数据类型和能力。这篇文章是我在入门系列里整理的第六篇,目标读者和我当时一样:已经知道 Redis 基本用法,但不满足于只拿它当缓存,还想让它承担更多活的开发者。我会从安装、核心模块、组合实战到容易踩的坑,按我实际摸过的顺序写下来。
1. 先别急着装:Redis Stack 和原版 Redis 到底差在哪
1.1 它不是新数据库,而是“官方打包发行版”
很多人第一次听到 Redis Stack,容易误以为这是一个全新的数据库,或者以为它是个测试版,其实都不是。它更像是“发行版”的概念:底层依然是 Redis 本身,数据结构和命令完全兼容,只是官方把几个经过长期验证的模块,和 Redis 二进制打包在一起发布,让你不用自己去编译模块、配模块、担心模块和内核版本不匹配。
早期这些模块是独立的,比如 RedisJSON、RediSearch、RedisTimeSeries、RedisBloom,你要自己下载、编译、然后在 redis.conf 里通过 loadmodule 加载。听起来不复杂,但真做起来挺烦的:模块有各自的版本,和 Redis 版本有兼容矩阵,升级 Redis 的时候模块可能编译不过,生产环境里还得给每个节点重复操作一遍。Redis Stack 就是把这一坨事情收敛成“一个安装包搞定”。
另外早期 Redis Stack 还有自己独立的版本号,比如 6.2.x;后来官方把版本号直接和 Redis 对齐了,目前你看到 7.2 的 Stack 就是基于 Redis 7.2 的,这样理解起来更简单。
1.2 装上以后你会多出哪几类命令
模块不是藏起来的,它直接映射成新命令。你装完 Redis Stack,在 redis-cli 里敲 COMMAND COUNT 会发现命令数量比原版多出几百条。我按用途给你分一下:
- JSON 相关:JSON.SET、JSON.GET、JSON.ARRAPPEND、JSON.OBJKEYS 等,专门操作 JSON 文档。
- 搜索相关:FT.CREATE、FT.SEARCH、FT.AGGREGATE、FT.INFO,支持全文检索、字段过滤、聚合、排序,7.2 以后还带了向量相似度搜索。
- 时序相关:TS.CREATE、TS.ADD、TS.RANGE、TS.CREATERULE,支持时序数据的写入、范围查询、聚合降采样。
- 概率相关:BF.ADD、BF.EXISTS、BF.RESERVE,也就是布隆过滤器;还有 Cuckoo Filter、Count-Min Sketch、Top-K 这些概率数据结构。
早期版本里还打包过一个 Graph 模块,后来官方决定停掉这块的维护并把它从默认集合里摘了出去。所以现在你看到文档里的 Redis Stack 模块集合,就是 JSON、Search、Time Series、Bloom 四个为主。这个变化其实挺能说明问题的:不是模块越多越好,维护不住的东西就会被砍掉,你用的时候也别指望长期依赖某个冷门模块。
1.3 大量教程不提的版本合并细节
再强调一个容易忽略的点:Redis Stack 是“打包”,不是“改写”。也就是说你原来部署的原版 Redis 数据文件(RDB/AOF)理论上可以迁移到 Redis Stack,因为核心存储引擎还是同一个;反过来也一样。但如果你用了模块产生的新数据类型,比如 JSON 类型、索引结构,那旧版原版 Redis 是读不了的。
还有一点,Stack 安装包默认会启用全部模块。如果你只打算用 JSON,搜索模块也照样会加载,会占一点内存和启动时间。这一点在 Docker 容器、小内存 VPS 上尤其要注意,后面我会单独讲怎么按需启用。总之,对“Redis Stack 是什么”有个准确认知,后面不管装还是用,思路都会清晰很多。
2. 一条命令跑起来:安装选择和第一轮实测验证
2.1 最省事的 Docker 方式与端口细节
我自己最先试的就是 Docker,因为不需要在当前系统里折腾编译依赖。官方提供了 redis-stack 和 redis-stack-server 两个镜像,区别很简单:带 Server 的是精简服务端,没有图形界面;不带 Server 的额外包含 RedisInsight 图形管理工具。日常学习和开发,直接跑完整的镜像更方便:
docker run -d --name redis-stack \ -p 6379:6379 \ -p 8001:8001 \ redis/redis-stack:latest这里 6379 是 Redis 默认端口,8001 是 RedisInsight 网页管理工具的端口。有一点我得提醒:如果你是在自己电脑上跑,建议把 8001 绑定到本机,别直接暴露到局域网,RedisInsight 本身功能很全,暴露出去等于把数据管理入口公开了。更好的写法是:
docker run -d --name redis-stack \ -p 127.0.0.1:6379:6379 \ -p 127.0.0.1:8001:8001 \ redis/redis-stack:latest启动后可以用 docker logs 看启动日志,等出现 Ready to accept connections 就可以连了。
2.2 在 Linux 上直接装二进制
不用 Docker 的场景,比如你有一台长期运行的测试服务器,不想为 Redis 额外引入容器层,那可以直接从官方提供的中文文档界面找到对应系统的下载包。以 Ubuntu 这种 Debian 系为例,它支持通过 apt 仓库安装,装完之后会有 redis-stack-server 这个可执行文件。
如果下载的是 tar.gz 包,解压后目录里有 bin/redis-stack-server,直接运行即可:
./bin/redis-stack-server --daemonize yes它会自己去加载配置目录里的模块,不需要手工 loadmodule。要注意的是,Stack 的默认配置里持久化是打开的,如果你只是临时测试,不想写盘,启动时记得加一个空配置或者把 save 参数改成 "",否则测试完一堆 dump.rdb 堆在目录里。
2.3 装完之后先做这几个检查
装完别急着写业务代码,先确认模块真的加载了。在 redis-cli 里执行下面三条,基本就能看出状态:
redis-cli MODULE LIST redis-cli COMMAND COUNT redis-cli INFO serverMODULE LIST 会列出已经加载的模块名,正常情况下你能看到 json、search、timeseries、bf 这些。COMMAND COUNT 如果比原版多很多,说明新命令已经注册进去了。也可以直接试一条最简单的 JSON 命令,比如 JSON.SET test $ '"hello"' 然后 JSON.GET test,能正常返回说明 JSON 模块可用。我遇到过一种坑:某些第三方构建的“Redis Stack”镜像其实只是装了原版 Redis 再塞进去部分模块,模块不全,所以检查这一步一定不要省。
3. 四个高频模块逐个拆解:JSON、Search、Time Series、Bloom
3.1 RedisJSON:让“更新缓存里的一个字段”不再心惊胆战
先说我最常用的 JSON 模块。原版 Redis 里存 JSON 对象,普遍做法是 SET key '{"user":{"name":"张三","age":30}}',整串存取。问题在于:你只是想改 age 字段,也得先把整串 GET 回来,在应用里解析,修改,再 SET 回去。万一这个过程中另一个请求也在改同一个 key,就会出现“后写覆盖先写”的丢失更新问题。
RedisJSON 解决的是这个根子上的问题:它把 JSON 文档当成一种原生数据类型存储,允许你直接对文档内部的路径做原子操作。最常用的几个命令:
JSON.SET user:1001 $ '{"name":"张三","age":30,"skills":["Redis","Docker"]}' JSON.GET user:1001 $.name JSON.SET user:1001 $.age 31 JSON.ARRAPPEND user:1001 $.skills "Go"第二个命令里的 $.name 是 JSONPath 语法,$ 表示根节点,.name 表示取根节点下的 name 字段。这比 XPath 简单,但写法上也有不少讲究。比如想取 skills 数组的第一个元素,可以用 $.skills[0];想判断字段是否存在,可以用 JSON.TYPE user:1001 $.name。文档结构越复杂,越能体现出它比整串读写的优势。我实际感受是:会话信息、用户资料、配置快照这类“半结构化、经常局部修改”的场景,从原版迁移到 RedisJSON 后,并发更新问题基本消失,代码里也不需要再做序列化反序列化那一套了。
3.2 RediSearch:缓存层顺手把全文检索也干了
如果不了解 Redis Stack,你大概率不会把“搜索引擎”和“缓存”放在一个进程里。但 RediSearch 恰恰做了这件事。它支持哈希和 JSON 两种数据结构建索引,支持全文检索、数值范围过滤、排序、聚合、甚至向量相似度搜索。我最早用它是在一个商品服务里给商品名称和描述做站内搜索,效果确实比让我自己遍历所有商品再内存筛选好太多。
建索引的逻辑是:先告诉它哪些前缀的 key 要进索引,再告诉它哪些字段按什么类型处理。比如商品 key 都是以 product: 开头,里面有 name 和 price,那就这么建:
FT.CREATE idx:product ON JSON PREFIX 1 "product:" \ SCHEMA $.name AS name TEXT \ $.price AS price NUMERIC然后搜“机械键盘”并按照价格从低到高排序:
FT.SEARCH idx:product "@name:机械键盘" SORTBY price ASC LIMIT 0 10FT.SEARCH 返回的结果会把整条文档带回来,省去你查完索引再回 Redis 取数据的第二步。这一点在业务代码里非常舒服。
RediSearch 的聚合是另一个亮点,比如你想统计每个品牌下面有多少商品、平均价格多少,一条 FT.AGGREGATE 就能出结果,不需要把几万条商品拉到应用层 reduce。不过别因为功能多就贪心:建索引本身要占内存,字段越多索引越大,索引创建的瞬间还可能阻塞主线程,后面性能部分我会展开讲。
3.3 RedisTimeSeries:时序数据入库前先压缩
Time Series 模块解决的是监控指标、传感器数据这类不停追加时间戳数值的场景。如果直接往 Redis 里写几百万个字符串 key,内存开销大,查询也不方便。Time Series 模块提供专门的数据结构,每条记录是“时间戳 + 值”,底层按块存储,还支持自动压缩和聚合规则。
基本用法:
TS.CREATE metric:cpu_usage RETENTION 86400000 TS.ADD metric:cpu_usage 1718000000000 42.5 TS.RANGE metric:cpu_usage 1718000000000 1718003600000 AGGREGATION avg 60000第一条创建一条保留 24 小时的时序,超时的数据自动清理,不需要你再写定时任务扫 key。第三条是范围查询,按每 60 秒一个桶做平均值聚合,非常适合做监控图表的数据源。
我最喜欢的是 TS.CREATERULE,它能在写入的同时,自动把原始数据聚合成更粗粒度的另一条时序,比如把 1 秒粒度聚合成 1 分钟粒度,这样查询 30 天趋势的时候不用对几百万个点做聚合计算。这对监控系统来说几乎是刚需。要注意的是,时序数据的 key 数量一多,Docker 跑着没设内存上限很容易把宿主机内存吃爆,最好给每个时序设置合理的 RETENTION,宁可后面查不到太久远的数据,也别让它无限增长。
3.4 RedisBloom:极低内存挡住不存在的 key
Bloom 模块跟前面几个不同,它不存储原始数据,只维护一个概率结构,用来回答“某个 key 肯定不存在或可能存在”。最典型的场景是防缓存穿透:很多恶意请求会拿一串根本不存在的 ID 来打你的缓存接口,如果每个请求都穿透到数据库,数据库分分钟被拖垮。
用法非常简单:
BF.RESERVE bf:product 0.001 100000 BF.ADD bf:product 1001 BF.EXISTS bf:product 1001 BF.EXISTS bf:product 99999第一行的两个参数分别是期望误判率(0.1%)和预计要加入的元素数量(10 万个)。这两个参数会直接决定底层位数组的大小。有一个近似公式可以估算内存:位数组长度约等于-n * ln(p) / (ln 2)^2,n 是元素数量,p 是误判率。按 10 万个元素、0.1% 误判率算,大概不到 200KB,非常省。如果只加不涨,误判率又会慢慢升高,所以调参要给自己留余量。
另外 Bloom 模块里还有 Cuckoo Filter,支持删除元素,布隆过滤器本身不支持删除,所以如果你有频繁删除的需求,可以用 CF 系列命令。但大多数接口防穿透场景都是只增不减,BF 就够了。
4. 组合起来用一次:商品详情缓存加搜索引擎加防穿透
4.1 场景设定和需要解决的问题
单个模块的用法看明白之后,最有价值的其实是把它们组合起来。我拿一个典型的电商商品服务来举例,这个服务要解决四个问题:商品详情缓存、商品名称搜索、按价格筛选、以及防止不存在的商品 ID 打爆数据库。
原版 Redis 的做法是:详情用字符串缓存序列化后的商品 JSON,搜索要么靠数据库 LIKE 要么靠外部搜索引擎,防穿透要么用空值缓存要么自己维护一个布隆过滤器。每一样都能凑合用,但凑起来很零散。用 Redis Stack,一个进程全搞定,而且都是原生命令级操作,不需要额外中间件。
4.2 用 JSON 模块做详情缓存与局部更新
首先把商品详情存成 JSON 格式:
JSON.SET product:1001 $ '{"id":1001,"name":"机械键盘","brand":"keychron","price":399,"tags":["外设","无线"]}'然后用 JSON 命令做局部更新。比如用户下单后库存减一,不需要整串更新,只需:
JSON.NUMINCRBY product:1001 $.stock -1JSON.NUMINCRBY 是对数值类型字段做增减的原子操作,比“读出来改后写回”安全得多。这个场景如果只用原版 Redis,你多半会引入 Lua 脚本来保证原子性,现在用 JSON 模块原生命令,省了一层事。
4.3 用 Search 模块做标题搜索与价格筛选
商品数据在 JSON 里,搜索模块可以直接对它建索引,不需要另一份冗余结构。索引建好后,即便后面新增的 product: 开头的 key,也会自动进入索引,前提是它在 PREFIX 1 "product:" 指定的前缀范围内。
FT.CREATE idx:product ON JSON PREFIX 1 "product:" \ SCHEMA $.name AS name TEXT \ $.brand AS brand TAG \ $.price AS price NUMERIC \ $.tags[*] AS tags TAG这里有一个细节:品牌用 TAG 类型而不是 TEXT,因为品牌适合精确匹配和分类过滤,不适合分词。TAG 类型查询要用大括号包起来,比如:
FT.SEARCH idx:product "@brand:{keychron} @price:[300 500]" LIMIT 0 10这句话的意思是:品牌是 keychron、价格在 300 到 500 之间的商品,取前 10 条。TEXT 和 TAG 用错是新手最容易犯的错误,搞错了搜出来的结果要么是 0,要么把不该分词的词拆碎了。记住一个判断口诀:需要分词模糊搜用 TEXT,需要精确分类过滤用 TAG。
4.4 用 Bloom 模块挡掉恶意扫描
商品 ID 通常是自增数字,恶意请求很容易连续遍历不存在的 ID。做法是在查询流程前面加一道布隆过滤器:
- 请求进来,先 BF.EXISTS bf:product {id}
- 如果返回 0,说明这个 ID 大概率不存在,直接返回空,不打数据库
- 如果返回 1,说明可能存在,继续查缓存或数据库
- 数据库中查到后,把真实存在的 ID 加到布隆过滤器里
这里要注意布隆过滤器是存在判断,不是缓存,所以不会因为 TTL 过期导致数据丢失。初始化参数方面,我建议按业务三年内的商品总量再乘 2 来设置容量,因为加元素不加容量会让误判率上升。误判率我一般设 0.001,也就是千分之一,足够挡住绝大多数穿透请求。
4.5 补上 TTL、命名与数据一致性方案
组合方案里最容易被忽略的是命名规范和失效策略。我建议所有 key 统一用业务前缀加冒号分层,比如 product:1001、idx:product、bf:product,这样后续用 SCAN 做统计也好分类。
再一个就是 TTL。布隆过滤器不能设置 TTL,因为删除 key 相当于重建,会把所有已存在的 ID 判定消失。但商品详情 JSON 可以给一个短 TTL,比如 30 分钟,缓存自动过期后再从数据库加载。这里就有一个一致性问题:如果数据库里的商品改了,缓存 TTL 没到怎么办?比较粗暴也实用的做法是双写:修改数据库成功后,顺便调 JSON.SET 覆盖缓存;如果事务失败,就 DEL product:1001 让缓存失效。加上 TTL 兜底,基本能接受。
整个组合方案跑下来的感觉是:架构上少了一堆外部依赖,运维上只需要盯一个 Redis 实例,业务代码也不用维护两套不同中间件的客户端。对于中小型服务,这种“一个进程解决缓存、搜索、防穿透”的设计,性价比确实很高。
5. 那些看不见的内存和协议门槛,踩过才懂
5.1 RESP2 和 RESP3 的客户端兼容问题
Redis 从 6.0 开始支持 RESP3 协议,Redis Stack 的模块大量依赖响应类型更丰富的 RESP3。RESP2 时代返回的很多是字符串数组,RESP3 能返回 Map、Double 等结构化类型。问题是如果你的客户端库太老,默认协商的是 RESP2,会发现部分模块命令返回的数据格式诡异。
我遇到过的情况是:用某个老版本客户端去执行 FT.AGGREGATE,结果解析出来的结果全是字符串拼接,得不到结构化字段。排查了半天,最后发现是客户端桶不支持 RESP3。解决办法很直接:
- 升级客户端库到支持 RESP3 的版本,比如较新的 redis-py、Jedis、node-redis。
- 如果你的客户端在连接参数里能设置 protocol=3,手动声明。
- 实在不想升级,就还在 RESP2 模式下跑,但最好别把聚合这种复杂响应类型的命令用在生产代码里。
这个问题网上资料少,也不容易想到,但我身边的同事真踩过。所以装完 Stack,先查一查你用的客户端库版本和协议支持情况,能省下后面很多排查时间。
5.2 索引内存比数据本身还大,怎么预估
RediSearch 建索引的时候,索引结构会单独占用内存。字段越多的类型,索引越大,特别是 TEXT 类型要分词,分词后每个词都要建倒排表,内存开销很可观。可能你整个商品 JSON 原始数据只有 100MB,但索引内存反而到了 300MB。
这个现象在 INFO memory 里能看出来,used_memory 会比你预想高不少。要精确区分数据内存和索引内存,可以用 MODULE 的命令或者查看内存分析工具。但更务实的做法是在设计阶段就控制:
- 只给需要搜索的字段建索引,别把什么备注、日志字段统统塞进 SCHEMA。
- TEXT 字段能不建全文就不要建,没搜索需求的字段用 TAG 或者直接不索引。
- 考虑给索引设置上限,比如 FT.CONFIG 调整内存限制,超过阈值索引不再写入,避免整个 Redis OOM。
我在测试环境碰到过一次:商品详情就几千条,建了 6 个字段的索引,其中两个大字段用了 TEXT,结果内存直接比无索引时大了 4 倍。后来把那两个字段改成 TAG,或者去掉索引,内存立刻降下来。所以索引不是“建了就完事”,它是要用内存换查询速度的,你得知道这笔账怎么算。
5.3 阻塞与持久化的取舍:大对象操作要谨慎
Redis 单线程执行命令,Redis Stack 的模块命令也不例外。这就意味着:一条很大的 JSON 写入、一次很大的 FT.CREATE、一次复杂的聚合查询,都会阻塞其他命令的执行,导致整个实例的延迟抖动。
我实测过:往 RedisJSON 里写入一个几 MB 的 JSON 文档,解析和构建内部结构花了几十毫秒,这几十毫秒内其他读写命令全部排队。所以对于大 JSON 写入,要么在业务低峰期做,要么拆分成小块写入。Search 模块建索引也是一样,第一次 FT.CREATE 如果覆盖几百万个 key,最好是凌晨跑,或者分批用 PREFIX 范围控制。
持久化方面还有一个容易忽略的点:模块产生的数据结构和索引,在 RDB 持久化和 AOF 重写时都会被序列化。不同模块有自己的序列化格式,所以 Redis Stack 的 RDB 文件只能由同样模块版本兼容的实例加载。升级的时候如果只升级 Redis 版本而模块版本不匹配,可能直接加载失败。我的建议是:升级前先备份,最好先在一个临时实例上恢复一下 RDB 验证能不能起来,再滚到生产。
6. 什么时候该选 Stack,什么时候继续用原版
6.1 适合接入的三种典型架构
不是所有场景都该用 Redis Stack,过早把模块全启用了反而多占内存。我梳理下来,它特别适合这三类架构:
第一类是“缓存加搜索一体化”的业务。比如商品、文章、用户列表这类既要缓存又要检索的场景,Stack 把缓存和搜索放一起,省掉一个 Elasticsearch 或 Solr 的重型依赖。
第二类是“高并发防穿透加”的接口层。电商、内容站、账务系统,总会遇到扫 ID 的恶意流量,Bloom 过滤器加缓存是一套标准方案,Stack 内置之后不需要再单独引入其他服务。
第三类是“监控和缓存共存”的运维场景。应用服务本身用 Redis 做缓存,同时又要采集一些业务指标,比如接口调用量、耗时分布,Time Series 模块可以直接塞到同一个实例里,不一定非要再搭一套时序数据库。
6.2 集群模式和模块的兼容性现实
要提醒的是,Redis Stack 在单机、主从、哨兵模式下都比较好使,但在 Redis Cluster 模式下有些模块的命令支持是受限的。搜索模块在集群环境下可以做分布式搜索,但需要特殊的部署方式和配置;JSON 模块在集群模式下的跨 slot 操作也会受限。这些坑文档里都有说明,但很多人没仔细看,导致搭好了集群才发现某个命令报错。
我自己的体会是:如果业务量还没到必须上集群的地步,优先用单机配持久化,省心得多;如果真到了集群规模,先把模块命令的集群兼容性表拉出来一个一个对照业务场景,别等上线了再试错。
对于新项目,还有一个“反向选择”的思路:先用原版 Redis 把基本缓存跑通,确实遇到模块需求,再迁移到 Stack,迁移成本其实很低,因为底层数据格式兼容。不用一开始就把所有模块都用上。
6.3 我现在的选型心法和上手建议
整理一下我的选型心法:内存预算够、想省架构组件、团队愿意接受新命令,Redis Stack 值得用;反之如果只有几百 MB 内存、业务纯粹是 KV 缓存,老老实实跑原版 Redis,别为了“新功能”而扩容。
如果你决定上手,建议按这个顺序学:先把 JSON 模块用熟,因为它带来的收益最直观、风险最小;接着玩 Search,从最简单的 TAG 过滤开始,再碰 TEXT 全文索引;然后加 Bloom 防穿透;最后在需要监控指标时再碰 Time Series。不要一上来四个模块一起上,出了问题你都分不清是哪个模块引起的。
最后分享一个小习惯:我会在每次升级 Redis Stack 版本前,跑一遍自己整理的“模块冒烟脚本”,集中执行 JSON 写入、索引创建、时序写入、布隆判断这几类命令,确认返回结果符合预期再动生产。版本升级这种事,官方文档写得再清楚,也顶不上自己亲手验证一遍来得踏实。