Redisソース(8)-Redisのデータベース

20015 ワード

前のブログでは、Redisの下位データ構造とRedisのいくつかのオブジェクトを比較的詳細に分析しましたが、Redis-高速分散データベースとして、アクセスキーの設定、期限切れの設定、プライマリ・スレーブ・レプリケーションなど、Redisの典型的な機能については説明していません.次にRedisのいくつかの機能について詳細な分析を行う.
1つのデータベースとして、Redisのデータベース機能は最も基本的で最も核心的な機能であり、次にこれについて紹介する.
一、データベースの構造定義
redisでhファイルのredisDbは、Redisデータベース構造を定義します.
/* Redis database representation. There aremultiple databases identified
 * byintegers from 0 (the default database) up to the max configured
 * database.The database number is the 'id' field in the structure. */
typedef struct redisDb {
 
    //       ,             
    dict*dict;                 /* The keyspacefor this DB */
 
    //       ,      ,          UNIX    
    dict*expires;              /* Timeout of keyswith a timeout set */
 
    //          
    dict*blocking_keys;        /* Keys withclients waiting for data (BLPOP) */
 
    //         
    dict*ready_keys;           /* Blocked keysthat received a PUSH */
 
    //     WATCH       
    dict*watched_keys;         /* WATCHED keysfor MULTI/EXEC CAS */
 
    structevictionPoolEntry *eviction_pool;    /*Eviction pool of keys */
 
    //      
    intid;                     /* Database ID */
 
    //          TTL ,    
    longlong avg_ttl;          /* Average TTL,just for stats */
 
} redisDb;

データベース内のすべてのキー値ペアがdict辞書構造で保存され、この辞書はキー空間と呼ばれます.このほかredisDb構造にはキーの有効期限の辞書やデータベースのIDなどの属性が格納されている.
 
二、データベース関連操作
 db.cファイルでは、データベースに関する操作が実現されています.
1). キー値のペアを追加するには、次の手順に従います.
/* Add the key to the DB. It's up to the callerto increment the reference
 * counterof the value if needed.
 *
 *        key   val        。
 *
 *        key   val          。
 *
 * Theprogram is aborted if the key already exists.
 *
 *             。
 */
void dbAdd(redisDb *db, robj *key, robj *val) {
 
    //     
    sdscopy = sdsdup(key->ptr);
 
    //        
    intretval = dictAdd(db->dict, copy, val);
 
    //        ,    
   redisAssertWithInfo(NULL,key,retval == REDIS_OK);
 
    //          ,          
    if(server.cluster_enabled) slotToKeyAdd(key);
 }

2). キー値のペアを削除するには、次の手順に従います.
/* Delete a key, value, and associated expirationentry if any, from the DB
 *
 *            ,   ,        。
 *
 *        1 ,              ,   0 。
 */
int dbDelete(redisDb *db, robj *key) {
 
    /*Deleting an entry from the expires dict will not free the sds of
     * thekey, because it is shared with the main dictionary. */
    //         
    if(dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr);
 
    //      
    if(dictDelete(db->dict,key->ptr) == DICT_OK) {
        //          ,           
        if(server.cluster_enabled) slotToKeyDel(key);
       return 1;
    } else{
        //     
       return 0;
    }
}

3). 検索キーに対応する値:
/*
 *      db      key   (  )
 *
 *    key     ,      ;  ,   NULL 。
 */
robj *lookupKey(redisDb *db, robj *key) {
 
    //      
   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
         * acopy on write madness. */
        //       (           ,     copy-on-write   )
        if(server.rdb_child_pid == -1 && server.aof_child_pid == -1)
           val->lru = LRU_CLOCK();
 
        //    
       return val;
    } else{
 
        //      
 
       return NULL;
    }
}

4). 更新キーに対応する値:
/* Overwrite an existing key with a new value.Incrementing the reference
 * count ofthe new value is up to the caller.
 *
 *             。
 *
 *          val          。
 *
 * Thisfunction does not modify the expire time of the existing key.
 *
 *               。
 *
 * Theprogram is aborted if the key was not already present.
 *
 *       ,      。
 */
void dbOverwrite(redisDb *db, robj *key, robj*val) {
   dictEntry *de = dictFind(db->dict,key->ptr);
   
    //       ,    
   redisAssertWithInfo(NULL,key,de != NULL);
 
    //     
   dictReplace(db->dict, key->ptr, val);
}

5). キーの有効期限を設定するには、Redisには、キーの有効期限を設定できる4つの異なるコマンドがあります.EXPIRE(生存時間は秒単位)、EXPIREAT(生存時間はミリ秒単位)、PEXPIRE(生存時間は秒のタイムスタンプ)、PEXPIREAT(生存時間はミリ秒のタイムスタンプ)ですが、この4つのコマンドはいずれもPEXPIREATを使用して実現されます.
void expireCommand(redisClient *c) {
   expireGenericCommand(c,mstime(),UNIT_SECONDS);
}
 
void expireatCommand(redisClient *c) {
   expireGenericCommand(c,0,UNIT_SECONDS);
}
 
void pexpireCommand(redisClient *c) {
   expireGenericCommand(c,mstime(),UNIT_MILLISECONDS);
}
 
void pexpireatCommand(redisClient *c) {
   expireGenericCommand(c,0,UNIT_MILLISECONDS);
}

expireGenericCommand関数は次のとおりです.
/* This is the generic command implementation forEXPIRE, PEXPIRE, EXPIREAT
 * andPEXPIREAT. Because the commad second argument may be relative or absolute
 * the"basetime" argument is used to signal what the base time is (either 0
 * for *ATvariants of the command, or the current time for relative expires).
 *
 *       EXPIRE 、PEXPIRE 、 EXPIREAT  PEXPIREAT          。
 *
 *               ,       。
 *     *AT    , basetime   0 ,      ,             。
 *
 * unit iseither UNIT_SECONDS or UNIT_MILLISECONDS, and is only used for
 * theargv[2] parameter. The basetime is always specified in milliseconds.
 *
 * unit      argv[2] (      )   ,
 *      UNIT_SECONDS  UNIT_MILLISECONDS ,
 * basetime          。
 */
void expireGenericCommand(redisClient *c, longlong basetime, int unit) {
    robj*key = c->argv[1], *param = c->argv[2];
    longlong when; /* unix time in milliseconds when the key will expire. */
 
    //    when   
    if(getLongLongFromObjectOrReply(c, param, &when, NULL) != REDIS_OK)
       return;
 
    //                 ,         
    if(unit == UNIT_SECONDS) when *= 1000;
    when +=basetime;
 
    /* Nokey, return zero. */
    //    
    if(lookupKeyRead(c->db,key) == NULL) {
       addReply(c,shared.czero);
       return;
    }
 
   /* EXPIREwith negative TTL, or EXPIREAT with a timestamp into the past
     *should never be executed as a DEL when load the AOF or in the context
     * of a slave instance.
     *
     *       ,           ,
     *    EXPIRE   TTL    ,   EXPIREAT           ,
     *              ,             DEL   。
     *
     *Instead we take the other branch of the IF statement setting an expire
     *(possibly in the past) and wait for an explicit DEL from the master.
     *
     *       (          TTL)         ,
     *           DEL   。
     */
    if(when <= mstime() && !server.loading && !server.masterhost){
 
        //when          ,       ,        
 
       robj *aux;
 
       redisAssertWithInfo(c,key,dbDelete(c->db,key));
       server.dirty++;
 
        /*Replicate/AOF this as an explicit DEL. */
        //    DEL   
        aux= createStringObject("DEL",3);
 
       rewriteClientCommandVector(c,2,aux,key);
       decrRefCount(aux);
 
       signalModifiedKey(c->db,key);
       notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"del",key,c->db->id);
 
       addReply(c, shared.cone);
 
       return;
    } else{
 
        //         
        //           ,         ,
        //      when         
       setExpire(c->db,key,when);
 
       addReply(c,shared.cone);
 
       signalModifiedKey(c->db,key);
       notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"expire",key,c->db->id);
 
       server.dirty++;
 
       return;
    }
}

