由於個人我的網站 restran.net 已經啓用,博客園的內容已經再也不更新。請訪問個人我的網站獲取這篇文章的最新內容,一次使用 Redis 優化查詢性能的實踐html
應用背景redis
有一個應用須要上傳一組ID到服務器來查詢這些ID所對應的數據,數據庫中存儲的數據量是7千萬,每次上傳的ID數量通常都是幾百至上千數量級別。sql
之前的解決方案數據庫
這樣作的優勢是減小了查詢次數(不用每一個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 512 hash-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 512 list-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 128 zset-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 腳本不能出現死循環,也不要用來執行費時的操做。
性能分析
測試環境:
實驗基本設置:
(數據僅供參考,由於未真正結合Web服務器進行測試)
使用上述方法,對Redis的內存優化效果很是好。
實驗設置:
響應速度與查詢的數據量,幾乎是線性相關。30s 的時間就能夠處理2000次請求,100W個ID的查詢。因爲Oracle速度實在太慢,就不作測試了。
實驗設置:
查詢速度受存儲數據量的影響較小。當存儲的數據量較多時,第二級hash存儲的數據量就會更多,所以查詢時間會有略微的上升,但依然很快。
參考文獻