溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

使用Redis優(yōu)化查詢性能的實(shí)踐是怎樣的

發(fā)布時間:2021-11-10 18:07:06 來源:億速云 閱讀:188 作者:柒染 欄目:大數(shù)據(jù)

使用Redis優(yōu)化查詢性能的實(shí)踐是怎樣的,很多新手對此不是很清楚,為了幫助大家解決這個難題,下面小編將為大家詳細(xì)講解,有這方面需求的人可以來學(xué)習(xí)下,希望你能有所收獲。

應(yīng)用背景

有一個應(yīng)用需要上傳一組ID到服務(wù)器來查詢這些ID所對應(yīng)的數(shù)據(jù),數(shù)據(jù)庫中存儲的數(shù)據(jù)量是7千萬,每次上傳的ID數(shù)量一般都是幾百至上千數(shù)量級別。

以前的解決方案

  1. 數(shù)據(jù)存儲在Oracle中,為ID建立了索引;

  2. 查詢時,先將這些上傳的ID數(shù)據(jù)存儲到臨時表中,然后用表關(guān)聯(lián)的方法來查詢。

這樣做的優(yōu)點(diǎn)是減少了查詢次數(shù)(不用每個ID都查詢一次),減少了解析SQL的時間(只需要執(zhí)行1次查詢SQL,但是多了插入數(shù)據(jù)的SQL處理時間)。

但是這樣的設(shè)計(jì)仍然存在巨大的提升空間,當(dāng)并發(fā)查詢的數(shù)量增加時,數(shù)據(jù)庫的響應(yīng)就會很久。雖然建立了索引,但是每個ID查詢的時間復(fù)雜度仍是O(logn)級別的,那么總的查詢時間復(fù)雜度就應(yīng)該是m*O(logn)。不知道Oracle對表關(guān)聯(lián)查詢有做了哪些優(yōu)化,但應(yīng)該也是改變不了時間復(fù)雜度的級別。

解決方法

一遇到讀數(shù)據(jù)庫存在瓶頸的問題,首先想到的就是要用內(nèi)存數(shù)據(jù)庫,用緩存來解決。首選 Redis,因?yàn)镽edis是一種提供了豐富數(shù)據(jù)結(jié)構(gòu)的key-value數(shù)據(jù)庫,value可以存儲STRING(字符串)、HASH(哈希),LIST(列表),ZSET(有序集)。

首先需要將數(shù)據(jù)的存儲改成 key-value 架構(gòu)。簡單的做法就是一個ID對應(yīng)一個字符串的 Value。但是一個 ID 可以對應(yīng)多條數(shù)據(jù),而且一條數(shù)據(jù)內(nèi)又可以包含多個字段。這時候就需要將這些數(shù)據(jù)重新組合一下,拼在一起,或者是采用列表、哈?;蚣蟻泶鎯?Value。

Redis內(nèi)部采用 HashTable(哈希表)來實(shí)現(xiàn)key-value的數(shù)據(jù)結(jié)構(gòu),是一種空間占用較高的數(shù)據(jù)結(jié)構(gòu)。而我的應(yīng)用場景又是ID有幾千萬規(guī)模的,如果按上述方法,使用每個ID作為key,那么內(nèi)存的消耗將是巨大的。每個key-vaulue結(jié)構(gòu),Redis本身的維護(hù)開銷就要80幾字節(jié),即便value存儲的是純數(shù)字(會使用long類型,占用4個字節(jié)),也依然很大,1000萬的數(shù)據(jù),就要占用快1G內(nèi)存。

使用兩級Hash優(yōu)化內(nèi)存

依據(jù)官方文檔的內(nèi)存優(yōu)化方法,以及這篇文章 節(jié)約內(nèi)存:Instagram的Redis實(shí)踐,建議對ID分段作為key,并使用 hash 來存儲第一級 key 的 value,第二級存儲較少的數(shù)據(jù)量(推薦1000),因此第二級的key使用ID的后3位。

為了節(jié)約內(nèi)存,Redis默認(rèn)使用ziplist(壓縮列表)來存儲HASH(哈希),LIST(列表),ZSET(有序集)這些數(shù)據(jù)結(jié)構(gòu)。當(dāng)某些條件被滿足時,自動轉(zhuǎn)換成 hash table(哈希表),linkedlist(雙端列表),skiplist(跳表)。

ziplist是用一個數(shù)組來實(shí)現(xiàn)的雙向鏈表結(jié)構(gòu),顧名思義,使用ziplist可以減少雙向鏈表的存儲空間,主要是節(jié)省了鏈表指針的存儲,如果存儲指向上一個鏈表結(jié)點(diǎn)和指向下一個鏈表結(jié)點(diǎn)的指針需要8個字節(jié),而轉(zhuǎn)化成存儲上一個結(jié)點(diǎn)長度和當(dāng)前結(jié)點(diǎn)長度在大多數(shù)情況下可以節(jié)省很多空間(最好的情況下只需2個字節(jié))。但是每次向鏈表增加元素都需要重新分配內(nèi)存?!?引用自這里的描述