最も簡単なキー値対の操作を簡単に示すが、詳細はdbを参照してください.cファイル.Redisは、データベースを操作する際に、キー空間に対して命令で指定された読み書き操作を実行するほか、追加の操作も行います.
1). キーを読み込むと、キーが存在するかどうかに基づいて、サーバ内のキーヒット(hit)およびミス(miss)属性が更新されます.
robj *lookupKeyRead(redisDb *db, robj *key) {
    robj*val;
 
    //    key       
   expireIfNeeded(db,key);
 
    //           
    val =lookupKey(db,key);
 
    //     /     
    if (val== NULL)
       server.stat_keyspace_misses++;
    else
       server.stat_keyspace_hits++;
 
    //    
    returnval;
}

2). キーの値を読み込むと、サーバーはキーの最後の使用時間(LRU)を更新します.この時間はキーの空き時間を計算するために使用されます.これは、メモリ不足のためにキャッシュを消去する際に使用される可能性があります(たとえば、最近の最小使用アルゴリズムを使用してキャッシュを淘汰するなど).
//       (           ,     copy-on-write   )
        if(server.rdb_child_pid == -1 && server.aof_child_pid == -1)
           val->lru = LRU_CLOCK();

3). サーバがキーを操作すると、dirtyカウンタの値が更新されます.この値は、サーバの永続化時に使用されます.
//              
void persistCommand(redisClient *c) {
   dictEntry *de;
 
    //    
    de =dictFind(c->db->dict,c->argv[1]->ptr);
 
    if (de== NULL) {
        //        
       addReply(c,shared.czero);
 
    } else{
 
        //        ,      
        if(removeExpire(c->db,c->argv[1])) {
           addReply(c,shared.cone);
           server.dirty++;
 
        //         
        }else {
           addReply(c,shared.czero);
        }
    }
}

4). サーバが通知を開くと、キーを変更すると、上のコードのaddReply関数のような通知が発行されます.
 
三、期限切れのキー値削除ポリシー
Redisの期限切れキーのいくつかの操作について説明しましたが、期限切れキーはいつ削除されますか?普通の人の論理では、一般的に次の2~3つの削除ポリシーが考えられます.
1.最も簡単なのは、タイミング削除です.キーの生存時間を得ることができるので、このキーにタイマーを定義します.時間になるとイベントがトリガーされ、このキーを削除します.何千もの期限切れキーがあれば?何千ものタイマーをつけて処理する必要があるのだろうか.それは間違いなくほとんどのCPUをこれらの期限切れキーを監視するのに費やしたに違いない.このスキームの性能は明らかに悪い.
2.定期的に削除:このシナリオは最初のシナリオの改善です.各キーにタイマを作成できない以上、サーバに一定時間おきにデータベースをスキャンさせ、期限切れのキーをクリアさせます.しかし、タイミング削除の頻度をどのようにバランスさせるかは難点です.
3.遅延削除:不活性削除とも呼ばれ、これはメモリで時間を変える方案である.キーが期限切れになったサーバーは自発的に彼を削除しない.クライアントがこの期限切れのキーを操作するまで、サーバーはクライアントに「このキーがない」(期限切れになったため)と伝え、この無効なキーを削除する.何千もの無効なキーが要求されていない場合、これらの無効なキーがメモリに蓄積され、メモリに不利になることは間違いありません.
では、高性能のキャッシュ・データベースとして、Redisはどのようにしていますか.--Redisは定期削除と遅延削除の2つのスキームを組み合わせて,時間と空間の取捨選択をバランスさせた.
遅延削除はexpireIfNeeded関数によって実現され、キーを操作する前にexpireIfNeededが呼び出され、このキーが無効かどうかを確認します(上のlookupKeyRead関数など):
/*
 *    key       ,     ,         。
 *
 *    0          ,      。
 *
 *    1               。
 */
