出處:redis4.0之基於LFU的熱點key發現機制redis
業務中存在訪問熱點是在所不免的,redis也會遇到這個問題,然而如何發現熱點key一直困擾着許多用戶,redis4.0爲咱們帶來了許多新特性,其中便包括基於LFU的熱點key發現機制。算法
Least Frequently Used——簡稱LFU,意爲最不常常使用,是redis4.0新增的一類內存逐出策略,關於內存逐出能夠參考文章《Redis數據過時和淘汰策略詳解》。bash
從LFU的字面意思咱們很容易聯想到key的訪問頻率,可是4.0最第一版本僅用來作內存逐出,對於訪問頻率並無很好的記錄,那麼通過一番改造,redis於4.0.3版本開始正式支持基於LFU的熱點key發現機制。函數
在redis中每一個對象都有24 bits空間來記錄LRU/LFU信息:ui
typedef struct redisObject { unsigned type:4; unsigned encoding:4; unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or * LFU data (least significant 8 bits frequency * and most significant 16 bits access time). */ int refcount; void *ptr; } robj;
當這24 bits用做LFU時,其被分爲兩部分:spa
這裏讀者可能會有疑問,8 bits最大值也就是255,只用8位來記錄訪問頻率夠用嗎?對於counter,redis用了一個trick的手段,counter並非一個簡單的線性計數器,而是用基於機率的對數計數器來實現,算法以下:code
uint8_t LFULogIncr(uint8_t counter) { if (counter == 255) return 255; double r = (double)rand()/RAND_MAX; double baseval = counter - LFU_INIT_VAL; if (baseval < 0) baseval = 0; double p = 1.0/(baseval*server.lfu_log_factor+1); if (r < p) counter++; return counter; }
對應的機率分佈計算公式爲: 1/((counter-LFU_INIT_VAL)*server.lfu_log_factor+1)
server
其中LFU_INIT_VAL爲5,咱們看下機率分佈圖會有一個更直觀的認識,以默認server.lfu_log_factor=10爲例:對象
從上圖能夠看到,counter越大,其增長的機率越小,8 bits也足夠記錄很高的訪問頻率,下表是不一樣機率因子server.lfu_log_factor與訪問頻率counter的對應關係: blog
也就是說,默認server.lfu_log_factor爲10的狀況下,8 bits的counter能夠表示1百萬的訪問頻率。
從上一小節的counter增加函數LFULogIncr中咱們能夠看到,隨着key的訪問量增加,counter最終都會收斂爲255,這就帶來一個問題,若是counter只增加不衰減就沒法區分熱點key。
爲了解決這個問題,redis提供了衰減因子server.lfu_decay_time,其單位爲分鐘,計算方法也很簡單,若是一個key長時間沒有訪問那麼它的計數器counter就要減小,減小的值由衰減因子來控制:
unsigned long LFUDecrAndReturn(robj *o) { unsigned long ldt = o->lru >> 8; unsigned long counter = o->lru & 255; unsigned long num_periods = server.lfu_decay_time ? LFUTimeElapsed(ldt) / server.lfu_decay_time : 0; if (num_periods) counter = (num_periods > counter) ? 0 : counter - num_periods; return counter; }
默認爲1的狀況下也就是N分鐘內沒有訪問,counter就要減N。
機率因子和衰減因子都可配置,推薦使用redis的默認值便可:
lfu-log-factor 10 lfu-decay-time 1
介紹完LFU算法,接下來就是咱們關心的熱點key發現機制。
其核心就是在每次對key進行讀寫訪問時,更新LFU的24 bits域表明的訪問時間和counter,這樣每一個key就能夠得到正確的LFU值:
void updateLFU(robj *val) { unsigned long counter = LFUDecrAndReturn(val); counter = LFULogIncr(counter); val->lru = (LFUGetTimeInMinutes()<<8) | counter; }
那麼用戶如何獲取訪問頻率呢?redis提供了OBJECT FREQ子命令來獲取LFU信息,可是要注意須要先把內存逐出策略設置爲allkeys-lfu或者volatile-lfu,不然會返回錯誤:
127.0.0.1:6379> config get maxmemory-policy 1) "maxmemory-policy" 2) "noeviction" 127.0.0.1:6379> object freq counter:000000006889 (error) ERR An LFU maxmemory policy is not selected, access frequency not tracked. Please note that when switching between policies at runtime LRU and LFU data will take some time to adjust. 127.0.0.1:6379> config set maxmemory-policy allkeys-lfu OK 127.0.0.1:6379> object freq counter:000000006889 (integer) 3
使用scan命令遍歷全部key,再經過OBJECT FREQ獲取訪問頻率並排序,便可獲得熱點key。爲了方便用戶使用,redis 4.0.3同時也提供了redis-cli的熱點key發現功能,執行redis-cli時加上--hotkeys選項便可,示例以下:
$./redis-cli --hotkeys # Scanning the entire keyspace to find hot keys as well as # average sizes per key type. You can use -i 0.1 to sleep 0.1 sec # per 100 SCAN commands (not usually needed). [00.00%] Hot key 'counter:000000000002' found so far with counter 87 [00.00%] Hot key 'key:000000000001' found so far with counter 254 [00.00%] Hot key 'mylist' found so far with counter 107 [00.00%] Hot key 'key:000000000000' found so far with counter 254 [45.45%] Hot key 'counter:000000000001' found so far with counter 87 [45.45%] Hot key 'key:000000000002' found so far with counter 254 [45.45%] Hot key 'myset' found so far with counter 64 [45.45%] Hot key 'counter:000000000000' found so far with counter 93 -------- summary ------- Sampled 22 keys in the keyspace! hot key found with counter: 254 keyname: key:000000000001 hot key found with counter: 254 keyname: key:000000000000 hot key found with counter: 254 keyname: key:000000000002 hot key found with counter: 107 keyname: mylist hot key found with counter: 93 keyname: counter:000000000000 hot key found with counter: 87 keyname: counter:000000000002 hot key found with counter: 87 keyname: counter:000000000001 hot key found with counter: 64 keyname: myset
能夠看到,排在前幾位的便是熱點key。