• Redis 分页 + 多条件模糊查询太头疼?这套方案帮你轻松搞定!

Redis 分页 + 多条件模糊查询太头疼?这套方案帮你轻松搞定!

2025-05-07 10:00:03 栏目:宝塔面板 41 阅读

我猜不少搞 Java 开发的兄弟,在项目里碰到 Redis 分页和多条件模糊查询的时候,都跟我一样,心里直犯嘀咕:"这玩意儿咋整啊?咋就这么难搞呢?" 别慌,今儿个咱就来好好唠唠,怎么把这俩难题轻松搞定,让你在同事面前狠狠露一手!

一、先搞明白为啥 Redis 分页和多条件模糊查询让人头大

咱先说说 Redis 分页。用过 Redis 的都知道,它和咱们熟悉的 MySQL 这些关系型数据库不一样。MySQL 里有个 LIMIT 关键字,分页查询那叫一个方便,直接就能指定查第几页、每页多少条。可 Redis 呢,它主要是基于内存的键值对存储,数据结构虽然丰富,但原生就没有像数据库那样专门的分页功能。

你要是存的数据是放在列表(List)里,想分页的话,可能得用 LRANGE 命令。比如说列表键是 users,想查第 1 页,每页 10 条,就用 LRANGE users 0 9。乍一看好像还行,可要是列表里的数据是动态变化的,比如经常有新增、删除操作,列表里元素的位置就会变,这时候用 LRANGE 分页,结果可能就不准确了。而且要是列表特别大,每次用 LRANGE 都得遍历一堆元素,性能也会受影响。

再看看多条件模糊查询。Redis 本身的查询能力比较有限,不像数据库能支持复杂的 SQL 语句,什么 LIKE 啊、多个条件组合啊都能轻松搞定。Redis 里的键匹配,一般就靠 KEYS 命令或者 SCAN 命令。KEYS 命令能根据通配符匹配键,比如 KEYS user:* 能查出所有以 user: 开头的键,可这玩意儿有个大问题,它是全量扫描,在生产环境用的话,要是键的数量特别多,会把 Redis 搞得很慢,甚至卡住。

SCAN 命令虽然能增量扫描,避免全量扫描的问题,但它返回的只是键,要是你想根据键对应的值里的多个条件进行模糊查询,比如用户表里要根据用户名包含 "张三",年龄在 20 到 30 之间来查询,SCAN 就没办法直接做到了,你得把键对应的所有值都取出来,在应用层进行过滤,这就会增加应用服务器的负担,而且效率也不高。

举个简单的例子,假设咱们有个电商项目,要在 Redis 里存储商品信息,每个商品的键是 product:1、product:2 这样的形式,值是 JSON 格式,包含商品名称、价格、类别等信息。现在要查询名称里包含 "手机",价格在 2000 到 4000 之间的商品,并且要分页显示。这时候问题就来了,怎么根据商品名称和价格这两个条件来查询呢?直接用 Redis 原生的功能很难实现,这就需要咱们想办法来解决。

二、搞定 Redis 分页的实用方案

(一)基于有序集合(Sorted Set)的分页方案

有序集合是 Redis 里一个很强大的数据结构,它每个元素都有一个分数(score),可以根据分数对元素进行排序。咱们可以利用这个特性来实现分页。

比如说,咱们还是以用户数据为例,每个用户有一个唯一的 ID,咱们可以把用户 ID 作为有序集合的成员,把用户的创建时间作为分数。这样有序集合里的元素就是按照创建时间排序的。

要实现分页查询,假设每页显示 n 条数据,第 m 页的起始索引就是 (m - 1) * n,结束索引就是 m * n - 1。然后用 ZRANGE 命令来获取指定范围内的成员。比如有序集合键是 users_sorted,查第 1 页,每页 10 条,就是 ZRANGE users_sorted 0 9。

但是这里有个问题,如果用户数据是不断更新的,比如有用户删除了,有序集合里的元素数量会减少,这时候原来的索引就会发生变化。不过对于大部分分页场景来说,只要不是频繁删除中间的元素,这种方案还是比较可行的。

(二)记录上一页最后一个元素的分页方案

这种方案适合数据是按照一定顺序排列的情况,比如时间顺序。咱们在查询上一页数据的时候,记录下最后一个元素的相关信息,比如时间戳或者 ID,然后在下一页查询时,根据这个信息来获取下一页的数据。