int expireIfNeeded(redisDb *db, robj *key) {
 
    //         
   mstime_t when = getExpire(db,key);
   mstime_t now;
 
    //       
    if(when < 0) return 0; /* No expire for this key */
 
    /*Don't expire anything while loading. It will be done later. */
    //            ,           
    if(server.loading) return 0;
 
    /* Ifwe are in the context of a Lua script, we claim that time is
     *blocked to when the Lua script started. This way a key can expire
     * onlythe first time it is accessed and not in the middle of the
     *script execution, making propagation to slaves / AOF consistent.
     * Seeissue #1525 on Github for more information. */
    now =server.lua_caller ? server.lua_time_start : mstime();
 
    /* Ifwe are running in the context of a slave, return ASAP:
     * theslave key expiration is controlled by the master that will
     * sendus synthesized DEL operations for expired keys.
     *
     *Still we try to return the right information to the caller,
     * thatis, 0 if we think the key should be still valid, 1 if
     * wethink the key is expired at this time. */
    //         replication    
    //            key
    //                
    //                        
    //          
    if(server.masterhost != NULL) return now > when;
 
    //      ,         ,         
 
    /*Return when this key has not expired */
    //      ,   0
    if (now<= when) return 0;
 
    /*Delete the key */
   server.stat_expiredkeys++;
 
    //   AOF              
   propagateExpire(db,key);
 
    //       
   notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED,
       "expired",key,db->id);
 
    //            
    returndbDelete(db,key);
}

定期的に削除するポリシーはredisです.cファイルのactiveExpireCycle関数で実現する、Redisは定期的に(デフォルトでは100 msに1回)redisを実行する.c/serverCron関数はdatabasesCron関数を実行し、databasesCron関数にはactiveExpireCycle関数の実行が含まれます.
/* Try to expire a few timed out keys. Thealgorithm used is adaptive and
 * will usefew CPU cycles if there are few expiring keys, otherwise
 * it willget more aggressive to avoid that too much memory is used by
 * keysthat can be removed from the keyspace.
 *
 *                 。
 *              ,         ,
 *              ,                  ,
 *                 。
 *
 * No morethan REDIS_DBCRON_DBS_PER_CALL databases are tested at every
 *iteration.
 *
 *                    REDIS_DBCRON_DBS_PER_CALL 。
 *
 * Thiskind of call is used when Redis detects that timelimit_exit is
 * true, sothere is more work to do, and we do it more incrementally from
 * thebeforeSleep() function of the event loop.
 *
 *    timelimit_exit   ,              ,
 *     beforeSleep()      ,           。
 *
 * Expirecycle type:
 *
 *        :
 *
 * If typeis ACTIVE_EXPIRE_CYCLE_FAST the function will try to run a
 *"fast" expire cycle that takes no longer thanEXPIRE_FAST_CYCLE_DURATION
 *microseconds, and is not repeated again before the same amount of time.
 *
 *          ACTIVE_EXPIRE_CYCLE_FAST ,
 *       “    ”    ,
 *           EXPIRE_FAST_CYCLE_DURATION   ,
 *     EXPIRE_FAST_CYCLE_DURATION            。
 *
 * If typeis ACTIVE_EXPIRE_CYCLE_SLOW, that normal expire cycle is
 *executed, where the time limit is a percentage of the REDIS_HZ period
 * asspecified by the REDIS_EXPIRELOOKUPS_TIME_PERC define.
 *
 *          ACTIVE_EXPIRE_CYCLE_SLOW ,
 *       “    ”    ,
 *          REDIS_HS         ,
 *        REDIS_EXPIRELOOKUPS_TIME_PERC   。
 */
 
