redis中数据模糊查询scan用法详解
作者:xswlw_guoquanbao
redis中数据模糊查找-scan用法
1.查找方法
Redis中有一个经典的问题,在巨大的数据量的情况下,做类似于查找符合某种规则的Key的信息,这里就有两种方式,
一是keys命令,简单粗暴,由于Redis单线程这一特性,keys命令是以阻塞的方式执行的,keys是以遍历的方式实现的复杂度是 O(n),Redis库中的key越多,查找实现代价越大,产生的阻塞时间越长。
二是scan命令,以非阻塞的方式实现key值的查找,绝大多数情况下是可以替代keys命令的,可选性更强
2.keys命令
127.0.0.1:6379> keys s* 1) "s1" 2) "sddddf" 3) "sss" 4) "s358" 127.0.0.1:6379>
3.scan命令
SCAN cursor [MATCH pattern] [COUNT count] cursor - 游标。 pattern - 匹配的模式。 count - 指定从数据集里返回多少元素,默认值为 10 。 可用版本 >= 2.8.0
示例:
127.0.0.1:6379> keys s* 1) "s1" 2) "sddddf" 3) "sss" 4) "s358" 127.0.0.1:6379> scan 0 match s* count 20 1) "0" 2) 1) "s1" 2) "sss" 3) "s358" 4) "sddddf" 127.0.0.1:6379> scan 0 match s* count 2 1) "4" 2) 1) "s1" 127.0.0.1:6379> scan 4 match s* count 2 1) "1" 2) 1) "sss" 2) "s358" 127.0.0.1:6379> scan 1 match s* count 2 1) "3" 2) 1) "sddddf"
SCAN 命令是一个基于游标的迭代器,每次被调用之后, 都会向用户返回一个新的游标, 用户在下次迭代时需要使用这个新游标作为 SCAN 命令的游标参数, 以此来延续之前的迭代过程。
4.Java代码中的实现
public Set<String> scan(String matchKey) { Set<String> keys = redisTemplate.execute((RedisCallback<Set<String>>) connection -> { Set<String> keysTmp = new HashSet<>(); Cursor<byte[]> cursor = connection.scan( new ScanOptions.ScanOptionsBuilder().match("*" + matchKey + "*").count(1000).build() ); while (cursor.hasNext()) { keysTmp.add(new String(cursor.next())); } return keysTmp; }); return keys; }
redis的模糊查询提高效率
Redis 中进行模糊查询(例如使用 KEYS pattern
或 SCAN cursor MATCH pattern
)本质上效率是相对较低的,尤其是在大型数据集上,因为 Redis 的核心优势在于通过精确键进行 O(1) 复杂度的快速访问,而模糊查询通常需要遍历键空间。
提高 Redis 模糊查询效率的核心策略是:避免在运行时进行全键空间扫描!
以下是一些有效的方法来提高效率或规避效率瓶颈:
🛑 1. 避免使用 KEYS
命令 (最重要!)
- 问题:
KEYS
命令会阻塞 Redis 服务,直到遍历完所有键并返回匹配结果。在生产环境的大数据集上,这可能导致服务不可用。 - 解决方案: 绝对禁止 在生产环境使用
KEYS
。使用SCAN
替代。
🔍 2. 使用 SCAN
命令进行迭代式查询
原理: SCAN
命令使用游标(cursor)进行迭代,每次只返回一小部分匹配的键。它不会阻塞服务器,因为每次调用只占用少量时间。
优点:
- 非阻塞: 不会导致服务停顿。
- 增量式: 可以分批处理结果,减轻客户端和服务端压力。
缺点:
- 不是原子快照: 在迭代过程中,如果键空间发生变化(增、删、改),可能会看到重复的键或遗漏部分键。这通常可以接受。
- 整体耗时可能不短: 虽然每次调用快,但要获取所有匹配结果,最终需要完成的“工作总量”和
KEYS
类似(都需要遍历大部分或全部键空间)。 - 客户端逻辑复杂: 需要管理游标和循环。
用法:
SCAN 0 MATCH user:profile:*:email COUNT 100
0
是起始游标(第一次调用)。
MATCH pattern
指定模糊匹配模式(可选)。
COUNT n
建议每次迭代返回的元素数量(只是个提示,Redis 可能返回更多或更少)。适当增加 COUNT
(如 500, 1000) 可以在网络往返次数和单次耗时之间取得平衡,提高整体效率。
变种: SSCAN
(扫描 Set), HSCAN
(扫描 Hash), ZSCAN
(扫描 Sorted Set)。这些用于扫描特定键内部的大集合元素,避免阻塞或大结果集。
🧠 3. 设计可查询的键结构 (最重要的优化方向!)
核心思想是将运行时扫描转化为精确查找或小范围查找。这通常需要牺牲一些存储空间(空间换时间)和增加写入/更新时的维护成本。
- a) 使用索引集合 (Index Set):
- 场景: 查询具有特定前缀、后缀或中间部分的键(如
user:123:profile
,order:abc:details
)。 - 方法:
- 创建一个专门的 Set 类型键(如
index:user:ids
)。 - 每当创建一个新用户键(如
SET user:123:profile {...}
),同时将123
添加到索引集合(SADD index:user:ids 123
)。 - 当需要查询所有用户键时,使用
SMEMBERS index:user:ids
或SSCAN index:user:ids
获取所有用户 ID。 - 客户端拿到 ID 列表后,再通过精确键(
GET user:<id>:profile
)获取数据。
- 创建一个专门的 Set 类型键(如
- 优点: 获取键列表非常快(O(1) 或 O(N),N 是用户数而非总键数),避免了全键扫描。
- 缺点: 需要维护索引;占用额外内存;获取完整数据需要多次查询(N+1 问题)。
- 场景: 查询具有特定前缀、后缀或中间部分的键(如
- b) 使用 Sorted Set 按模式存储键或引用:
- 场景: 需要按范围(如时间范围、分数范围)查询,或者需要排序。
- 方法:
- 创建一个 Sorted Set 键(如
zindex:orders:by_time
)。 - 成员(member)可以是:
- 完整的键名(如
order:abc:details
) - 适用于键名本身包含信息(如时间戳)。 - 或一个唯一 ID(如
abc
),分数(score)是查询依据(如订单创建时间戳)。
- 完整的键名(如
- 当需要查询某时间段内的订单:
- 使用
ZRANGEBYSCORE zindex:orders:by_time start_timestamp end_timestamp
获取键名或 ID。 - 再通过精确键获取数据。
- 使用
- 创建一个 Sorted Set 键(如
- 优点: 支持高效的范围查询和排序。
- 缺点: 维护索引;额外内存;潜在 N+1 问题。
使用 Hash 存储子字段索引:
场景: 需要根据对象内部字段的值进行查询(如查找所有
email
以@gmail.com
结尾的用户)。方法:
创建辅助数据结构:
反向索引(Inverted Index): 对于需要查询的字段值(如邮箱后缀
gmail.com
),创建一个 Set(如index:email_suffix:gmail.com
),存储拥有该后缀的用户 ID。
更新数据时:
修改用户 Hash (
HSET user:123 email new@domain.com
)。将用户
123
从旧后缀索引集移除(SREM index:email_suffix:old.com 123
)。将用户
123
添加到新后缀索引集(SADD index:email_suffix:new.com 123
)。
查询时:
SMEMBERS index:email_suffix:gmail.com
获取用户 ID 列表,然后HGETALL user:<id>
。优点: 对于特定字段的等值查询非常高效。
缺点: 维护成本最高(尤其字段值频繁更新时);占用大量额外内存;只适用于等值查询或有限模式(后缀=
SADD
时存储后缀);N+1 问题。
d) 拆分键名 + 利用集合操作:
场景: 键名由多个部分组成(如
country:region:city:userid
),需要按不同层级查询。方法:
为每个层级维护索引集:
countries = { 'us', 'uk', 'jp' ... }
regions:us = { 'ca', 'ny', 'tx' ... }
cities:us:ca = { 'sf', 'la', 'sd' ... }
查询用户时:
先通过精确键获取国家列表、特定国家的地区列表、特定国家地区的城市列表。
然后构造出所有可能的键前缀(如
us:ca:sf
)。最后用
SSCAN
遍历user:us:ca:sf:*
(范围大大缩小)。
优点: 将全键扫描缩小到特定小范围扫描。
缺点: 需要精心设计键结构和索引;维护索引;可能仍需小范围
SCAN
。
📦 4. 缓存模糊查询结果
- 场景: 模糊查询模式相对固定且结果变化不频繁(如查询所有“活跃用户”列表)。
- 方法: 定期(如每分钟)或在数据变更时触发一次
SCAN
,将结果存入一个 Redis Set 或 List 中。 - 查询时: 直接读取这个缓存的结果集合。
- 优点: 查询速度极快(O(1) 或 O(N) 读集合)。
- 缺点: 数据不是实时最新(最终一致性);需要维护缓存更新逻辑;占用额外内存。
🌐 5. 使用外部索引/搜索引擎 (对于复杂查询或海量数据)
- 原理: 当 Redis 内置的查询能力(即使是优化后的索引)无法满足复杂模式匹配(如全文搜索、多字段组合查询)或数据量极大时。
- 工具: Redis 官方模块 RediSearch, Elasticsearch, Solr, OpenSearch 等。
- 方法:
- 数据写入/更新 Redis 的同时,异步写入索引到搜索引擎。
- 查询请求发送到搜索引擎,获取匹配的键 ID 或文档。
- 根据 ID 回 Redis 获取完整数据(或搜索引擎已存储所需数据)。
- 优点: 提供极其强大和高效的全文搜索、复杂过滤、聚合分析能力;避免 Redis 自身遍历。
- 缺点: 系统架构复杂度显著增加;需要维护额外的服务;数据同步有延迟(异步);运维成本高。
📌 总结与建议
- 绝对禁止
KEYS
: 总是用SCAN
替代。 - 优先考虑设计优化: 这是最根本的解决方案。思考业务查询需求,通过设计可查询的键结构(索引集合、Sorted Set、Hash 索引)将运行时模糊匹配转化为精确查找或小范围扫描。 这是“空间换时间”的经典应用。
- 合理使用
SCAN
:- 对于无法避免扫描的场景,务必用
SCAN
。 - 适当调整
COUNT
值(如 500-1000),在单次耗时和总网络往返次数之间找到最佳平衡点。 - 在客户端处理好游标迭代。
- 对于无法避免扫描的场景,务必用
- 考虑缓存: 对结果变化慢、查询频繁的模式,缓存
SCAN
结果。 - 评估外部索引: 当数据量大、查询模式复杂、性能要求极高时,认真考虑引入 RediSearch 或 Elasticsearch 等专用搜索引擎。它们是为这类场景量身定制的。
关键权衡:
- 内存 vs CPU/延迟: 优化设计(索引)会消耗更多内存,但极大降低查询延迟和 CPU 消耗(避免了遍历)。
- 写入复杂度 vs 读取复杂度: 维护索引增加了写入/更新操作的复杂度(需要同时更新索引),但极大简化并加速了读取操作。
- 实时性 vs 效率: 外部索引通常是异步更新,牺牲了一点实时性换取了强大的查询能力和可扩展性。
选择哪种策略取决于:
- 你的数据规模
- 查询模式的复杂度和频率
- 对查询延迟的要求
- 对数据实时性的要求
- 可接受的内存开销
- 系统的复杂度容忍度
务必根据你的具体应用场景进行设计和选择! 没有放之四海而皆准的最优解,但遵循“避免运行时扫描”的核心原则是关键。💪🏻
到此这篇关于redis中数据模糊查询scan用法详解的文章就介绍到这了,更多相关redis scan模糊查询内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!