ziplist的詳細(xì)信息請看 Redis book ziplist 章節(jié)

查看 Redis 的 .conf 文件,可以查看到轉(zhuǎn)換條件的設(shè)置信息。

# 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 查找的時間復(fù)雜度是 O(N),但是數(shù)據(jù)量較少,第二級Hash的查詢速度依然在O(1)級別。

對第二級Hash存儲的數(shù)據(jù)再編碼

在我的應(yīng)用場景中每個ID對應(yīng)的數(shù)據(jù)可以有很多個字段,這些字段有很多實(shí)際上是類型數(shù)據(jù),存儲的也是ID。為了進(jìn)一步節(jié)約內(nèi)存,對這些使用數(shù)字作為ID的字段,采用base62編碼(0-9,A-Z,a-z),這樣可以使這些ID的字符長度變短,進(jìn)一步減少在Redis中第二級hash需要存儲的數(shù)據(jù)量,從而減少Redis占用的內(nèi)存。

使用Lua腳本來處理批量操作

由于每次查詢都上傳幾百上千個ID,如果對這些ID,都單獨(dú)調(diào)用一次HGET命令,那么一次查詢就需要上千次TCP通信,速度很慢。這個時候最好的方法就是一次性將所有的查詢都發(fā)送到 Redis Server,然后在 Redis Server 處再依次執(zhí)行HGET命令,這個時候就要用到 Redis 的Pipelining(管道),Lua 腳本(需要 Redis 2.6以上版本)。這兩項(xiàng)功能可以用來處理批量操作。由于Lua腳本更簡單好用,因此我就直接選用Lua腳本。

Redis Lua 腳本具有原子性,執(zhí)行過程會鎖住 Redis Server,因此 Redis Server 會全部執(zhí)行完 Lua 腳本里面的所有命令,才會去處理其他命令請求,不用擔(dān)心并發(fā)帶來的共享資源讀寫需要加鎖問題。實(shí)際上所有的 Redis 命令都是原子的,執(zhí)行任何 Redis 命令,包括 info,都會鎖住 Redis Server。

不過需要注意的是:

為了防止某個腳本執(zhí)行時間過長導(dǎo)致Redis無法提供服務(wù)(比如陷入死循環(huán)),Redis提供了lua-time-limit參數(shù)限制腳本的最長運(yùn)行時間,默認(rèn)為5秒鐘(見.conf配置文件)。當(dāng)腳本運(yùn)行時間超過這一限制后,Redis將開始接受其他命令但不會執(zhí)行(以確保腳本的原子性,因?yàn)榇藭r腳本并沒有被終止),而是會返回"BUSY"錯誤——引用自這里的描述

遇到這種情況,就需要使用 SCRIPT KILL 命令來終止 Lua 腳本的執(zhí)行。因此,千萬要注意 Lua 腳本不能出現(xiàn)死循環(huán),也不要用來執(zhí)行費(fèi)時的操作。

性能分析

測試環(huán)境:

  • 內(nèi)存:1333MHz

  • CPU:Intel Core i3 2330M 2.2GHz

  • 硬盤:三星 SSD

實(shí)驗(yàn)基本設(shè)置:

  1. 將7000萬數(shù)據(jù)按照上面描述的方法,使用兩級Hash以及對數(shù)據(jù)再編碼,存儲到Redis中。

  2. 模擬數(shù)據(jù)請求(沒有通過HTTP請求,直接函數(shù)調(diào)用),查詢數(shù)據(jù),生成響應(yīng)的JSON數(shù)據(jù)。

(數(shù)據(jù)僅供參考,因?yàn)槲凑嬲Y(jié)合Web服務(wù)器進(jìn)行測試)

使用上述方法,對Redis的內(nèi)存優(yōu)化效果非常好。

實(shí)驗(yàn)設(shè)置:

  1. 模擬每次查詢500個ID,分批次連續(xù)查詢。用于模擬測試并發(fā)情況下的查詢性能。

響應(yīng)速度與查詢的數(shù)據(jù)量,幾乎是線性相關(guān)。30s 的時間就可以處理2000次請求,100W個ID的查詢。由于Oracle速度實(shí)在太慢,就不做測試了。

實(shí)驗(yàn)設(shè)置:

  1. 連續(xù)查詢1W個ID,每次500個,分20次。用于測試Redis中存儲的數(shù)據(jù)量對查詢性能的影響。

查詢速度受存儲數(shù)據(jù)量的影響較小。當(dāng)存儲的數(shù)據(jù)量較多時,第二級hash存儲的數(shù)據(jù)量就會更多,因此查詢時間會有略微的上升,但依然很快。

看完上述內(nèi)容是否對您有幫助呢?如果還想對相關(guān)知識有進(jìn)一步的了解或閱讀更多相關(guān)文章,請關(guān)注億速云行業(yè)資訊頻道,感謝您對億速云的支持。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI