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 獲取具體的商品信息
}
}
}
五、注意事項和優化技巧
(一)索引維護
建立的索引會增加 Redis 的內存占用,所以要根據實際的查詢需求,合理選擇需要建立索引的條件,不要建立過多無用的索引。同時,在數據更新(比如刪除、修改)時,要及時更新對應的索引,保證索引的一致性。
(二)性能優化
- 對于大規模數據,使用 SCAN 命令代替 KEYS 命令進行鍵的掃描,避免全量掃描影響 Redis 性能。
- 在進行集合交集、并集等操作時,注意集合的大小,如果集合過大,操作可能會比較耗時,可以考慮在應用層進行部分過濾,減少 Redis 層的操作壓力。
- 可以對常用的查詢結果進行緩存,比如熱門的查詢條件和分頁結果,減少重復查詢的開銷。
(三)數據結構選擇
根據不同的業務場景選擇合適的數據結構,比如有序集合適合需要排序和范圍查詢的場景,集合適合需要去重和交集、并集操作的場景,字符串適合存儲單個對象的詳細信息。
六、總結
通過上面的方案,咱們基本上解決了 Redis 分頁和多條件模糊查詢的難題。利用有序集合、集合等數據結構建立索引,對數據進行預處理,結合分頁算法,能夠在 Redis 中實現高效的分頁和多條件查詢。當然,具體的實現還需要根據項目的實際需求進行調整和優化,比如索引的建立方式、數據結構的選擇、查詢條件的處理等。