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

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

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

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