前言
Redis作為緩存使用時(shí),一些場(chǎng)景下要考慮內(nèi)存的空間消耗問題。Redis會(huì)刪除過期鍵以釋放空間,過期鍵的刪除策略有兩種:
- 惰性刪除:每次從鍵空間中獲取鍵時(shí),都檢查取得的鍵是否過期,如果過期的話,就刪除該鍵;如果沒有過期,就返回該鍵。
- 定期刪除:每隔一段時(shí)間,程序就對(duì)數(shù)據(jù)庫進(jìn)行一次檢查,刪除里面的過期鍵。
另外,Redis也可以開啟LRU功能來自動(dòng)淘汰一些鍵值對(duì)。
LRU算法
當(dāng)需要從緩存中淘汰數(shù)據(jù)時(shí),我們希望能淘汰那些將來不可能再被使用的數(shù)據(jù),保留那些將來還會(huì)頻繁訪問的數(shù)據(jù),但最大的問題是緩存并不能預(yù)言未來。一個(gè)解決方法就是通過LRU進(jìn)行預(yù)測(cè):最近被頻繁訪問的數(shù)據(jù)將來被訪問的可能性也越大。緩存中的數(shù)據(jù)一般會(huì)有這樣的訪問分布:一部分?jǐn)?shù)據(jù)擁有絕大部分的訪問量。當(dāng)訪問模式很少改變時(shí),可以記錄每個(gè)數(shù)據(jù)的最后一次訪問時(shí)間,擁有最少空閑時(shí)間的數(shù)據(jù)可以被認(rèn)為將來最有可能被訪問到。
舉例如下的訪問模式,A每5s訪問一次,B每2s訪問一次,C與D每10s訪問一次,|代表計(jì)算空閑時(shí)間的截止點(diǎn):
~~~~~A~~~~~A~~~~~A~~~~A~~~~~A~~~~~A~~|
~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~|
~~~~~~~~~~C~~~~~~~~~C~~~~~~~~~C~~~~~~|
~~~~~D~~~~~~~~~~D~~~~~~~~~D~~~~~~~~~D|
可以看到,LRU對(duì)于A、B、C工作的很好,完美預(yù)測(cè)了將來被訪問到的概率B>A>C,但對(duì)于D卻預(yù)測(cè)了最少的空閑時(shí)間。
但是,總體來說,LRU算法已經(jīng)是一個(gè)性能足夠好的算法了
LRU配置參數(shù)
Redis配置中和LRU有關(guān)的有三個(gè):
- maxmemory: 配置Redis存儲(chǔ)數(shù)據(jù)時(shí)指定限制的內(nèi)存大小,比如100m。當(dāng)緩存消耗的內(nèi)存超過這個(gè)數(shù)值時(shí), 將觸發(fā)數(shù)據(jù)淘汰。該數(shù)據(jù)配置為0時(shí),表示緩存的數(shù)據(jù)量沒有限制, 即LRU功能不生效。64位的系統(tǒng)默認(rèn)值為0,32位的系統(tǒng)默認(rèn)內(nèi)存限制為3GB
- maxmemory_policy: 觸發(fā)數(shù)據(jù)淘汰后的淘汰策略
- maxmemory_samples: 隨機(jī)采樣的精度,也就是隨即取出key的數(shù)目。該數(shù)值配置越大, 越接近于真實(shí)的LRU算法,但是數(shù)值越大,相應(yīng)消耗也變高,對(duì)性能有一定影響,樣本值默認(rèn)為5。
淘汰策略
淘汰策略即maxmemory_policy的賦值有以下幾種:
- noeviction:如果緩存數(shù)據(jù)超過了maxmemory限定值,并且客戶端正在執(zhí)行的命令(大部分的寫入指令,但DEL和幾個(gè)指令例外)會(huì)導(dǎo)致內(nèi)存分配,則向客戶端返回錯(cuò)誤響應(yīng)
- allkeys-lru: 對(duì)所有的鍵都采取LRU淘汰
- volatile-lru: 僅對(duì)設(shè)置了過期時(shí)間的鍵采取LRU淘汰
- allkeys-random: 隨機(jī)回收所有的鍵
- volatile-random: 隨機(jī)回收設(shè)置過期時(shí)間的鍵
- volatile-ttl: 僅淘汰設(shè)置了過期時(shí)間的鍵---淘汰生存時(shí)間TTL(Time To Live)更小的鍵
volatile-lru, volatile-random和volatile-ttl這三個(gè)淘汰策略使用的不是全量數(shù)據(jù),有可能無法淘汰出足夠的內(nèi)存空間。在沒有過期鍵或者沒有設(shè)置超時(shí)屬性的鍵的情況下,這三種策略和noeviction差不多。
一般的經(jīng)驗(yàn)規(guī)則:
- 使用allkeys-lru策略:當(dāng)預(yù)期請(qǐng)求符合一個(gè)冪次分布(二八法則等),比如一部分的子集元素比其它其它元素被訪問的更多時(shí),可以選擇這個(gè)策略。
- 使用allkeys-random:循環(huán)連續(xù)的訪問所有的鍵時(shí),或者預(yù)期請(qǐng)求分布平均(所有元素被訪問的概率都差不多)
- 使用volatile-ttl:要采取這個(gè)策略,緩存對(duì)象的TTL值最好有差異
volatile-lru 和 volatile-random策略,當(dāng)你想要使用單一的Redis實(shí)例來同時(shí)實(shí)現(xiàn)緩存淘汰和持久化一些經(jīng)常使用的鍵集合時(shí)很有用。未設(shè)置過期時(shí)間的鍵進(jìn)行持久化保存,設(shè)置了過期時(shí)間的鍵參與緩存淘汰。不過一般運(yùn)行兩個(gè)實(shí)例是解決這個(gè)問題的更好方法。
為鍵設(shè)置過期時(shí)間也是需要消耗內(nèi)存的,所以使用allkeys-lru這種策略更加節(jié)省空間,因?yàn)檫@種策略下可以不為鍵設(shè)置過期時(shí)間。
近似LRU算法
我們知道,LRU算法需要一個(gè)雙向鏈表來記錄數(shù)據(jù)的最近被訪問順序,但是出于節(jié)省內(nèi)存的考慮,Redis的LRU算法并非完整的實(shí)現(xiàn)。Redis并不會(huì)選擇最久未被訪問的鍵進(jìn)行回收,相反它會(huì)嘗試運(yùn)行一個(gè)近似LRU的算法,通過對(duì)少量鍵進(jìn)行取樣,然后回收其中的最久未被訪問的鍵。通過調(diào)整每次回收時(shí)的采樣數(shù)量maxmemory-samples,可以實(shí)現(xiàn)調(diào)整算法的精度。
根據(jù)Redis作者的說法,每個(gè)Redis Object可以擠出24 bits的空間,但24 bits是不夠存儲(chǔ)兩個(gè)指針的,而存儲(chǔ)一個(gè)低位時(shí)間戳是足夠的,Redis Object以秒為單位存儲(chǔ)了對(duì)象新建或者更新時(shí)的unix time,也就是LRU clock,24 bits數(shù)據(jù)要溢出的話需要194天,而緩存的數(shù)據(jù)更新非常頻繁,已經(jīng)足夠了。
Redis的鍵空間是放在一個(gè)哈希表中的,要從所有的鍵中選出一個(gè)最久未被訪問的鍵,需要另外一個(gè)數(shù)據(jù)結(jié)構(gòu)存儲(chǔ)這些源信息,這顯然不劃算。最初,Redis只是隨機(jī)的選3個(gè)key,然后從中淘汰,后來算法改進(jìn)到了N個(gè)key的策略,默認(rèn)是5個(gè)。
Redis3.0之后又改善了算法的性能,會(huì)提供一個(gè)待淘汰候選key的pool,里面默認(rèn)有16個(gè)key,按照空閑時(shí)間排好序。更新時(shí)從Redis鍵空間隨機(jī)選擇N個(gè)key,分別計(jì)算它們的空閑時(shí)間idle,key只會(huì)在pool不滿或者空閑時(shí)間大于pool里最小的時(shí),才會(huì)進(jìn)入pool,然后從pool中選擇空閑時(shí)間最大的key淘汰掉。
真實(shí)LRU算法與近似LRU的算法可以通過下面的圖像對(duì)比:
淺灰色帶是已經(jīng)被淘汰的對(duì)象,灰色帶是沒有被淘汰的對(duì)象,綠色帶是新添加的對(duì)象。可以看出,maxmemory-samples值為5時(shí)Redis 3.0效果比Redis 2.8要好。使用10個(gè)采樣大小的Redis 3.0的近似LRU算法已經(jīng)非常接近理論的性能了。
數(shù)據(jù)訪問模式非常接近冪次分布時(shí),也就是大部分的訪問集中于部分鍵時(shí),LRU近似算法會(huì)處理得很好。
在模擬實(shí)驗(yàn)的過程中,我們發(fā)現(xiàn)如果使用冪次分布的訪問模式,真實(shí)LRU算法和近似LRU算法幾乎沒有差別。
LRU源碼分析
Redis中的鍵與值都是redisObject對(duì)象:
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;
unsigned的低24 bits的lru記錄了redisObj的LRU time。
Redis命令訪問緩存的數(shù)據(jù)時(shí),均會(huì)調(diào)用函數(shù)lookupKey:
robj *lookupKey(redisDb *db, robj *key, int flags) {
dictEntry *de = dictFind(db->dict,key->ptr);
if (de) {
robj *val = dictGetVal(de);
/* Update the access time for the ageing algorithm.
* Don't do it if we have a saving child, as this will trigger
* a copy on write madness. */
if (server.rdb_child_pid == -1
server.aof_child_pid == -1
!(flags LOOKUP_NOTOUCH))
{
if (server.maxmemory_policy MAXMEMORY_FLAG_LFU) {
updateLFU(val);
} else {
val->lru = LRU_CLOCK();
}
}
return val;
} else {
return NULL;
}
}
該函數(shù)在策略為L(zhǎng)RU(非LFU)時(shí)會(huì)更新對(duì)象的lru值, 設(shè)置為L(zhǎng)RU_CLOCK()值:
/* Return the LRU clock, based on the clock resolution. This is a time
* in a reduced-bits format that can be used to set and check the
* object->lru field of redisObject structures. */
unsigned int getLRUClock(void) {
return (mstime()/LRU_CLOCK_RESOLUTION) LRU_CLOCK_MAX;
}
/* This function is used to obtain the current LRU clock.
* If the current resolution is lower than the frequency we refresh the
* LRU clock (as it should be in production servers) we return the
* precomputed value, otherwise we need to resort to a system call. */
unsigned int LRU_CLOCK(void) {
unsigned int lruclock;
if (1000/server.hz = LRU_CLOCK_RESOLUTION) {
atomicGet(server.lruclock,lruclock);
} else {
lruclock = getLRUClock();
}
return lruclock;
}
LRU_CLOCK()取決于LRU_CLOCK_RESOLUTION(默認(rèn)值1000),LRU_CLOCK_RESOLUTION代表了LRU算法的精度,即一個(gè)LRU的單位是多長(zhǎng)。server.hz代表服務(wù)器刷新的頻率,如果服務(wù)器的時(shí)間更新精度值比LRU的精度值要小,LRU_CLOCK()直接使用服務(wù)器的時(shí)間,減小開銷。
Redis處理命令的入口是processCommand:
int processCommand(client *c) {
/* Handle the maxmemory directive.
*
* Note that we do not want to reclaim memory if we are here re-entering
* the event loop since there is a busy Lua script running in timeout
* condition, to avoid mixing the propagation of scripts with the
* propagation of DELs due to eviction. */
if (server.maxmemory !server.lua_timedout) {
int out_of_memory = freeMemoryIfNeededAndSafe() == C_ERR;
/* freeMemoryIfNeeded may flush slave output buffers. This may result
* into a slave, that may be the active client, to be freed. */
if (server.current_client == NULL) return C_ERR;
/* It was impossible to free enough memory, and the command the client
* is trying to execute is denied during OOM conditions or the client
* is in MULTI/EXEC context? Error. */
if (out_of_memory
(c->cmd->flags CMD_DENYOOM ||
(c->flags CLIENT_MULTI c->cmd->proc != execCommand))) {
flagTransaction(c);
addReply(c, shared.oomerr);
return C_OK;
}
}
}
只列出了釋放內(nèi)存空間的部分,freeMemoryIfNeededAndSafe為釋放內(nèi)存的函數(shù):
int freeMemoryIfNeeded(void) {
/* By default replicas should ignore maxmemory
* and just be masters exact copies. */
if (server.masterhost server.repl_slave_ignore_maxmemory) return C_OK;
size_t mem_reported, mem_tofree, mem_freed;
mstime_t latency, eviction_latency;
long long delta;
int slaves = listLength(server.slaves);
/* When clients are paused the dataset should be static not just from the
* POV of clients not being able to write, but also from the POV of
* expires and evictions of keys not being performed. */
if (clientsArePaused()) return C_OK;
if (getMaxmemoryState(mem_reported,NULL,mem_tofree,NULL) == C_OK)
return C_OK;
mem_freed = 0;
if (server.maxmemory_policy == MAXMEMORY_NO_EVICTION)
goto cant_free; /* We need to free memory, but policy forbids. */
latencyStartMonitor(latency);
while (mem_freed mem_tofree) {
int j, k, i, keys_freed = 0;
static unsigned int next_db = 0;
sds bestkey = NULL;
int bestdbid;
redisDb *db;
dict *dict;
dictEntry *de;
if (server.maxmemory_policy (MAXMEMORY_FLAG_LRU|MAXMEMORY_FLAG_LFU) ||
server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL)
{
struct evictionPoolEntry *pool = EvictionPoolLRU;
while(bestkey == NULL) {
unsigned long total_keys = 0, keys;
/* We don't want to make local-db choices when expiring keys,
* so to start populate the eviction pool sampling keys from
* every DB. */
for (i = 0; i server.dbnum; i++) {
db = server.db+i;
dict = (server.maxmemory_policy MAXMEMORY_FLAG_ALLKEYS) ?
db->dict : db->expires;
if ((keys = dictSize(dict)) != 0) {
evictionPoolPopulate(i, dict, db->dict, pool);
total_keys += keys;
}
}
if (!total_keys) break; /* No keys to evict. */
/* Go backward from best to worst element to evict. */
for (k = EVPOOL_SIZE-1; k >= 0; k--) {
if (pool[k].key == NULL) continue;
bestdbid = pool[k].dbid;
if (server.maxmemory_policy MAXMEMORY_FLAG_ALLKEYS) {
de = dictFind(server.db[pool[k].dbid].dict,
pool[k].key);
} else {
de = dictFind(server.db[pool[k].dbid].expires,
pool[k].key);
}
/* Remove the entry from the pool. */
if (pool[k].key != pool[k].cached)
sdsfree(pool[k].key);
pool[k].key = NULL;
pool[k].idle = 0;
/* If the key exists, is our pick. Otherwise it is
* a ghost and we need to try the next element. */
if (de) {
bestkey = dictGetKey(de);
break;
} else {
/* Ghost... Iterate again. */
}
}
}
}
/* volatile-random and allkeys-random policy */
else if (server.maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM ||
server.maxmemory_policy == MAXMEMORY_VOLATILE_RANDOM)
{
/* When evicting a random key, we try to evict a key for
* each DB, so we use the static 'next_db' variable to
* incrementally visit all DBs. */
for (i = 0; i server.dbnum; i++) {
j = (++next_db) % server.dbnum;
db = server.db+j;
dict = (server.maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM) ?
db->dict : db->expires;
if (dictSize(dict) != 0) {
de = dictGetRandomKey(dict);
bestkey = dictGetKey(de);
bestdbid = j;
break;
}
}
}
/* Finally remove the selected key. */
if (bestkey) {
db = server.db+bestdbid;
robj *keyobj = createStringObject(bestkey,sdslen(bestkey));
propagateExpire(db,keyobj,server.lazyfree_lazy_eviction);
/* We compute the amount of memory freed by db*Delete() alone.
* It is possible that actually the memory needed to propagate
* the DEL in AOF and replication link is greater than the one
* we are freeing removing the key, but we can't account for
* that otherwise we would never exit the loop.
*
* AOF and Output buffer memory will be freed eventually so
* we only care about memory used by the key space. */
delta = (long long) zmalloc_used_memory();
latencyStartMonitor(eviction_latency);
if (server.lazyfree_lazy_eviction)
dbAsyncDelete(db,keyobj);
else
dbSyncDelete(db,keyobj);
latencyEndMonitor(eviction_latency);
latencyAddSampleIfNeeded("eviction-del",eviction_latency);
latencyRemoveNestedEvent(latency,eviction_latency);
delta -= (long long) zmalloc_used_memory();
mem_freed += delta;
server.stat_evictedkeys++;
notifyKeyspaceEvent(NOTIFY_EVICTED, "evicted",
keyobj, db->id);
decrRefCount(keyobj);
keys_freed++;
/* When the memory to free starts to be big enough, we may
* start spending so much time here that is impossible to
* deliver data to the slaves fast enough, so we force the
* transmission here inside the loop. */
if (slaves) flushSlavesOutputBuffers();
/* Normally our stop condition is the ability to release
* a fixed, pre-computed amount of memory. However when we
* are deleting objects in another thread, it's better to
* check, from time to time, if we already reached our target
* memory, since the "mem_freed" amount is computed only
* across the dbAsyncDelete() call, while the thread can
* release the memory all the time. */
if (server.lazyfree_lazy_eviction !(keys_freed % 16)) {
if (getMaxmemoryState(NULL,NULL,NULL,NULL) == C_OK) {
/* Let's satisfy our stop condition. */
mem_freed = mem_tofree;
}
}
}
if (!keys_freed) {
latencyEndMonitor(latency);
latencyAddSampleIfNeeded("eviction-cycle",latency);
goto cant_free; /* nothing to free... */
}
}
latencyEndMonitor(latency);
latencyAddSampleIfNeeded("eviction-cycle",latency);
return C_OK;
cant_free:
/* We are here if we are not able to reclaim memory. There is only one
* last thing we can try: check if the lazyfree thread has jobs in queue
* and wait... */
while(bioPendingJobsOfType(BIO_LAZY_FREE)) {
if (((mem_reported - zmalloc_used_memory()) + mem_freed) >= mem_tofree)
break;
usleep(1000);
}
return C_ERR;
}
/* This is a wrapper for freeMemoryIfNeeded() that only really calls the
* function if right now there are the conditions to do so safely:
*
* - There must be no script in timeout condition.
* - Nor we are loading data right now.
*
*/
int freeMemoryIfNeededAndSafe(void) {
if (server.lua_timedout || server.loading) return C_OK;
return freeMemoryIfNeeded();
}
幾種淘汰策略maxmemory_policy就是在這個(gè)函數(shù)里面實(shí)現(xiàn)的。
當(dāng)采用LRU時(shí),可以看到,從0號(hào)數(shù)據(jù)庫開始(默認(rèn)16個(gè)),根據(jù)不同的策略,選擇redisDb的dict(全部鍵)或者expires(有過期時(shí)間的鍵),用來更新候選鍵池子pool,pool更新策略是evictionPoolPopulate:
void evictionPoolPopulate(int dbid, dict *sampledict, dict *keydict, struct evictionPoolEntry *pool) {
int j, k, count;
dictEntry *samples[server.maxmemory_samples];
count = dictGetSomeKeys(sampledict,samples,server.maxmemory_samples);
for (j = 0; j count; j++) {
unsigned long long idle;
sds key;
robj *o;
dictEntry *de;
de = samples[j];
key = dictGetKey(de);
/* If the dictionary we are sampling from is not the main
* dictionary (but the expires one) we need to lookup the key
* again in the key dictionary to obtain the value object. */
if (server.maxmemory_policy != MAXMEMORY_VOLATILE_TTL) {
if (sampledict != keydict) de = dictFind(keydict, key);
o = dictGetVal(de);
}
/* Calculate the idle time according to the policy. This is called
* idle just because the code initially handled LRU, but is in fact
* just a score where an higher score means better candidate. */
if (server.maxmemory_policy MAXMEMORY_FLAG_LRU) {
idle = estimateObjectIdleTime(o);
} else if (server.maxmemory_policy MAXMEMORY_FLAG_LFU) {
/* When we use an LRU policy, we sort the keys by idle time
* so that we expire keys starting from greater idle time.
* However when the policy is an LFU one, we have a frequency
* estimation, and we want to evict keys with lower frequency
* first. So inside the pool we put objects using the inverted
* frequency subtracting the actual frequency to the maximum
* frequency of 255. */
idle = 255-LFUDecrAndReturn(o);
} else if (server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL) {
/* In this case the sooner the expire the better. */
idle = ULLONG_MAX - (long)dictGetVal(de);
} else {
serverPanic("Unknown eviction policy in evictionPoolPopulate()");
}
/* Insert the element inside the pool.
* First, find the first empty bucket or the first populated
* bucket that has an idle time smaller than our idle time. */
k = 0;
while (k EVPOOL_SIZE
pool[k].key
pool[k].idle idle) k++;
if (k == 0 pool[EVPOOL_SIZE-1].key != NULL) {
/* Can't insert if the element is the worst element we have
* and there are no empty buckets. */
continue;
} else if (k EVPOOL_SIZE pool[k].key == NULL) {
/* Inserting into empty position. No setup needed before insert. */
} else {
/* Inserting in the middle. Now k points to the first element
* greater than the element to insert. */
if (pool[EVPOOL_SIZE-1].key == NULL) {
/* Free space on the right? Insert at k shifting
* all the elements from k to end to the right. */
/* Save SDS before overwriting. */
sds cached = pool[EVPOOL_SIZE-1].cached;
memmove(pool+k+1,pool+k,
sizeof(pool[0])*(EVPOOL_SIZE-k-1));
pool[k].cached = cached;
} else {
/* No free space on right? Insert at k-1 */
k--;
/* Shift all elements on the left of k (included) to the
* left, so we discard the element with smaller idle time. */
sds cached = pool[0].cached; /* Save SDS before overwriting. */
if (pool[0].key != pool[0].cached) sdsfree(pool[0].key);
memmove(pool,pool+1,sizeof(pool[0])*k);
pool[k].cached = cached;
}
}
/* Try to reuse the cached SDS string allocated in the pool entry,
* because allocating and deallocating this object is costly
* (according to the profiler, not my fantasy. Remember:
* premature optimizbla bla bla bla. */
int klen = sdslen(key);
if (klen > EVPOOL_CACHED_SDS_SIZE) {
pool[k].key = sdsdup(key);
} else {
memcpy(pool[k].cached,key,klen+1);
sdssetlen(pool[k].cached,klen);
pool[k].key = pool[k].cached;
}
pool[k].idle = idle;
pool[k].dbid = dbid;
}
}
Redis隨機(jī)選擇maxmemory_samples數(shù)量的key,然后計(jì)算這些key的空閑時(shí)間idle time,當(dāng)滿足條件時(shí)(比pool中的某些鍵的空閑時(shí)間還大)就可以進(jìn)pool。pool更新之后,就淘汰pool中空閑時(shí)間最大的鍵。
estimateObjectIdleTime用來計(jì)算Redis對(duì)象的空閑時(shí)間:
/* Given an object returns the min number of milliseconds the object was never
* requested, using an approximated LRU algorithm. */
unsigned long long estimateObjectIdleTime(robj *o) {
unsigned long long lruclock = LRU_CLOCK();
if (lruclock >= o->lru) {
return (lruclock - o->lru) * LRU_CLOCK_RESOLUTION;
} else {
return (lruclock + (LRU_CLOCK_MAX - o->lru)) *
LRU_CLOCK_RESOLUTION;
}
}
空閑時(shí)間基本就是就是對(duì)象的lru和全局的LRU_CLOCK()的差值乘以精度LRU_CLOCK_RESOLUTION,將秒轉(zhuǎn)化為了毫秒。
參考鏈接
- Random notes on improving the Redis LRU algorithm
- Using Redis as an LRU cache
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,謝謝大家對(duì)腳本之家的支持。
您可能感興趣的文章:- 關(guān)于redis Key淘汰策略的實(shí)現(xiàn)方法
- 淺談redis的maxmemory設(shè)置以及淘汰策略
- 淺談Redis緩存有哪些淘汰策略