redisソースの概要-8-データベースの実装

12175 ワード

環境説明:redisソースバージョン5.0.3;ソースコードを読む過程でコメントをしましたgitアドレス:https://gitee.com/xiaoangg/redis_annotation参考書:『redisの設計と実現』

目次
 
サーバ内のデータベース
二切替データ
さんじげんライブラリキーくうかん
4キーの有効期限
5期限切れキー削除ポリシー
六削除ポリシーの実装

サーバ内のデータベース


redisサーバはすべてのデータベースをserverに保存します.h/redisServer構造のdb配列;配列の各項目はserverです.h/redisDb構造で、各構造はデータベースを表す.
struct redisServer {
    /* General */
   ....
    redisDb *db; //     
   ....
    int dbnum;                      /* Total number of configured DBs */
}

初期化データの数はdbnumによって決定される.デフォルトは16です.

二切替データ


各redisクライアントには独自のターゲットデータベース、クライアントステータスserverがある.h/client構造のdb属性記録は、クライアントの現在のターゲットデータベースに記録される.
/* With multiplexing we need to take per-client state.
 * Clients are taken in a linked list. */
typedef struct client {
    uint64_t id;            /* Client incremental unique ID. */
    int fd;                 /* Client socket. */
    redisDb *db;            /* Pointer to currently SELECTed DB. */
    .....
}

クライアントがselectコマンドを使用してターゲット・データベースを変更すると、クライアントのdbポインタは2番のデータベースを指定し、ターゲット・データベースを切り替える機能を実現します.

さんじげんライブラリキーくうかん


