分布式专题——7 Redis Stack扩展功能
1 了解 Redis 产品
-
官网:Redis 产品 | 文档 - Redis 文档;
-
Redis Software 是 Redis 的基础软件,是 Redis 各项功能得以实现的核心。用户可以在自己的服务器环境中安装和配置 Redis Software;
-
Redis Cloud 是 Redis 的托管云服务,由 Redis 官方提供。它简化了 Redis 的部署、管理和扩展流程;
-
Redis for Kubernetes 是专门为在 Kubernetes 容器编排平台上部署 Redis 而设计的产品;
-
Redis Open Source 是 Redis 的开源版本,用户可以免费使用和修改其源代码;
-
Redis Insight 是一个可视化工具,用于管理和监控 Redis 数据库;
-
-
还有 Redis Stack,其是 Redis 的扩展,添加了现代数据模型和处理引擎,为开发者提供完整的开发体验, 是 Redis 官方推出的一站式解决方案。它整合了 Redis 最先进的模块和功能,是构建实时应用的理想平台。官网:关于 Redis Stack - Redis - Redis 文档。
2 申请 RedisCloud 实例
- Redis Cloud 快速入门_Redis中文网。
3 Redis Stack 体验
3.1 RedisStack 有哪些扩展?
-
Redis 的官网上,单独构建了 Redis 的指令页面:命令 | 文档 - Redis 文档,在这个页面可以直接搜索相关的功能
-
在 redis-cli 客户端也可以使用
module list
指令查看当前 Redis 服务中有哪些扩展; -
Redis Stack 的这些扩展功能,可以手动添加到自己的 Redis 服务中,但是并不是全部都是必须的,可以使用 RedisCloud 上的实例先完整体验一下,再考虑要不要使用这些扩展;
-
下面介绍几个常见的扩展模块。
3.2 RedisJSON
3.2.1 简介
-
RedisJSON | NoSQL 文档数据库 - Redis 文档;
-
RedisJSON 是 Redis 的一个扩展模块,它为 Redis 带来了对 JSON 数据的原生支持。这意味着我们能直接把 JSON 数据存储到 Redis 里,而且借助丰富的命令集,能对 JSON 数据进行高效的查询和操作。它不仅让数据处理流程变得更简单,还极大地提高了处理 JSON 数据的性能。
3.2.2 作用
-
RedisJSON的常用指令,在官网的 Commands 页面按 JSON 组搜索就能看到:
-
RedisJSON 为 Redis 新增了对 JSON 数据类型的支持,并且能对 JSON 数据快速执行增、删、改、查等操作:
-
设置 JSON 数据:通过
JSON.SET
命令可以设置一个 JSON 数据,比如JSON.SET user $ '{"name":"shisan","age":18}'
,其中$
表示 JSON 数据的根节点; -
获取 JSON 数据及属性:
JSON.GET
可用于获取 JSON 数据或其特定属性,像JSON.GET user
能获取整个user
的 JSON 数据,JSON.GET user $.name
则能获取user
中name
属性的值; -
查看数据类型:
JSON.TYPE
命令可以查看 JSON 数据或其属性的类型,例如JSON.TYPE user
显示user
是object
类型,JSON.TYPE user $.name
显示name
是string
类型; -
修改 JSON 数据:
JSON.NUMINCRBY
可对数值类型的属性进行增减操作,如JSON.NUMINCRBY user $.age 2
能将age
增加 2 ; -
添加字段和元素:
JSON.SET
可以添加新的字段,JSON.ARRAPPEND
能在 JSON 数组中添加元素,比如向hobbies
数组添加"swimming"
; -
查看 JSON 元素个数:
JSON.OBJLEN
可查看 JSON 对象中某个字段的元素个数,如JSON.OBJLEN user $.address
; -
查看所有键和删除字段:
JSON.OBJKEYS
能查看 JSON 对象的所有键,JSON.DEL
可删除 JSON 中的某个字段,比如JSON.DEL user $.address
;
-
3.2.3 优势
-
JSON 是现代应用程序里常用的数据类型,即便没有 RedisJSON 插件,人们也常以 JSON 格式缓存复杂数据,像分布式场景下用户登录功能,会把用户信息以 JSON 字符串存到 Redis 替代单体应用的 Session,实现统一登录状态管理。而 RedisJSON 插件的出现,让这种数据管理变得顺理成章,很好地契合了实际应用中对 JSON 数据处理的需求;
-
使用 RedisJSON 插件相比用 String 管理 JSON 数据,还能带来一些很明显的优势:
-
存储与读写性能:RedisJSON 底层以高效的二进制格式存储数据,相较于简单的文本格式,二进制格式进行 JSON 读写时性能更高,也更节省内存。根据官网性能测试报告,其读写 JSON 数据的性能已能与 MongoDB、ElasticSearch 等传统 NoSQL 数据库媲美;
-
查询效率:RedisJSON 采用树状结构存储 JSON,这种结构能快速访问子元素,和传统文本存储方案相比,能更高效地执行查询操作;
-
生态集成度:RedisJSON 作为 Redis 的扩展模块,和 Redis 的其他功能、工具无缝集成,开发者可以继续使用 Redis 的 TTL(过期时间设置)、事务、发布/订阅、Lua 脚本等功能,充分利用 Redis 生态的丰富能力,无需额外学习新的生态体系,降低了开发和使用成本。
-
3.3 RedisSearch
- 当 Redis 中存储的数据比较多时,搜索 Redis 中的数据是一件比较麻烦的事情。通常使用的
keys *
这样的指令,在生产环境一般都是直接禁用的,因为这样会产生严重的线程阻塞,影响其他的读写操作; - 那么如何快速搜索 Redis 中的数据(主要是key)呢? Redis 中原生提供了
Scan
指令,另外在 Redis Stack 中也增加了 RedisSearch 模块。
3.3.1 传统Scan
搜索
-
SCAN | Docs;
-
Scan
的核心思想是每次只返回查询的部分结果数据,通过迭代逐步返回完整数据; -
基础使用方式:
SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]
cursor
:游标,代表每次迭代返回的偏移量,首次查询从 0 开始,后续用返回的游标值继续迭代,直到游标返回 0 表示所有数据过滤完成;pattern
:匹配字符串,用于匹配要查询的键,如user*
表示以user
开头的字符串;count
:数字,指示每次迭代返回多少条数据;type
:键的类型,可指定如string
、set
、zset
等类型,针对不同键类型还有专属指令,像SSCAN
针对Set
类型、HSCAN
针对Hash
类型、ZSCAN
针对ZSet
类型;
-
例:
# 准备数据:使用Redis Lua脚本批量设置30个键值对 # 格式:SET k1 v1, SET k2 v2, ..., SET k30 v30 eval 'for i = 1,30,1 do redis.call("SET","k"..tostring(i),"v"..tostring(i)) end' 0# 简单按照cursor迭代所有key(无过滤条件) # 第一次迭代:从cursor 0开始,返回下一个cursor和部分keys scan 0 1) 18 # 下一次迭代的cursor值,非0表示还有更多数据 2) ... # 本次迭代返回的key列表(此处省略具体keys)# 第二次迭代:使用上一次返回的cursor 18继续 scan 18 1) 21 # 新的下一次迭代cursor 2) ... # 本次迭代返回的key列表# 第三次迭代:使用cursor 21继续,返回0表示迭代完成 scan 21 1) 0 # cursor为0表示所有数据迭代完成 2) ... # 最后一批key列表# 按照pattern过滤:只返回匹配k*模式的key # 第一次带模式匹配的迭代 scan 0 MATCH k* 1) 18 # 下一次迭代cursor 2) ... # 本次匹配k*的keys# 继续使用上一次返回的cursor进行模式匹配迭代 scan 18 MATCH k* 1) 21 # 新的迭代cursor 2) ... # 本次匹配的keys# 最后一次模式匹配迭代,cursor返回0表示完成 scan 21 MATCH k* 1) 0 # 迭代完成标志 2) ... # 最后一批匹配k*的keys# 设置每次迭代返回数量的上限(COUNT参数) # 第一次迭代:要求最多返回20个元素(实际可能少于20) scan 0 MATCH k* count 20 1) 21 # 下一次迭代cursor 2) ... # 最多20个匹配k*的keys# 使用新的cursor继续迭代,仍要求最多返回20个元素 scan 21 MATCH k* count 20 1) 0 # 迭代完成 2) ... # 最后一批匹配的keys
3.3.2 RedisSearch 搜索
-
Redis 实时搜索、查询与索引 - Redis 文档;
-
传统
Scan
搜索只能简单过滤键,若要进行复杂搜索(如电商场景中按品牌、型号、价格等多条件过滤商品)就力不从心,以往需将数据导入 MongoDB 或 ElasticSearch 等搜索引擎。而 Redis 提供的RediSearch
插件,可视为 ElasticSearch 这类搜索引擎的平替,大部分 ElasticSearch 能实现的搜索功能,在 Redis 里可直接进行,极大减少了数据迁移的麻烦。 -
要进行搜索,需要支持结构化查询的数据结构,Redis 中只有
Hash
和JSON
能支持; -
操作示例:具体可以参考Querying data | Docs
# 清空Redis中所有数据 flushall# 创建RediSearch索引(基于JSON文档) FT.CREATE productIndex ON JSON SCHEMA $.name AS name TEXT # 从JSON文档的name字段创建文本索引$.price AS price NUMERIC # 从JSON文档的price字段创建数值索引 # 索引配置说明: # productIndex: 索引名称 # ON JSON: 表示索引基于JSON文档(需要RedisJSON模块支持),默认为ON HASH # SCHEMA: 定义索引字段映射 # $.name: JSON路径表达式,指向文档中的name字段 # AS name: 将JSON字段映射为索引中的字段名 # TEXT: 文本类型,支持全文搜索 # NUMERIC: 数值类型,支持范围查询# 添加产品数据(JSON格式) JSON.SET phone:1 $ '{"id":1,"name":"HUAWEI 1","description":"HUAWEI PHONE 1","price":1999}' JSON.SET phone:2 $ '{"id":2,"name":"HUAWEI 2","description":"HUAWEI PHONE 2","price":2999}' JSON.SET phone:3 $ '{"id":3,"name":"HUAWEI 3","description":"HUAWEI PHONE 3","price":3999}' JSON.SET phone:4 $ '{"id":4,"name":"HUAWEI 4","description":"HUAWEI PHONE 4","price":4999}' JSON.SET phone:5 $ '{"id":5,"name":"HUAWEI 5","description":"HUAWEI PHONE 5","price":5999}' JSON.SET phone:6 $ '{"id":6,"name":"HUAWEI 6","description":"HUAWEI PHONE 6","price":6999}' JSON.SET phone:7 $ '{"id":7,"name":"HUAWEI 7","description":"HUAWEI PHONE 7","price":7999}' JSON.SET phone:8 $ '{"id":8,"name":"HUAWEI 8","description":"HUAWEI PHONE 8","price":8999}' JSON.SET phone:9 $ '{"id":9,"name":"HUAWEI 9","description":"HUAWEI PHONE 9","price":9999}' JSON.SET phone:10 $ '{"id":10,"name":"HUAWEI 10","description":"HUAWEI PHONE 10","price":19999}'# 如果是ON HASH格式的替代方案: # FT.ADD productIndex 'product:1' 1.0 FIELDS # "id" 1 # "name" "HUAWEI1" # "description" "HUAWEI PHONE 1" # "price" 3999 # 说明: # 'product:1': 文档的唯一标识符 # 1.0: 文档的权重分数 # FIELDS: 后面跟着字段的键值对# 查看索引状态和信息 FT.INFO productIndex # 返回索引的详细信息,包括:字段定义、文档数量、索引选项、统计信息等# 搜索产品(复杂查询) FT.SEARCH productIndex # 查询条件:在name字段中搜索包含"HUAWEI"的文档,在price字段中搜索1000到5000之间的数值(两个条件之间默认是AND关系)"@name:HUAWEI @price:[1000 5000]"# 返回字段。2: 返回2个字段,id name: 指定返回的字段名称RETURN 2 id name# 其他常用查询示例: # 1. 全文搜索: "@name:HUAWEI" # 2. 精确匹配: "@name:\"HUAWEI 1\"" # 3. 范围查询: "@price:[1000 5000]" # 4. 或条件: "(@name:HUAWEI | @name:XIAOMI)" # 5. 否定条件: "(-@price:[0 1000])"# 官方查询文档参考: # https://redis.io/docs/latest/develop/interact/search-and-query/query/
3.4 RedisBloom
3.4.1 布隆过滤器是什么
-
布隆过滤器是一种能快速检索一个元素是否存在于海量集合中的算法;
- 比如签到活动限制用户重复签到,若不考虑数据量,可将签到用户ID存到集合,签到时查集合;
- 但面对海量数据(如淘宝海量用户信息),集合会极大,检索变慢,此时布隆过滤器就派上用场,它能在海量数据集合中快速判断元素是否存在,且更节省空间、效率更高;
- 其典型应用是作为缓存数据的前端过滤缓存,像淘宝登录场景,可快速判断用户名是否存在,若不存在直接拒绝,避免无效数据库查询;
-
原理:布隆过滤器利用一个很长的二进制位数组和一系列哈希函数来保存元素
-
位数组(Bit Array):长度固定,每个位置只占1比特(0或1),初始全为0。位数组长度和哈希函数数量决定了过滤器的误报率和容量;
-
哈希函数集合:使用多个哈希函数,每个函数将输入元素映射到位数组的不同位置。理想哈希函数需有良好散列性,让不同输入尽可能均匀映射到位数组不同位置;
-
-
优缺点:
-
优点:非常节省空间,查询时间也非常快;
-
缺点:有一定误报率(判断元素在集合中时,元素可能实际不在),且无法删除元素,也无法给元素计数;
-
-
判断逻辑:
-
布隆过滤器判断一个元素不在集合中,那该元素肯定不在;
-
但判断元素在集合中时,该元素有可能实际不在;
-
如图中,要判断的元素(d),经哈希映射后对应位数组位置有1有0,所以“可能存在”;元素(e)对应位数组位置有0,所以“一定不存在”;
有1即可能存在,有0就一定不存在。
-
3.4.2 Guava 的布隆过滤器示例
-
布隆过滤器存在误判情况,即原本不在集合中的元素会被误判为在集合中,误判率是布隆过滤器的关键控制指标。在算法实现时,误判率可通过设定更复杂的哈希函数组合以及使用更大的位数组来控制,所以初始化布隆过滤器时,通常只需指定过滤器的容量和误判率即可;
-
通过在
pom.xml
文件中添加依赖,引入 Guava 库:<dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>33.1.0-jre</version> </dependency>
-
Guava 布隆过滤器使用示例:
public static void main(String[] args) {// 创建布隆过滤器,指定处理的是字符串类型数据、过滤器容量为 10000、误判率为 0.01// Funnels.stringFunnel(StandardCharsets.UTF_8):指定处理的是字符串类型数据BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(StandardCharsets.UTF_8),10000,0.01);//把 A~Z 放入布隆过滤器for (int i = 64; i <= 90 ; i++) {bloomFilter.put(String.valueOf((char) i));}// 判断元素是否可能存在于布隆过滤器中System.out.println(bloomFilter.mightContain("A")); // trueSystem.out.println(bloomFilter.mightContain("a")); // false }
3.4.3 Redis的BloomFilter使用示例
-
RedisBloom | Redis 的布隆和布谷鸟过滤器 - Redis 文档;
-
布隆过滤器依赖二进制数组存储数据,而 Redis 的 Bitmap 数据结构天生适合作为分布式布隆过滤器的底层存储,不过此前算法需自行实现,现在 Redis 提供了 BloomFilter 模块,降低了使用门槛;
# 创建一个key为bf的布隆过滤器,设置容错率为0.01,初始容量为1000。NONSCALING 表示过滤器不会自动扩容,当数据达到容量上限时会报错 BF.RESERVE bf 0.01 1000 NONSCALING# 向布隆过滤器中添加单个元素 BF.ADD bf A BF.ADD bf B BF.ADD bf C# 批量向布隆过滤器中添加多个元素 BF.MADD bf D E F G H I # 返回结果:每个元素的添加状态(通常为1表示成功)# 使用INSERT命令创建并添加元素(如果过滤器不存在则自动创建) BF.INSERT bf CAPACITY 1000 ERROR 0.01 ITEMS hello # 参数说明: # CAPACITY 1000: 设置容量为1000 # ERROR 0.01: 设置错误率为0.01(1%) # ITEMS hello: 要添加的元素# 查看布隆过滤器中已添加元素的数量(估算值) BF.CARD bf # 返回:当前过滤器中的元素数量(由于布隆过滤器的特性,这是估算值)# 判断单个元素是否可能在过滤器中 BF.EXISTS bf a # 返回值说明: # 0: 元素肯定不在过滤器中(100%准确) # 1: 元素可能在过滤器中(存在一定的误判率)# 批量判断多个元素是否可能在过滤器中 BF.MEXISTS bf A a B b # 返回:每个元素的判断结果数组,如 [1, 0, 1, 0]# 查看布隆过滤器的详细信息 BF.INFO bf # 返回信息包括: # Capacity: 设计容量 # Size: 实际内存使用量 # Number of filters: 过滤器数量 # Number of items inserted: 已插入元素数量 # Expansion rate: 扩展率(如果支持扩容) # Error rate: 错误率# 迭代布隆过滤器的位数组(用于备份和恢复) BF.SCANDUMP bf 0 # 参数说明: # bf: 过滤器名称 # 0: 迭代起始游标(0表示从头开始) # 返回:[next_iteration_cursor, chunk_data],当前访问到的数据和下一次迭代的起点 # 当下次迭代游标为0时表示迭代完成# 使用示例: # 第一次迭代: BF.SCANDUMP bf 0 → [13, "chunk_data_1"] # 第二次迭代: BF.SCANDUMP bf 13 → [27, "chunk_data_2"] # 第三次迭代: BF.SCANDUMP bf 27 → [0, "chunk_data_3"] (完成)# 配合BF.LOADCHUNK恢复布隆过滤器: # BF.LOADCHUNK new_bf 0 "chunk_data_1" # BF.LOADCHUNK new_bf 13 "chunk_data_2" # BF.LOADCHUNK new_bf 27 "chunk_data_3"
3.5 Cuckoo Filter
3.5.1 简介
-
布隆过滤器存在无法删除数据的问题,Cuckoo Filter(布谷鸟过滤器)是布隆过滤器的改进版本。它相比布隆过滤器,能够删除数据,且在相同集合和误报率下,通常占用空间更少,但算法实现更复杂,同样存在误判率,即可能将不在集合中的元素错误判断为在集合中;
- 误判率控制:布隆过滤器的误报率通过调整位数组大小和哈希函数控制,而 Cuckoo Filter 的误报率受指纹大小和桶大小控制;
- 存储结构:
- Cuckoo Filter 的数组里存的是桶(bucket),每个桶可存放多个数据(由 BUCKETSIZE 决定,Redis 中 BUCKETSIZE 是 1 到 255 之间的整数,默认值为 2);
- 桶中保存的是数据的指纹(可看作压缩后的数据,是数据对象的几个低位数据),指纹越小,哈希冲突导致误判的几率越小,不过 Redis 的 Cuckoo Filter 不支持调整指纹相关参数;
- 同一桶中存放数据越多,空间利用率越高,但误判率也越高,性能更慢。
3.5.2 使用示例
# 创建布谷鸟过滤器(Cuckoo Filter)
CF.RESERVE cf 1000 BUCKETSIZE 2 MAXITERATIONS 20 EXPANSION 1# 参数解读:
# cf: 过滤器名称
# 1000: 容量(必填参数)- 初始的桶数量,决定了过滤器的基础大小# 可选参数(使用Redis默认值):
# BUCKETSIZE 2: 每个桶中可存放的指纹数量
# - 值越大:空间利用率越高(可以存储更多元素)
# - 缺点:误判率更高,查询性能下降(需要检查更多位置)
# - 默认值2在空间效率和性能间取得平衡# MAXITERATIONS 20: 插入元素时的最大重试次数
# - 值越小:插入性能越好(快速失败)
# - 值越大:空间利用率越好(有更多机会找到空位)
# - 默认值20在插入成功率和性能间取得平衡
# - 达到最大迭代次数后插入会失败# EXPANSION 1: 扩容时的增长因子
# - 值1: 每次扩容时容量翻倍(100%增长)
# - 值2: 容量变为原来的2倍
# - 值小于1: 容量按比例缩小(一般不使用)
# - 默认值1提供适中的扩容步长
- 其他操作:和布隆过滤器操作类似,只是多了
CF.DEL
命令用于删除元素。
4 Redis Stack 下载使用方式
4.1 手动安装
-
Redis Stack 的这些扩展模块除了在 RedisCloud 上可以直接使用外,也可以手动集成到自己的 Redis 服务当中;
-
登录 RedisCloud,进入下载中心,可以手动下载对应的扩展模块(注意选择对应的 Redis 版本以及操作系统),下载后得到以
.so
为后缀的扩展文件,将其上传到服务器后,在 Redis 的配置文件中加载这个扩展模块:# Load modules at startup. If the server is not able to load modules # it will abort. It is possible to use multiple loadmodule directives. # loadmodule /root/myredis/redisbloom.so
-
然后重启 Redis 服务,使用客户端登录 Redis 后查看扩展模块的加载情况:
MODULE LIST
- 注意:如果模块加载错误,那么 Redis 服务启动是会失败的,这时要去查看日志逐步排查问题;
- 例如:在 Linux 服务器上,如果没有给
redisbloom.so
文件添加可执行的x
权限,那么 Redis 就会启动失败。
4.2 Java 客户端调用
-
这些扩展模块在目前都还是比较新的功能,所以目前 Java 的一些客户端工具都还没有集成这些功能,需要手动进行扩展。大部分情况下,只能通过 Lua 脚本手动调用这些扩展功能。但是由于在客户端无法确定服务端是否安装了对应的扩展模块,所以在写 Lua 脚本时,一定要注意处理好各种各样的异常情况;
-
以布隆过滤器为例:
@SpringBootTest @RunWith(SpringRunner.class) public class RedisStackTest {@ResourceRedisTemplate<String,Object> redisTemplate; // 注入RedisTemplate用于操作RedisList<String> keys = List.of("a-bf"); // 布隆过滤器的key名称@Testpublic void createBloomFilter(){if(!redisTemplate.hasKey("a-bf")){ // 检查布隆过滤器是否已存在try{// Lua脚本:创建布隆过滤器String createFilterScriptText= """return redis.call('BF.RESERVE', KEYS[1], '0.01','1000','NONSCALING')""";DefaultRedisScript<String> redisScript = new DefaultRedisScript<>(createFilterScriptText, String.class);String execute = redisTemplate.execute(redisScript,keys); // 执行脚本System.out.println("CREATE BF:"+execute);}catch (Exception e){// 异常处理:如果Redis不支持布隆过滤器命令System.out.println("COMMAND NOT SUPPORT");}}else{System.out.println("BF KEY is already exists"); // 过滤器已存在}}@Testpublic void addData(){if(!redisTemplate.hasKey("a-bf")){ // 检查过滤器是否存在System.out.println("BF KEY is not exists");}else{try{String[] args = new String[]{"A","B","C","D","E","F","G"}; // 要添加的元素String addDataScriptText= """for i,arg in ipairs(ARGV) dolocal addRes = redis.call('BF.ADD',KEYS[1],arg) // 循环添加所有元素endreturn 'OK' // 返回成功标识""";DefaultRedisScript<String> redisScript = new DefaultRedisScript<>(addDataScriptText, String.class);System.out.println("ADDDATA BF:"+redisTemplate.execute(redisScript, keys, args));}catch (Exception e){System.out.println("COMMAND NOT SUPPORTED"); // 命令不支持异常}}}@Testpublic void checkData(){if(!redisTemplate.hasKey("a-bf")){ // 检查过滤器是否存在System.out.println("BF KEY is not exists");}else{String[] args = new String[]{"A","B","C","D","E","F","G"}; // 要检查的元素String checkDataScriptText= """local checkRes = redis.call('BF.EXISTS',KEYS[1],ARGV[1]) // 检查单个元素return checkRes // 返回检查结果""";DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(checkDataScriptText, Long.class);try{Long res = redisTemplate.execute(redisScript, keys, args);if(1L == res){System.out.println("KEY EXISTS"); // 元素可能存在(有误判概率)} else if (0L==res) {System.out.println("KEY NOT EXISTS"); // 元素肯定不存在}else{System.out.println("ERROR"); // 其他错误情况}}catch (Exception e){System.out.println("COMMAND NOT SUPPORTED"); // 命令不支持异常}}} }