void activeExpireCycle(int type) {
    /* Thisfunction has some global state in order to continue the work
     *incrementally across calls. */
    //     ,              
    staticunsigned int current_db = 0; /* Last DB tested. */
    staticint timelimit_exit = 0;      /* Timelimit hit in previous call? */
    staticlong long last_fast_cycle = 0; /* When last fast cycle ran. */
 
   unsigned int j, iteration = 0;
    //             
   unsigned int dbs_per_call = REDIS_DBCRON_DBS_PER_CALL;
    //        
    longlong start = ustime(), timelimit;
 
    //     
    if(type == ACTIVE_EXPIRE_CYCLE_FAST) {
        /*Don't start a fast cycle if the previous cycle did not exited
         *for time limt. Also don't repeat a fast cycle for the same period
         *as the fast cycle total duration itself. */
        //            timelimit_exit ,       
        if(!timelimit_exit) return;
        //               ,       
        if(start < last_fast_cycle + ACTIVE_EXPIRE_CYCLE_FAST_DURATION*2) return;
        //      ,        ,      
       last_fast_cycle = start;
    }
 
    /* Weusually should test REDIS_DBCRON_DBS_PER_CALL per iteration, with
     * twoexceptions:
     *
     *      ,      REDIS_DBCRON_DBS_PER_CALL     ,
     *   :
     *
     * 1)Don't test more DBs than we have.
     *               REDIS_DBCRON_DBS_PER_CALL
     * 2)If last time we hit the time limit, we want to scan all DBs
     * inthis iteration, as there is work to do in some DB and we don't want
     *expired keys to use memory for too much time.
     *                  ,                ,
     *                    
     */
    if(dbs_per_call > server.dbnum || timelimit_exit)
       dbs_per_call = server.dbnum;
 
    /* Wecan use at max ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC percentage of CPU time
     * periteration. Since this function gets called with a frequency of
     *server.hz times per second, the following is the max amount of
     *microseconds we can spend in this function. */
    //            
    //ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC     25 ,    25 %   CPU   
   timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100;
   timelimit_exit = 0;
    if(timelimit <= 0) timelimit = 1;
 
    //             
    //          FAST_DURATION   
    //      1000 (  )
    if(type == ACTIVE_EXPIRE_CYCLE_FAST)
       timelimit = ACTIVE_EXPIRE_CYCLE_FAST_DURATION; /* in microseconds. */
 
    //      
    for (j= 0; j < dbs_per_call; j++) {
        intexpired;
        //          
       redisDb *db = server.db+(current_db % server.dbnum);
 
        /*Increment the DB now so we are sure if we run out of time
         *in the current DB we'll restart from the next. This allows to
         *distribute the time evenly across DBs. */
        //   DB      ,     do            
        //            DB     
       current_db++;
 
        /*Continue to expire if at the end of the cycle more than 25%
         *of the keys were expired. */
        do{
           unsigned long num, slots;
           long long now, ttl_sum;
           int ttl_samples;
 
           /* If there is nothing to expire try next DB ASAP. */
           //                 
           //        0 ,         
           if ((num = dictSize(db->expires)) == 0) {
                db->avg_ttl = 0;
               break;
           }
           //             
           slots = dictSlots(db->expires);
           //     
           now = mstime();
 
           /* When there are less than 1% filled slots getting random
            * keys is expensive, so stop here waiting for better times...
            * The dictionary will be resized asap. */
           //             1% ,        (      MISS)
            //   ,          
           if (num && slots > DICT_HT_INITIAL_SIZE &&
               (num*100/slots < 1)) break;
 
           /* The main collection cycle. Sample random keys among keys
            * with an expire set, checking for expired ones.
            *
            *      
            */
           //          
           expired = 0;
           //     TTL    
           ttl_sum = 0;
           //          
           ttl_samples = 0;
 
           //          LOOKUPS_PER_LOOP   
           if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)
               num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;
 
           //        
           while (num--) {
               dictEntry *de;
               long long ttl;
 
               //   expires               
               if ((de = dictGetRandomKey(db->expires)) == NULL) break;
               //    TTL
               ttl = dictGetSignedIntegerVal(de)-now;
               //        ,     ,   expired      
                if(activeExpireCycleTryExpire(db,de,now)) expired++;
               if (ttl < 0) ttl = 0;
               //      TTL
               ttl_sum += ttl;
               //         
               ttl_samples++;
           }
 
           /* Update the average TTL stats for this database. */
           //            TTL     
           if (ttl_samples) {
               //        
               long long avg_ttl = ttl_sum/ttl_samples;
               
               //                TTL ,       
               if (db->avg_ttl == 0) db->avg_ttl = avg_ttl;
               /* Smooth the value averaging with the previous one. */
               //           TTL       TTL     
               db->avg_ttl = (db->avg_ttl+avg_ttl)/2;
           }
 
            /* We can't block forever here even ifthere are many keys to
            * expire. So after a given amount of milliseconds return to the
            * caller waiting for the other active expire cycle. */
           //               ,
           //                   
 
           //       
           iteration++;
 
           //     16      
           if ((iteration & 0xf) == 0 && /* check once every 16iterations. */
               (ustime()-start) > timelimit)
           {
                //           16    
               //            timelimit
               //      timelimit_exit
               timelimit_exit = 1;
           }
 
           //      ,  
           if (timelimit_exit) return;
 
           /* We don't repeat the cycle if there are less than 25% of keys
            * found expired in the current DB. */
           //                            25 %
           //       
        }while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
    }
}