サーバ内の各データベースはserverである.h/redisDb構造は、dict辞書がデータベース内のすべてのキー値ペアに保存されることを表す.
/* Redis database representation. There are multiple databases identified
 * by integers 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 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 */
    int id;                     /* Database ID */
    long long avg_ttl;          /* Average TTL, just for stats */
    list *defrag_later;         /* List of key names to attempt to defrag one by one, gradually. */
} redisDb;
  • 辞書のキーはデータベースのキーであり、各キーは文字列オブジェクト
  • である.
  • 辞書の値はデータベースの値であり、値は文字列オブジェクト、リストオブジェクト、ハッチングテーブル、セット、整列セットのいずれかのredisオブジェクト
  • であることができる.
    データベースのキー空間は辞書であるため、データベースに対するすべての操作は、辞書の操作に基づいて実現される.
    1.新しいキーを追加する追加キーのエントリはdbにある.c/setKeyは、データベース内のすべてのkeyがこの方法で作成されます. 
    /*
           key            
    */
    /* High level Set operation. This function can be used in order to set
     * a key, whatever it was existing or not, to a new object.
     *
     * 1) The ref count of the value object is incremented.
     * 2) clients WATCHing for the destination key notified.
     * 3) The expire time of the key is reset (the key is made persistent).
     *
     * All the new keys in the database should be created via this interface. */
    void setKey(redisDb *db, robj *key, robj *val) {
        if (lookupKeyWrite(db,key) == NULL) { 
            dbAdd(db,key,val);
        } else {
            dbOverwrite(db,key,val);
        }
        incrRefCount(val);//    +1
        removeExpire(db,key);
        signalModifiedKey(db,key);
    }
    
    /*
     key   db ,                  
    */
    /* Add the key to the DB. It's up to the caller to increment the reference
     * counter of the value if needed.
     *
     * The program is aborted if the key already exists. */
    void dbAdd(redisDb *db, robj *key, robj *val) {
        sds copy = sdsdup(key->ptr);
        int retval = dictAdd(db->dict, copy, val);
    
        serverAssertWithInfo(NULL,key,retval == DICT_OK);
        if (val->type == OBJ_LIST ||
            val->type == OBJ_ZSET)
            signalKeyAsReady(db, key);
        if (server.cluster_enabled) slotToKeyAdd(key);
    }

    データベースにキーを追加し、最終的には辞書のdictAddメソッドを呼び出して実現されることがわかります.
    2.削除キー
    キータイムで呼び出されたディクショナリdict.c/dictGenericDeleteインタフェースを削除します.キー(次のコードのdictFreeKey(d,he)メソッドとキーに対応する値オブジェクト(次のコードのdictFreeValメソッド):
    
    /* Search and remove an element. This is an helper function for
     * dictDelete() and dictUnlink(), please check the top comment
     * of those functions. */
    static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) {
        uint64_t h, idx;
        dictEntry *he, *prevHe;
        int table;
    
        //hash    
        if (d->ht[0].used == 0 && d->ht[1].used == 0) return NULL;
    
        if (dictIsRehashing(d)) _dictRehashStep(d);
        h = dictHashKey(d, key);
    
        for (table = 0; table <= 1; table++) {
            idx = h & d->ht[table].sizemask;
            he = d->ht[table].table[idx];
            prevHe = NULL;
            while(he) {
                if (key==he->key || dictCompareKeys(d, key, he->key)) {
                    /* Unlink the element from the list */
                    if (prevHe)
                        prevHe->next = he->next;
                    else
                        d->ht[table].table[idx] = he->next;
                    if (!nofree) {
                        dictFreeKey(d, he);
                        dictFreeVal(d, he);
                        zfree(he);
                    }
                    d->ht[table].used--;
                    return he;
                }
                prevHe = he;
                he = he->next;
            }
            if (!dictIsRehashing(d)) break;
        }
        return NULL; /* not found */
    }

    3更新と検索操作
    更新と検索操作は、値オブジェクトのタイプに応じて、対応する操作を行います.getコマンドを例に挙げます.
    /*
                
    */
    int getGenericCommand(client *c) {
        robj *o;
    
        //db.c/lookupKeyReadOrReply   key       
        if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk)) == NULL)
            return C_OK;
    
        //         string
        if (o->type != OBJ_STRING) {
            addReply(c,shared.wrongtypeerr);
            return C_ERR;
        } else {
            addReplyBulk(c,o);
            return C_OK;
        }
    }
    
    void getCommand(client *c) {
        getGenericCommand(c);
    }

    4その他の操作
    追加削除の変更に加えて、fulshdbはキー空間のすべてのキー値ペアを削除することです.randdomkeyはキー空間でランダムにキーを返す.dbsize、exist、rename、keysなどはキー空間を操作することによって実現される.

    4キーの有効期限


    EXPIREまたはPEXPIREコマンドにより、あるkeyの生存時間を設定でき、指定した秒数またはミリ秒数を経過すると、サーバは生存時間が0のキーを削除します.
    1.有効期限の設定
    redisには4つのコマンドでキーの生存時間EXPIRE、PEXPIR、EXPIREAT、PEXPIREATを設定できます.以下に、この4つのコマンドはすべてexipreであることがわかります.c/expireGenericCommand関数によって実現される.
    /* EXPIRE key seconds */
    void expireCommand(client *c) {
        expireGenericCommand(c,mstime(),UNIT_SECONDS);
    }
    
    /* EXPIREAT key time */
    void expireatCommand(client *c) {
        expireGenericCommand(c,0,UNIT_SECONDS);
    }
    
    /* PEXPIRE key milliseconds */
    void pexpireCommand(client *c) {
        expireGenericCommand(c,mstime(),UNIT_MILLISECONDS);
    }
    
    /* PEXPIREAT key ms_time */
    void pexpireatCommand(client *c) {
        expireGenericCommand(c,0,UNIT_MILLISECONDS);
    }

    2.保存期限
    redisDbのexpires辞書は、データベース内のすべてのキーの有効期限を保存します.
  • 期限切れ辞書のkeyは、キー空間のあるキーオブジェクト
  • を指す.
  • 期限切れ辞書の値long longタイプの整数(ミリ秒精度のタイムスタンプ)
  • db.c/setExipre
    
    /* Set an expire to the specified key. If the expire is set in the context
     * of an user calling a command 'c' is the client, otherwise 'c' is set
     * to NULL. The 'when' parameter is the absolute unix time in milliseconds
     * after which the key will no longer be considered valid. */
    void setExpire(client *c, redisDb *db, robj *key, long long when) {
        dictEntry *kde, *de;
    
        /* Reuse the sds from the main dict in the expire dict */
        kde = dictFind(db->dict,key->ptr); //         key
        serverAssertWithInfo(NULL,key,kde != NULL);
        
        // db expire   key      key(  key             )
        de = dictAddOrFind(db->expires,dictGetKey(kde));
    
        dictSetSignedIntegerVal(de,when); //       
    
        int writable_slave = server.masterhost && server.repl_slave_ro == 0;
        if (c && writable_slave && !(c->flags & CLIENT_MASTER))
            rememberSlaveKeyWithExpire(db,key);
    }

    3.有効期限の削除
    期限切れ時間の削除は比較的簡単で、辞書インタフェースdictDeleteを呼び出してkeyを期限切れ辞書から削除するだけでよい.db.c/removeExpire
    int removeExpire(redisDb *db, robj *key) {
        /* An expire may only be removed if there is a corresponding entry in the
         * main dict. Otherwise, the key will never be freed. */
        serverAssertWithInfo(NULL,key,dictFind(db->dict,key->ptr) != NULL);
        return dictDelete(db->expires,key->ptr) == DICT_OK;
    }

    4.取得期限
    期限切れの取得もよく理解でき、期限切れの辞書で対応するkeyを検索する必要があります.TTL、PTTLコマンドの実装ソースコードを直接貼り付ける.
    //expire.c/ttlGenericCommand
    /* Implements TTL and PTTL */
    void ttlGenericCommand(client *c, int output_ms) {
        long long expire, ttl = -1;
    
        //  key    ,     -2
        /* If the key does not exist at all, return -2 */
        if (lookupKeyReadWithFlags(c->db,c->argv[1],LOOKUP_NOTOUCH) == NULL) {
            addReplyLongLong(c,-2);
            return;
        }
        
        //key          ,  -1
        /* The key exists. Return -1 if it has no expire, or the actual
         * TTL value otherwise. */
        expire = getExpire(c->db,c->argv[1]);
        if (expire != -1) {
            ttl = expire-mstime();
            if (ttl < 0) ttl = 0;
        }
        if (ttl == -1) {
            addReplyLongLong(c,-1);
        } else {
            addReplyLongLong(c,output_ms ? ttl : ((ttl+500)/1000));
        }
    }
    
    
    //db.c/getExpire
    
    /* Return the expire time of the specified key, or -1 if no expire
     * is associated with this key (i.e. the key is non volatile) */
    long long getExpire(redisDb *db, robj *key) {
        dictEntry *de;
    
        /* No expire? return ASAP */
        if (dictSize(db->expires) == 0 ||
           (de = dictFind(db->expires,key->ptr)) == NULL) return -1;
    
        /* The entry was found in the expire dict, this means it should also
         * be present in the main dict (safety check). */
        serverAssertWithInfo(NULL,key,dictFind(db->dict,key->ptr) != NULL);
        return dictGetSignedIntegerVal(de);
    }

    5期限切れキー削除ポリシー


    keyの期限切れは期限切れの配列に保存されますが、keyはいつ削除されますか?
  • タイミング削除?有効期限を設定すると同時に、キーの有効期限が到来すると直ちに削除操作
  • を実行するようにタイマを作成する.
  • 不活性削除?キーの期限切れは放置するが、キー空間でキーを取得するたびに期限切れと判断し、削除操作
  • を行う.
  • 定期的に削除しますか?一定時間おきに、プログラムはデータをチェックし、中の期限切れキーを削除します.(複数のデータベースをチェックし、複数のキーを削除するには、アルゴリズムによって決定されます)
  • 1.タイミング削除
    タイミング削除はメモリに友好的で、タイミング削除は期限切れのキーができるだけ早く削除され、キーが占有する空間を解放することを保証することができる.
    ただし、cpuには友好的ではありません.期限切れキーが多い場合、期限切れキーを削除する行為はcpuの時間の相当部分を占め、サーバの時間とスループットに一定の影響を与えます.
    2.不活性削除
    不活性削除はcpu時間にとって最も友好的である:プログラムはキーを取る時に期限切れの検査を行うだけで、検査は現在の操作のキーに限られる.
    欠点はメモリに友好的ではないことです.データベースの多くのキーが期限切れになっていて、これらのキーがアクセスされていない場合、これらのキーも削除されません.
    3.定期的に削除
    定期的な削除は、前の2つのポリシーの側面折衷と見なすことができます.
    定期的に削除して一定時間ごとに削除操作を行い、実行時間と頻度を限定してcpu操作時間への影響を低減する.

    六削除ポリシーの実装


    1.不活性削除
    不活性削除ポリシーの実装はdbにある.c/expireIfNeeded;
    db.cのデータベースキー値検索関数(lookupKeyXXXX関数)はexpireIfNeededメソッドを呼び出し、keyが期限切れであるかどうかを確認します.
    int expireIfNeeded(redisDb *db, robj *key) {
        if (!keyIsExpired(db,key)) return 0;
    
        /* If we are running in the context of a slave, instead of
         * evicting the expired key from the database, we return ASAP:
         * the slave key expiration is controlled by the master that will
         * send us synthesized DEL operations for expired keys.
         *
         * Still we try to return the right information to the caller,
         * that is, 0 if we think the key should be still valid, 1 if
         * we think the key is expired at this time. */
        if (server.masterhost != NULL) return 1;
    
        /* Delete the key */
        server.stat_expiredkeys++;
        propagateExpire(db,key,server.lazyfree_lazy_expire);
        notifyKeyspaceEvent(NOTIFY_EXPIRED,
            "expired",key,db->id);
        return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) :
                                             dbSyncDelete(db,key);
    }

    2.定期的に削除
    定期削除ポリシーの実装はdbにある.c/activeExpireCycle,redisのサーバがサーバを周期的に操作するたびに.c/databasesCron関数が実行されると、activeExpireCycleが実行されます.