您好,登錄后才能下訂單哦!
使用Redis優化查詢性能的實踐是怎樣的,很多新手對此不是很清楚,為了幫助大家解決這個難題,下面小編將為大家詳細講解,有這方面需求的人可以來學習下,希望你能有所收獲。
應用背景
有一個應用需要上傳一組ID到服務器來查詢這些ID所對應的數據,數據庫中存儲的數據量是7千萬,每次上傳的ID數量一般都是幾百至上千數量級別。
以前的解決方案
數據存儲在Oracle中,為ID建立了索引;
查詢時,先將這些上傳的ID數據存儲到臨時表中,然后用表關聯的方法來查詢。
這樣做的優點是減少了查詢次數(不用每個ID都查詢一次),減少了解析SQL的時間(只需要執行1次查詢SQL,但是多了插入數據的SQL處理時間)。
但是這樣的設計仍然存在巨大的提升空間,當并發查詢的數量增加時,數據庫的響應就會很久。雖然建立了索引,但是每個ID查詢的時間復雜度仍是O(logn)級別的,那么總的查詢時間復雜度就應該是m*O(logn)。不知道Oracle對表關聯查詢有做了哪些優化,但應該也是改變不了時間復雜度的級別。
解決方法
一遇到讀數據庫存在瓶頸的問題,首先想到的就是要用內存數據庫,用緩存來解決。首選 Redis,因為Redis是一種提供了豐富數據結構的key-value數據庫,value可以存儲STRING(字符串)、HASH(哈希),LIST(列表),ZSET(有序集)。
首先需要將數據的存儲改成 key-value 架構。簡單的做法就是一個ID對應一個字符串的 Value。但是一個 ID 可以對應多條數據,而且一條數據內又可以包含多個字段。這時候就需要將這些數據重新組合一下,拼在一起,或者是采用列表、哈希或集合來存儲 Value。
Redis內部采用 HashTable(哈希表)來實現key-value的數據結構,是一種空間占用較高的數據結構。而我的應用場景又是ID有幾千萬規模的,如果按上述方法,使用每個ID作為key,那么內存的消耗將是巨大的。每個key-vaulue結構,Redis本身的維護開銷就要80幾字節,即便value存儲的是純數字(會使用long類型,占用4個字節),也依然很大,1000萬的數據,就要占用快1G內存。
使用兩級Hash優化內存
依據官方文檔的內存優化方法,以及這篇文章 節約內存:Instagram的Redis實踐,建議對ID分段作為key,并使用 hash 來存儲第一級 key 的 value,第二級存儲較少的數據量(推薦1000),因此第二級的key使用ID的后3位。
為了節約內存,Redis默認使用ziplist(壓縮列表)來存儲HASH(哈希),LIST(列表),ZSET(有序集)這些數據結構。當某些條件被滿足時,自動轉換成 hash table(哈希表),linkedlist(雙端列表),skiplist(跳表)。
ziplist是用一個數組來實現的雙向鏈表結構,顧名思義,使用ziplist可以減少雙向鏈表的存儲空間,主要是節省了鏈表指針的存儲,如果存儲指向上一個鏈表結點和指向下一個鏈表結點的指針需要8個字節,而轉化成存儲上一個結點長度和當前結點長度在大多數情況下可以節省很多空間(最好的情況下只需2個字節)。但是每次向鏈表增加元素都需要重新分配內存。—— 引用自這里的描述
ziplist的詳細信息請看 Redis book ziplist 章節
查看 Redis 的 .conf 文件,可以查看到轉換條件的設置信息。
# Hashes are encoded using a memory efficient data structure when they have a# small number of entries, and the biggest entry does not exceed a given# threshold. These thresholds can be configured using the following directives.hash-max-ziplist-entries 512hash-max-ziplist-value 64# Similarly to hashes, small lists are also encoded in a special way in order# to save a lot of space. The special representation is only used when# you are under the following limits:list-max-ziplist-entries 512list-max-ziplist-value 64# Similarly to hashes and lists, sorted sets are also specially encoded in# order to save a lot of space. This encoding is only used when the length and# elements of a sorted set are below the following limits:zset-max-ziplist-entries 128zset-max-ziplist-value 64
ziplist 查找的時間復雜度是 O(N),但是數據量較少,第二級Hash的查詢速度依然在O(1)級別。
對第二級Hash存儲的數據再編碼
在我的應用場景中每個ID對應的數據可以有很多個字段,這些字段有很多實際上是類型數據,存儲的也是ID。為了進一步節約內存,對這些使用數字作為ID的字段,采用base62編碼(0-9,A-Z,a-z),這樣可以使這些ID的字符長度變短,進一步減少在Redis中第二級hash需要存儲的數據量,從而減少Redis占用的內存。
使用Lua腳本來處理批量操作
由于每次查詢都上傳幾百上千個ID,如果對這些ID,都單獨調用一次HGET命令,那么一次查詢就需要上千次TCP通信,速度很慢。這個時候最好的方法就是一次性將所有的查詢都發送到 Redis Server,然后在 Redis Server 處再依次執行HGET命令,這個時候就要用到 Redis 的Pipelining(管道),Lua 腳本(需要 Redis 2.6以上版本)。這兩項功能可以用來處理批量操作。由于Lua腳本更簡單好用,因此我就直接選用Lua腳本。
Redis Lua 腳本具有原子性,執行過程會鎖住 Redis Server,因此 Redis Server 會全部執行完 Lua 腳本里面的所有命令,才會去處理其他命令請求,不用擔心并發帶來的共享資源讀寫需要加鎖問題。實際上所有的 Redis 命令都是原子的,執行任何 Redis 命令,包括 info,都會鎖住 Redis Server。
不過需要注意的是:
為了防止某個腳本執行時間過長導致Redis無法提供服務(比如陷入死循環),Redis提供了lua-time-limit參數限制腳本的最長運行時間,默認為5秒鐘(見.conf配置文件)。當腳本運行時間超過這一限制后,Redis將開始接受其他命令但不會執行(以確保腳本的原子性,因為此時腳本并沒有被終止),而是會返回"BUSY"錯誤——引用自這里的描述
遇到這種情況,就需要使用 SCRIPT KILL
命令來終止 Lua 腳本的執行。因此,千萬要注意 Lua 腳本不能出現死循環,也不要用來執行費時的操作。
性能分析
測試環境:
內存:1333MHz
CPU:Intel Core i3 2330M 2.2GHz
硬盤:三星 SSD
實驗基本設置:
將7000萬數據按照上面描述的方法,使用兩級Hash以及對數據再編碼,存儲到Redis中。
模擬數據請求(沒有通過HTTP請求,直接函數調用),查詢數據,生成響應的JSON數據。
(數據僅供參考,因為未真正結合Web服務器進行測試)
使用上述方法,對Redis的內存優化效果非常好。
實驗設置:
模擬每次查詢500個ID,分批次連續查詢。用于模擬測試并發情況下的查詢性能。
響應速度與查詢的數據量,幾乎是線性相關。30s 的時間就可以處理2000次請求,100W個ID的查詢。由于Oracle速度實在太慢,就不做測試了。
實驗設置:
連續查詢1W個ID,每次500個,分20次。用于測試Redis中存儲的數據量對查詢性能的影響。
查詢速度受存儲數據量的影響較小。當存儲的數據量較多時,第二級hash存儲的數據量就會更多,因此查詢時間會有略微的上升,但依然很快。
看完上述內容是否對您有幫助呢?如果還想對相關知識有進一步的了解或閱讀更多相關文章,請關注億速云行業資訊頻道,感謝您對億速云的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。