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 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 的内存占用,所以要根据实际的查询需求,合理选择需要建立索引的条件,不要建立过多无用的索引。同时,在数据更新(比如删除、修改)时,要及时更新对应的索引,保证索引的一致性。
(二)性能优化
- 对于大规模数据,使用 SCAN 命令代替 KEYS 命令进行键的扫描,避免全量扫描影响 Redis 性能。
- 在进行集合交集、并集等操作时,注意集合的大小,如果集合过大,操作可能会比较耗时,可以考虑在应用层进行部分过滤,减少 Redis 层的操作压力。
- 可以对常用的查询结果进行缓存,比如热门的查询条件和分页结果,减少重复查询的开销。
(三)数据结构选择
根据不同的业务场景选择合适的数据结构,比如有序集合适合需要排序和范围查询的场景,集合适合需要去重和交集、并集操作的场景,字符串适合存储单个对象的详细信息。
六、总结
通过上面的方案,咱们基本上解决了 Redis 分页和多条件模糊查询的难题。利用有序集合、集合等数据结构建立索引,对数据进行预处理,结合分页算法,能够在 Redis 中实现高效的分页和多条件查询。当然,具体的实现还需要根据项目的实际需求进行调整和优化,比如索引的建立方式、数据结构的选择、查询条件的处理等。
本文地址:https://www.yitenyun.com/186.html