LRUキャッシュとしてのRedis

RedisはList機能など、いろいろ高機能っぽいのでちょっとリッチなLRUキャッシュとして使ってみようと考え始めて見た

memoryがしきい値を超えたときの説明は
http://siguniang.wordpress.com/2012/03/19/redis%E3%81%AEmaxmemory-policy/ 
に詳しく書いてあるのでそちらを参照

ここで候補に挙がるのは以下の2つ

  • volatile-lru
  • allkeys-lru

ちょっとソース追っかけながら見てみる。
メモリの解放処理とかをやってるのはこの辺
http://doublefree.org:8080/source/xref/redis-2.4.13/src/redis.c#1541 

1586            if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
1587                server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM)
1588            {
1589                dict = server.db[j].dict;
1590            } else {
1591                dict = server.db[j].expires;
1592            }
1593            if (dictSize(dict) == 0) continue

 expiresを追っかけてみると
http://doublefree.org:8080/source/xref/redis-2.4.13/src/redis.h#300 

300    dict *expires;              /* Timeout of keys with a timeout set */

http://doublefree.org:8080/source/xref/redis-2.4.13/src/db.c#448 

448void setExpire(redisDb *db, robj *key, time_t when) {
449    dictEntry *de;
450
451    /* Reuse the sds from the main dict in the expire dict */
452    de = dictFind(db->dict,key->ptr);
453    redisAssert(de != NULL);
454    dictReplace(db->expires,dictGetEntryKey(de),(void*)when);
455}
456

どうもexpire時間がセットされているkeyは通常のdictionary(dict)とは別のdictionary(expires)にもsetされるみたい。
ちなみに、replaceDictは

160static int dictReplace(dict *ht, void *key, void *val) {
161    dictEntry *entry, auxentry;
162
163    /* Try to add the element. If the key 164 * does not exists dictAdd will suceed. */
165    if (dictAdd(ht, key, val) == DICT_OK)
166        return 1;
167    /* It already exists, get the entry */
168    entry = dictFind(ht, key);
169    /* Free the old value and set the new one */
170    /* Set the new value and free the old one. Note that it is important 171 * to do that in this order, as the value may just be exactly the same 172 * as the previous one. In this context, think to reference counting, 173 * you want to increment (set), and then decrement (free), and not the 174 * reverse. */
175    auxentry = *entry;
176    dictSetHashVal(ht, entry, val);
177    dictFreeEntryVal(ht, &auxentry);
178    return 0;
179} 

keyがあれば上書きして、なければaddするだけ。dictからexpiresにkeyを移している訳じゃない

expireに関するredisのdocumentを参照してみると
http://redis.io/commands/expire
ふむ。expireというコマンドを秒数指定で既存のkeyに打つのか。 
実際にsetExpireが呼ばれているのも以下のコードから分かるようにexpireコマンドから
http://doublefree.org:8080/source/xref/redis-2.4.13/src/db.c#528 

528void expireGenericCommand(redisClient *c, robj *key, robj *param, long offset) {
529    dictEntry *de;
530    long seconds;
531
532    if (getLongFromObjectOrReply(c, param, &seconds, NULL) != REDIS_OK) return;
533
534    seconds -= offset;
535
536    de = dictFind(c->db->dict,key->ptr);
537    if (de == NULL) {
538        addReply(c,shared.czero);
539        return;
540    }
541    /* EXPIRE with negative TTL, or EXPIREAT with a timestamp into the past 542 * should never be executed as a DEL when load the AOF or in the context 543 * of a slave instance. 544 * 545 * Instead we take the other branch of the IF statement setting an expire 546 * (possibly in the past) and wait for an explicit DEL from the master. */
547    if (seconds <= 0 && !server.loading && !server.masterhost) {
548        robj *aux;
549
550        redisAssert(dbDelete(c->db,key));
551        server.dirty++;
552
553        /* Replicate/AOF this as an explicit DEL. */
554        aux = createStringObject("DEL",3);
555        rewriteClientCommandVector(c,2,aux,key);
556        decrRefCount(aux);
557        signalModifiedKey(c->db,key);
558        addReply(c, shared.cone);
559        return;
560    } else {
561        time_t when = time(NULL)+seconds;
562        setExpire(c->db,key,when);
563        addReply(c,shared.cone);
564        signalModifiedKey(c->db,key);
565        server.dirty++;
566        return;
567    }
568}
569
570void expireCommand(redisClient *c) {
571    expireGenericCommand(c,c->argv[1],c->argv[2],0);
572}
573
574void expireatCommand(redisClient *c) {
575    expireGenericCommand(c,c->argv[1],c->argv[2],time(NULL));
576} 

 なんか、volatile-lruが求めているものに近いなぁと思ったけど、これだとallkeys-lruの方が近い気がする。

でも、allkeys-lruだと、
http://doublefree.org:8080/source/xref/redis-2.4.13/src/redis.c#1604

1604            else if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
1605                server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)
1606            {
1607                for (k = 0; k < server.maxmemory_samples; k++) {
1608                    sds thiskey;
1609                    long thisval;
1610                    robj *o;
1611
1612                    de = dictGetRandomKey(dict);
1613                    thiskey = dictGetEntryKey(de);
1614                    /* When policy is volatile-lru we need an additonal lookup
1615                     * to locate the real key, as dict is set to db->expires. */
1616                    if (server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)
1617                        de = dictFind(db->dict, thiskey);
1618                    o = dictGetEntryVal(de);
1619                    thisval = estimateObjectIdleTime(o);
1620
1621                    /* Higher idle time is better candidate for deletion */
1622                    if (bestkey == NULL || thisval > bestval) {
1623                        bestkey = thiskey;
1624                        bestval = thisval;
1625                    }
1626                }
1627            }
1628

maxmemory_samplesの数(defaultで3)だけrandomに抽出してそのなかでLRUが一番古い(大きい)keyを消去対象として選出している

これだと運が良くないと、結構新しいkeyがどんどんevictされていっちゃうので、memcached的なLRUと結構違う。keyの管理方法の問題だけど、これだとなかなか悩ましい。maxmemory_samplesの値を調整するともうちょっとまともにならないか、試してみようと思う。