Redis(四):Key読み書きおよび期限切れポリシー


DB構造体Redisにはデフォルトで16のデータベースがあり、データを格納する前にSELECT INDEXでDB(デフォルトindexは0、DB構造体はserver.h/redisDb)を指定しなければならない.DBはキー値ペア情報を主に格納し維持する.Redisは現在、現在操作中のライブラリを取得するコマンドがないので、操作するたびにselectを選択するのが良いことに注意してください.
typedef struct redisDb {
    dict *dict;                 /* The keyspace for this DB */
    dict *expires;              /* Timeout of keys with a timeout set */
    dict *blocking_keys;        /* Keys with clients waiting for data (BLPOP) */
    dict *ready_keys;           /* Blocked keys that received a PUSH */
    dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */
    struct evictionPoolEntry *eviction_pool;    /* Eviction pool of keys */
    int id;                     /* Database ID */
    long long avg_ttl;          /* Average TTL, just for stats */
} redisDb;

dict辞書はキー空間とも呼ばれ、すべてのキー値ペアが格納され、値は5つのタイプの1つである.expiresは、有効期限が設定されたkeyとその有効期限のキー値のペアを格納する.Key読み書きユーザがKeyを読み出すと、dictにKeyが存在しない場合、miss回数+1がクライアント空に戻る.そうでなければhit回数+1とキーの最後の使用時間が更新され、この値によってkeyのアイドル時間が計算され、アイドル時間の長さは回収ポリシーにも影響し、キーが失効した場合はdictとexpiresのキー値のペアを同時にクリアし、SlaveにDELコマンドを送信します.info statsでhistやmissなどの情報が表示され、Object idletime keyでkeyのアイドル時間が表示されます.キーを書き込むと、dictにキーが既に存在する場合、対応する値の更新が行われ、そうでない場合、新しいキー値ペアが格納されます.有効期限が設定されているKeyへの書き込みはexpires辞書にも格納され、削除時にもexpiresの対応するキー値ペアがクリアされます.Key有効期限設定redisには、4つのコマンドexpire、pexpire、expireat、pexpireatがkeyをバインドできる有効期限があります(persistはバインドを解除できます).expireとpexpireはkeyが生存できる時間を設定し、expireatとpexpireatはkeyが失効する時間点を設定し、前者は秒単位、後者はミリ秒単位で、最初の3つのコマンドの下部はpexpireatを使用します.Key期限切れクリーンアップポリシー
  • タイミングクリーンアップ:k/vペアを設定すると、同時にタイマをオンにするか、失効時点をタイマに注入し、有効時間が到着した後にキー値ペアを削除します.この方式は一般的に、消費されたメモリをできるだけ早く解放することができるが、CPUの性能に対する消費は深刻であり、特に大量のデータや大量の連続要求の場合、こんなに多くのCPU資源が消費されず、実際には一般的ではない.
  • 不活性クリーンアップ:クライアントがkeyを要求したときに有効性が検出されます.この方法では、CPUリソースをタイミングクリーンアップのように大量に消費することはありませんが、メモリに対してメモリ漏洩が発生する可能性があります.クライアントが失効したキーを要求しないと、これらのキーはメモリに常駐し、解放されません(flushなどの操作を除きます).
  • 定期清掃:周期的な検出keyの有効性(必ずしも全量keyとは限らない)は、CPU資源を大量に消費することもメモリ漏れを招くこともないが、keyは基本的にタイムリーに清掃されず、実際の清掃のタイミングと周波数は容易に確定しない.

  • Redisは総合的に不活性クリーンアップと定期クリーンアップの戦略を採用しており、不活性クリーンアップについては上に説明したが、ここでは定期クリーンアップのソースコードを分析し、3.2.8バージョンではソースコードがserverにある.c/activeExpireCycle,この関数は周期的にserverCron()を呼び出す.DatabasesCron()は間接的に呼び出され、周波数はデフォルトで#define CONFIG_DEFAULT_HZ   10    /* Time interrupt calls/sec. */毎秒10回、ソースコードは以下の通りです.
    /*
     *type=ACTIVE_EXPIRE_CYCLE_FAST     ; type=ACTIVE_EXPIRE_CYCLE_SLOW     ;
     */
    void activeExpireCycle(int type) {
        
        static unsigned int current_db = 0; /*      DB */
        static int timelimit_exit = 0;      /*          */
        static long long last_fast_cycle = 0; /*       . */
    
        int j, iteration = 0;
        int dbs_per_call = CRON_DBS_PER_CALL; /*     DB  :16*/
        long long start = ustime(), timelimit;
    
        if (clientsArePaused()) return;
    
        if (type == ACTIVE_EXPIRE_CYCLE_FAST) {
            /*          ,      */
            if (!timelimit_exit) return;
            if (start < last_fast_cycle + ACTIVE_EXPIRE_CYCLE_FAST_DURATION*2) return;
            last_fast_cycle = start;
        }
    
        /*   DB   */
        if (dbs_per_call > server.dbnum || timelimit_exit)
            dbs_per_call = server.dbnum;
    
        /*         : 1000000 * 25/10/100 = 25000 == 25   */    
        timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100;
        timelimit_exit = 0;
        if (timelimit <= 0) timelimit = 1;
    
        /*        :1   */
        if (type == ACTIVE_EXPIRE_CYCLE_FAST)
            timelimit = ACTIVE_EXPIRE_CYCLE_FAST_DURATION; /* in microseconds. */
    
        /*   DB */
        for (j = 0; j < dbs_per_call; j++) {
            int expired;
            redisDb *db = server.db+(current_db % server.dbnum);
    
            /* DB   1,         DB */
            current_db++;
    
            /*    DB       20 key
             * 1:    key ttl,    ttl
             * 2:     key,     key   ,    key      1/4(  5 ),     DB      ,    DB   。
             * 3:   16 ,           ,               。         
             */
            do {
                unsigned long num, slots;
                long long now, ttl_sum;
                int ttl_samples;
               
                if ((num = dictSize(db->expires)) == 0) {
                    db->avg_ttl = 0;
                    break;
                }
                slots = dictSlots(db->expires);
                now = mstime();
              
                if (num && slots > DICT_HT_INITIAL_SIZE &&
                    (num*100/slots < 1)) break;
            
                expired = 0;
                ttl_sum = 0;
                ttl_samples = 0;
    
                /*       20 key */
                if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)
                    num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;           
                while (num--) {
                    dictEntry *de;
                    long long ttl;
    
                    if ((de = dictGetRandomKey(db->expires)) == NULL) break;
                    ttl = dictGetSignedIntegerVal(de)-now;
                    if (activeExpireCycleTryExpire(db,de,now)) expired++;/*        */
                    if (ttl > 0) {                   
                        ttl_sum += ttl;
                        ttl_samples++;
                    }
                }
              
                if (ttl_samples) {
                    long long avg_ttl = ttl_sum/ttl_samples;
                    if (db->avg_ttl == 0) db->avg_ttl = avg_ttl;
                    db->avg_ttl = (db->avg_ttl/50)*49 + (avg_ttl/50);
                }
    
                /*    16          */
                iteration++;
                if ((iteration & 0xf) == 0) { /* check once every 16 iterations. */
                    long long elapsed = ustime()-start;
    
                    latencyAddSampleIfNeeded("expire-cycle",elapsed/1000);
                    if (elapsed > timelimit) timelimit_exit = 1;
                }
                /*       ,      . */
                if (timelimit_exit) return;
                /*     >1/4   ,       DB   . */
            } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
        }
    }

    基本的にこの関数の動作パターンは,1)一定数のDBを反復し,最大数16と要約できる.2)各DBから最大20個のkey*1をランダムに抽出する:各keyのttlを取得し、平均ttl*2を統計的に求める:失効したkeyをクリーンアップし、失効したkeyの個数を記録し、失効したkeyの個数が1/4(5個未満)を超えない場合、現在のDBはクリーンアップを必要とせず、次のDBの処理を実行する.*3:長時間のブロックを防止するため、16回の実行ごとに実行時間が経過したかどうかを検出し、実行時間が上限に達した場合、今回のクリーンアップを終了し、次回の周期的な呼び出しを待つ.ただし、DB数が16個未満の場合、このステップは実行されません.この場合、実行時間が長くても、最後のクリーンアップが完了するまで終了しません.RDBとAOFの期限切れKeyに対する処理RDBとAOFの書き換えは本質的に現在のメモリに格納されているデータであるが、RDBはデータファイルとして格納されているが、書き換えAOFはコマンド形式で格納されているが、メモリにすでに失効しているKeyについては両方とも格納されず、RDBにとって利用可能なデータのみを保存する必要がある.ロード後に無効なデータをクリーンアップする必要はなく、AOFにとって最小命令セットを保存するだけで、書き込みと削除の2つの命令を保存する必要もありません.しかし、通常のAOFは失効Keyに対して、Delコマンドをaofファイルに追加します.プライマリスレーブノードの期限切れKeyに対する処理は、スレーブノードのデータがプライマリノードに由来するため、マスターノードとの状態を維持するために、Masterが削除命令を送信するまで、失効したkeyをクリーンアップするためにポリシーは使用されませんが、失効したKeyに対するすべての要求は空に戻ります.空に戻るのは、キーが失効したがプライマリノードdelコマンドが受信または失われていない場合、プライマリノードと一致する応答が必要であるためです.まとめ:
     
  • Redisデータベースは主にdictとexpiresの2つの辞書から構成され、dict辞書はキー値のペアを保存し、キーは文字列であり、値は5つのタイプの1つである.expires辞書は、キーと有効期限のペアを保存します.したがって、キーの操作は、辞書操作の上に確立されている
  • である.
  • Redisは、不活性な削除と定期的な削除の2つのポリシーを使用して、期限切れのキー
  • を削除する.
  • 失効キーが削除されると、既存のAOFファイルにDELが追加されますが、RDBまたはAOF書き換えには失効キー
  • は含まれません.
  • プライマリノードがキーを削除すると、すべてのスレーブノードにDELコマンドが送信されます.スレーブノードの場合、プライマリノードとの一貫性を維持するために、DELコマンドを受信する前にポリシーを使用して削除することはありませんが、失効keyのすべてのリクエストは空に戻ります.