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

我猜不少搞 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)

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

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

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

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

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

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

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

处理名称模糊查询:生成名称的通配符模式,比如 "手机",然后使用 SCAN 命令在 product_name_index 中查找所有分数包含 "手机" 的成员(这里需要注意,SCAN 命令本身不能直接根据分数的内容进行模糊查询,所以前面的索引建立方式可能需要调整,更合理的做法是将商品名称的关键词提取出来,作为有序集合的成员,分数作为商品 ID,或者使用其他数据结构来存储关键词和商品 ID 的映射关系。这里为了方便演示,假设我们有一个键为 name:手机 的集合,里面存储了所有名称包含 "手机" 的商品 ID)。获取价格在 2000 到 4000 之间的商品 ID 集合,使用 ZRANGEBYSCORE product_price_index 2000 4000。对这两个集合取交集,得到同时满足名称和价格条件的商品 ID 集合,可以使用 Redis 的 ZINTERSTORE 命令,将两个有序集合的交集存储到一个临时有序集合中。对临时有序集合进行分页查询,假设我们要按价格排序(也可以按其他条件排序),使用 ZRANGE 命令,根据页码和每页数量计算出起始和结束索引,比如第 2 页,每页 10 条,起始索引是 10,结束索引是 19,执行 ZRANGE temp_index 10 19,得到该页的商品 ID。根据商品 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<String> searchProducts(String nameKeyword, double minPrice, double maxPrice, int page, int pageSize) { List<String> resultProductIds = new ArrayList<>(); // 获取名称包含关键词的商品 ID 集合(简化处理,实际需根据关键词生成通配符并扫描) Set<String> nameMatchedProducts = new HashSet<>(); // 这里模拟通过关键词获取相关成员,实际可能需要使用 SCAN 命令遍历 product_name_index 并检查成员是否包含关键词 Set<Tuple> 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<String> 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<String> productIds = demo.searchProducts("手机", 2000, 4000, 1, 2); for (String productId : productIds) { System.out.println("查询到的商品 ID:" + productId); // 这里可以根据 productId 获取具体的商品信息 } } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.59.60.61.62.63.64.65.66.67.68.69.

五、注意事项和优化技巧

(一)索引维护

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

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

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

六、总结

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

阅读剩余
THE END