比如,咱们还是以按创建时间排序的用户数据为例,假设上一页最后一个用户的创建时间是 last_score,那么下一页查询的时候,就可以用 ZRANGEBYSCORE 命令,从 last_score 之后开始获取数据。命令大概是这样的:ZRANGEBYSCORE users_sorted (last_score 0 9,这里的 (last_score 表示不包含 last_score 这个分数的元素,然后获取 10 条数据。

这种方案的好处是可以避免因为中间元素删除导致索引变化的问题,而且每次查询的时间复杂度比较低,适合大数据量的分页场景。

三、解决 Redis 多条件模糊查询的巧妙办法

(一)预处理数据,建立多个索引

既然 Redis 原生不支持多条件模糊查询,那咱们可以在数据写入 Redis 的时候,对数据进行预处理,根据不同的查询条件建立索引。

还是以电商商品为例,商品有名称、价格、类别等属性。咱们可以建立三个有序集合:

  • 以商品名称为索引的有序集合 product_name_index,成员是商品 ID,分数可以是商品名称的某种哈希值或者直接是名称的拼音首字母(方便模糊查询)。
  • 以价格为索引的有序集合 product_price_index,成员是商品 ID,分数就是商品的价格。
  • 以类别为索引的有序集合 product_category_index,成员是商品 ID,分数可以是类别 ID。

当要进行多条件模糊查询时,比如查询名称包含 "手机",价格在 2000 到 4000 之间的商品,咱们可以先根据名称条件,从 product_name_index 中获取所有名称包含 "手机" 的商品 ID 集合,再从 product_price_index 中获取价格在 2000 到 4000 之间的商品 ID 集合,然后对这两个集合取交集,得到同时满足这两个条件的商品 ID,最后根据这些商品 ID 去获取具体的商品信息。

对于模糊查询名称包含 "手机",咱们可以在建立索引的时候,把商品名称的所有可能的子串都作为索引的一部分,或者使用一些模糊匹配的算法,比如编辑距离算法,不过这可能会增加索引的存储量。更简单的办法是,在应用层对输入的模糊查询关键词进行处理,生成对应的通配符模式,然后在 Redis 中使用 SCAN 命令结合键的模式来获取相关的索引键,再获取对应的商品 ID 集合。

(二)使用 Redis 的位图(Bitmap)

位图可以用来表示某个元素是否存在,或者某个条件是否满足。比如对于每个商品,我们可以用不同的位图来表示不同的条件,比如价格是否在某个区间,类别是否属于某一类等。

不过位图在多条件查询中的应用相对比较复杂,需要结合其他数据结构一起使用,这里咱们先重点介绍前面的索引方案。

四、综合方案:让分页和多条件模糊查询无缝结合

现在咱们把分页和多条件模糊查询结合起来,看看怎么在实际场景中应用。

假设咱们还是那个电商项目,要实现根据商品名称模糊查询、价格范围查询,并且进行分页显示的功能。具体步骤如下:

(一)数据写入阶段

  1. 当新增一个商品时,首先生成一个唯一的商品 ID,比如 product:1001。
  2. 将商品的详细信息以 JSON 格式存储在 Redis 的字符串键中,键为 product:1001,值为 {"name":"华为手机", "price":3000, "category":"电子产品", "other_info":"..."}。
  3. 建立名称索引:将商品名称进行处理,比如提取所有可能包含的关键词,这里假设我们简单地将整个名称作为索引的一部分,在有序集合 product_name_index 中,以商品 ID 为成员,以名称的拼音或者某种可以用于模糊查询的标识为分数(这里为了方便,暂时以名称本身作为分数,实际项目中可能需要更复杂的处理)。比如 ZADD product_name_index "华为手机" "product:1001"。
  4. 建立价格索引:在有序集合 product_price_index 中,以商品 ID 为成员,价格为分数,执行 ZADD product_price_index 3000 "product:1001"。
  5. 建立类别索引:在有序集合 product_category_index 中,以商品 ID 为成员,类别 ID 或者类别名称为分数,假设类别是 "电子产品",执行 ZADD product_category_index "电子产品" "product:1001"。

(二)查询阶段

当用户输入查询条件,比如名称包含 "手机",价格在 2000 到 4000 之间,要查询第 2 页,每页 10 条数据时:

  1. 处理名称模糊查询:生成名称的通配符模式,比如 "手机",然后使用 SCAN 命令在 product_name_index 中查找所有分数包含 "手机" 的成员(这里需要注意,SCAN 命令本身不能直接根据分数的内容进行模糊查询,所以前面的索引建立方式可能需要调整,更合理的做法是将商品名称的关键词提取出来,作为有序集合的成员,分数作为商品 ID,或者使用其他数据结构来存储关键词和商品 ID 的映射关系。这里为了方便演示,假设我们有一个键为 name:手机 的集合,里面存储了所有名称包含 "手机" 的商品 ID)。
  2. 获取价格在 2000 到 4000 之间的商品 ID 集合,使用 ZRANGEBYSCORE product_price_index 2000 4000。
  3. 对这两个集合取交集,得到同时满足名称和价格条件的商品 ID 集合,可以使用 Redis 的 ZINTERSTORE 命令,将两个有序集合的交集存储到一个临时有序集合中。
  4. 对临时有序集合进行分页查询,假设我们要按价格排序(也可以按其他条件排序),使用 ZRANGE 命令,根据页码和每页数量计算出起始和结束索引,比如第 2 页,每页 10 条,起始索引是 10,结束索引是 19,执行 ZRANGE temp_index 10 19,得到该页的商品 ID。
  5. 根据商品 ID 从对应的字符串键中获取商品的详细信息,返回给用户。

(三)代码示例(Java 版本)

这里使用 Jedis 客户端来演示部分代码:

import redis.clients.jedis.Jedis;
import redis.clients.jedis.Tuple;
import java.util.*;
public class RedisQueryDemo {
    private Jedis jedis;
    public RedisQueryDemo() {
        jedis = new Jedis("localhost", 6379);
    }
    // 写入商品数据并建立索引
    public void addProduct(String productId, String name, double price, String category) {
        // 存储商品详情
        String productKey = "product:" + productId;
        String productInfo = String.format("{"name":"%s", "price":%f, "category":"%s"}", name, price, category);
        jedis.set(productKey, productInfo);
        // 建立名称索引(这里简化处理,实际可能需要更复杂的关键词提取)
        jedis.zadd("product_name_index", 0, name + ":" + productId); // 这里分数设为 0,仅作为存储成员的方式,实际可根据需求设置
        // 建立价格索引
        jedis.zadd("product_price_index", price, productId);
        // 建立类别索引
        jedis.zadd("product_category_index", 0, category + ":" + productId); // 同理,分数设为 0
    }
    // 多条件模糊查询并分页
    public List searchProducts(String nameKeyword, double minPrice, double maxPrice, int page, int pageSize) {
        List resultProductIds = new ArrayList<>();
        // 获取名称包含关键词的商品 ID 集合(简化处理,实际需根据关键词生成通配符并扫描)
        Set nameMatchedProducts = new HashSet<>();
        // 这里模拟通过关键词获取相关成员,实际可能需要使用 SCAN 命令遍历 product_name_index 并检查成员是否包含关键词
        Set nameIndexTuples = jedis.zrangeWithScores("product_name_index", 0, -1);
        for (Tuple tuple : nameIndexTuples) {
            String member = tuple.getElement();
            if (member.contains(nameKeyword)) {
                String productId = member.split(":")[1];
                nameMatchedProducts.add(productId);
            }
        }
        // 获取价格范围内的商品 ID 集合
        Set priceMatchedProducts = jedis.zrangeByScore("product_price_index", minPrice, maxPrice);
        // 取交集
        priceMatchedProducts.retainAll(nameMatchedProducts);
        // 将交集转换为有序集合(假设按价格排序)
        String tempIndexKey = "temp_index:" + UUID.randomUUID().toString();
        int score = 0;
        for (String productId : priceMatchedProducts) {
            jedis.zadd(tempIndexKey, jedis.zscore("product_price_index", productId), productId);
        }
        // 分页查询
        long start = (page - 1) * pageSize;
        long end = start + pageSize - 1;
        resultProductIds = jedis.zrange(tempIndexKey, start, end);
        // 删除临时索引
        jedis.del(tempIndexKey);
        return resultProductIds;
    }
    public static void main(String[] args) {
        RedisQueryDemo demo = new RedisQueryDemo();
        // 模拟写入数据
        demo.addProduct("1001", "华为手机", 3000, "电子产品");
        demo.addProduct("1002", "小米手机", 2500, "电子产品");
        demo.addProduct("1003", "苹果手机", 4000, "电子产品");
        demo.addProduct("1004", "华为平板", 2000, "电子产品");
        demo.addProduct("1005", "海尔冰箱", 3500, "家电");
        // 模拟查询:名称包含"手机",价格在 2000 - 4000 之间,第 1 页,每页 2 条
        List productIds = demo.searchProducts("手机", 2000, 4000, 1, 2);
        for (String productId : productIds) {
            System.out.println("查询到的商品 ID:" + productId);
            // 这里可以根据 productId 获取具体的商品信息
        }
    }
}

五、注意事项和优化技巧

(一)索引维护

建立的索引会增加 Redis 的内存占用,所以要根据实际的查询需求,合理选择需要建立索引的条件,不要建立过多无用的索引。同时,在数据更新(比如删除、修改)时,要及时更新对应的索引,保证索引的一致性。

(二)性能优化

  1. 对于大规模数据,使用 SCAN 命令代替 KEYS 命令进行键的扫描,避免全量扫描影响 Redis 性能。
  2. 在进行集合交集、并集等操作时,注意集合的大小,如果集合过大,操作可能会比较耗时,可以考虑在应用层进行部分过滤,减少 Redis 层的操作压力。
  3. 可以对常用的查询结果进行缓存,比如热门的查询条件和分页结果,减少重复查询的开销。

(三)数据结构选择

根据不同的业务场景选择合适的数据结构,比如有序集合适合需要排序和范围查询的场景,集合适合需要去重和交集、并集操作的场景,字符串适合存储单个对象的详细信息。

六、总结

通过上面的方案,咱们基本上解决了 Redis 分页和多条件模糊查询的难题。利用有序集合、集合等数据结构建立索引,对数据进行预处理,结合分页算法,能够在 Redis 中实现高效的分页和多条件查询。当然,具体的实现还需要根据项目的实际需求进行调整和优化,比如索引的建立方式、数据结构的选择、查询条件的处理等。

本文地址:https://www.yitenyun.com/186.html

搜索文章

Tags

数据库 API FastAPI Calcite 电商系统 MySQL 数据同步 ACK Web 应用 异步数据库 双主架构 循环复制 Deepseek 宝塔面板 Linux宝塔 Docker 生命周期 序列 核心机制 JumpServer JumpServer安装 堡垒机安装 Linux安装JumpServer esxi esxi6 root密码不对 无法登录 web无法登录 Windows Windows server net3.5 .NET 安装出错 宝塔面板打不开 宝塔面板无法访问 SSL 堡垒机 跳板机 HTTPS 查看硬件 Linux查看硬件 Linux查看CPU Linux查看内存 无法访问宝塔面板 Windows宝塔 Mysql重置密码 HTTPS加密 连接控制 机制 ES 协同 Oracle 处理机制 Serverless 无服务器 语言 OB 单机版 Spring SQL 动态查询 scp Linux的scp怎么用 scp上传 scp下载 scp命令 技术 存储 索引 运维 缓存方案 缓存架构 缓存穿透 架构 InnoDB 日志文件 MIXED 3 分页查询 Rsync RocketMQ 长轮询 配置 监控 响应模型 修改DNS Centos7如何修改DNS Redis 电商 系统 HexHub Linux 安全 服务器 MySQL 9.3 聚簇 非聚簇 异步化 防火墙 黑客 查询 数据 主库 sftp 服务器 参数 group by 自定义序列化 数据库锁 SQLark PostgreSQL 管理口 业务 SQLite-Web SQLite 数据库管理工具 开源 存储引擎 线上 库存 预扣 Doris SeaTunnel MVCC 人工智能 向量数据库 推荐系统 R edis 线程 共享锁 网络架构 工具 网络配置 加密 场景 流量 数据备份 PG DBA AI 助手 Ftp redo log 重做日志 ​Redis 机器学习 推荐模型 信息化 智能运维 B+Tree ID 字段 • 索引 • 数据库 RDB AOF 缓存 高可用 Canal GreatSQL 连接数 Python 优化 万能公式 Redis 8.0 INSERT COMPACT 核心架构 订阅机制 网络故障 Postgres OTel Iceberg Hash 字段 同城 双活 云原生 IT运维 微软 SQL Server AI功能 prometheus Alert 不宕机 自动重启 Web 大模型 引擎 性能 SVM Embedding 高效统计 今天这篇文章就跟大家 Netstat Linux 服务器 端口 分库 分表 虚拟服务器 虚拟机 内存 向量库 Milvus OAuth2 Token Entity 开发 Undo Log 单点故障 DBMS 管理系统 MongoDB 容器 sqlmock LRU 崖山 新版本 ZODB 数据集成工具 容器化 窗口 函数 mini-redis INCR指令 JOIN 分布式 集中式 悲观锁 乐观锁 openHalo 排行榜 排序 磁盘架构 SpringAI Testcloud 云端自动化 Redka 数据脱敏 加密算法 Redisson 锁芯 QPS 高并发 分页 数据结构 EasyExcel MySQL8 Recursive PostGIS 启动故障 大表 业务场景 意向锁 记录锁 分布式架构 分布式锁​ Pottery 聚簇索引 非聚簇索引 数据类型 StarRocks 数据仓库 模型 原子性 AIOPS R2DBC 事务 Java SSH IT Caffeine CP MCP 开放协议 网络 InfluxDB 1 发件箱模式 池化技术 连接池 Web 接口 dbt 数据转换工具 RAG HelixDB 工具链 数据分类 部署 filelock 对象 速度 服务器中毒 字典 传统数据库 向量化 读写 Go 数据库迁移 事务隔离 优化器 单线程 仪表盘 双引擎 频繁 Codis 分页方案 排版 数据页 Order LLM 线程安全 Crash 代码 List 类型 UUIDv7 主键 订单 Pump 事务同步 日志 Ansible