Redisデータの有効期限設定

リリース: 2019-12-10 17:38:26
転載
5554 人が閲覧しました

Redisデータの有効期限設定

1. Redis のキーの有効期限

EXPIRE を通じてデータの有効期限を設定します。キー秒コマンド。 1 が返された場合は設定が成功したことを示し、0 が返された場合はキーが存在しないか、有効期限を正常に設定できないことを示します。キーに有効期限を設定すると、指定した秒数が経過するとキーは自動的に削除されます。有効期限が指定されたキーは、Redis では不安定であると言われます。

推奨: redis 入門チュートリアル

キーが DEL コマンドによって削除されるか、SET または GETSET コマンドによってリセットされると、それに関連付けられている有効期限がクリアされます。

127.0.0.1:6379> setex s 20 1
OK
127.0.0.1:6379> ttl s
(integer) 17
127.0.0.1:6379> setex s 200 1
OK
127.0.0.1:6379> ttl s
(integer) 195
127.0.0.1:6379> setrange s 3 100
(integer) 6
127.0.0.1:6379> ttl s
(integer) 152
127.0.0.1:6379> get s
"1\x00\x00100"
127.0.0.1:6379> ttl s
(integer) 108
127.0.0.1:6379> getset s 200
"1\x00\x00100"
127.0.0.1:6379> get s
"200"
127.0.0.1:6379> ttl s
(integer) -1
ログイン後にコピー

有効期限をクリアするには PERSIST を使用します

127.0.0.1:6379> setex s 100 test
OK
127.0.0.1:6379> get s
"test"
127.0.0.1:6379> ttl s
(integer) 94
127.0.0.1:6379> type s
string
127.0.0.1:6379> strlen s
(integer) 4
127.0.0.1:6379> persist s
(integer) 1
127.0.0.1:6379> ttl s
(integer) -1
127.0.0.1:6379> get s
"test"
ログイン後にコピー

キー値を変更する場合のみ名前変更を使用します

127.0.0.1:6379> expire s 200
(integer) 1
127.0.0.1:6379> ttl s
(integer) 198
127.0.0.1:6379> rename s ss
OK
127.0.0.1:6379> ttl ss
(integer) 187
127.0.0.1:6379> type ss
string
127.0.0.1:6379> get ss
"test"
ログイン後にコピー

注: Redis2.6 以降、有効期限の精度は次の範囲内で制御できます。 0 ~ 1 ミリ秒、およびキー値 期限切れ情報は絶対 Unix タイムスタンプの形式で保存されるため (Redis 2.6 以降はミリ秒レベルの精度で保存される)、複数のサーバーを同期する場合は、各サーバーの時刻を同期する必要があります

2. Redis の期限切れキーの削除戦略

Redis キーの有効期限が切れるには 3 つの方法があります:

(1) 受動的削除: いつ期限切れのキーの読み取り/書き込みにより、遅延削除戦略がトリガーされ、期限切れのキーが直接削除されます

(2). アクティブな削除: 遅延削除戦略ではコールド データが時間内に削除されることを保証できないため、Redis は期限切れのキーのバッチを定期的かつアクティブに削除します。key

(3)。現在使用されているメモリが maxmemory 制限を超えると、アクティブなクリーンアップ戦略がトリガーされます

パッシブ削除

キーが操作されたとき (GET など) のみ、REDIS はキーの有効期限が切れているかどうかを受動的にチェックし、有効期限が切れている場合は削除され、NIL が返されます。

1. この削除戦略は CPU に優しいもので、削除操作は必要な場合にのみ実行され、不必要な CPU 時間が他の期限切れのキーに浪費されることはありません。

2. ただし、この戦略はメモリには優しくなく、キーの有効期限が切れていますが、操作される前に削除されず、依然としてメモリ領域を占有します。期限切れのキーが多数存在してもほとんどアクセスされない場合、メモリ領域が大量に無駄になります。 expireIfNeeded(redisDb *db, robj *key) 関数は src/db.c にあります。

/*-----------------------------------------------------------------------------
 * Expires API
 *----------------------------------------------------------------------------*/
 
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. */
    redisAssertWithInfo(NULL,key,dictFind(db->dict,key->ptr) != NULL);
    return dictDelete(db->expires,key->ptr) == DICT_OK;
}
 
void setExpire(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);
    redisAssertWithInfo(NULL,key,kde != NULL);
    de = dictReplaceRaw(db->expires,dictGetKey(kde));
    dictSetSignedIntegerVal(de,when);
}
 
/* 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). */
    redisAssertWithInfo(NULL,key,dictFind(db->dict,key->ptr) != NULL);
    return dictGetSignedIntegerVal(de);
}
 
/* Propagate expires into slaves and the AOF file.
 * When a key expires in the master, a DEL operation for this key is sent
 * to all the slaves and the AOF file if enabled.
 *
 * This way the key expiry is centralized in one place, and since both
 * AOF and the master->slave link guarantee operation ordering, everything
 * will be consistent even if we allow write operations against expiring
 * keys. */
void propagateExpire(redisDb *db, robj *key) {
    robj *argv[2];
 
    argv[0] = shared.del;
    argv[1] = key;
    incrRefCount(argv[0]);
    incrRefCount(argv[1]);
 
    if (server.aof_state != REDIS_AOF_OFF)
        feedAppendOnlyFile(server.delCommand,db->id,argv,2);
    replicationFeedSlaves(server.slaves,db->id,argv,2);
 
    decrRefCount(argv[0]);
    decrRefCount(argv[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&#39;t expire anything while loading. It will be done later. */
    if (server.loading) return 0;
 
    /* If we 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
     * only the first time it is accessed and not in the middle of the
     * script execution, making propagation to slaves / AOF consistent.
     * See issue #1525 on Github for more information. */
    now = server.lua_caller ? server.lua_time_start : mstime();
 
    /* If we are running in the context of a slave, 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 now > when;
 
    /* Return when this key has not expired */
    if (now <= when) return 0;
 
    /* Delete the key */
    server.stat_expiredkeys++;
    propagateExpire(db,key);
    notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED,
        "expired",key,db->id);
    return dbDelete(db,key);
}
 
/*-----------------------------------------------------------------------------
 * Expires Commands
 *----------------------------------------------------------------------------*/
 
/* This is the generic command implementation for EXPIRE, PEXPIRE, EXPIREAT
 * and PEXPIREAT. 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 *AT variants of the command, or the current time for relative expires).
 *
 * unit is either UNIT_SECONDS or UNIT_MILLISECONDS, and is only used for
 * the argv[2] parameter. The basetime is always specified in milliseconds. */
void expireGenericCommand(redisClient *c, long long basetime, int unit) {
    robj *key = c->argv[1], *param = c->argv[2];
    long long when; /* unix time in milliseconds when the key will expire. */
 
    if (getLongLongFromObjectOrReply(c, param, &when, NULL) != REDIS_OK)
        return;
 
    if (unit == UNIT_SECONDS) when *= 1000;
    when += basetime;
 
    /* No key, return zero. */
    if (lookupKeyRead(c->db,key) == NULL) {
        addReply(c,shared.czero);
        return;
    }
 
    /* EXPIRE with 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.
     *
     * 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. */
    if (when <= mstime() && !server.loading && !server.masterhost) {
        robj *aux;
 
        redisAssertWithInfo(c,key,dbDelete(c->db,key));
        server.dirty++;
 
        /* Replicate/AOF this as an explicit 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 {
        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;
    }
}
 
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);
}
 
void ttlGenericCommand(redisClient *c, int output_ms) {
    long long expire, ttl = -1;
 
    /* If the key does not exist at all, return -2 */
    if (lookupKeyRead(c->db,c->argv[1]) == NULL) {
        addReplyLongLong(c,-2);
        return;
    }
    /* 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));
    }
}
 
void ttlCommand(redisClient *c) {
    ttlGenericCommand(c, 0);
}
 
void pttlCommand(redisClient *c) {
    ttlGenericCommand(c, 1);
}
 
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);
        }
    }
}
ログイン後にコピー

しかし、これだけでは十分ではありません。二度とアクセスされないキーが存在する可能性があるためです。これらの有効期限が設定されたキーも、有効期限が切れたら削除する必要があります。これを追加することもできます。状況は次のようにみなされます。メモリ リーク - 無駄なガベージ データが大量のメモリを占有しますが、サーバーはそれらを自動的に解放しません。実行ステータスがメモリに大きく依存する Redis サーバーにとって、これは明らかに良いアイデアではありません。

#アクティブな削除

最初に時間イベントについて話しましょう。継続的に実行されているサーバーの場合、サーバーは定期的に自身のリソースとステータスを確認して整理する必要があります。これにより、サーバーを維持することができます。この種の操作を総称して定期操作 (cron ジョブ) と呼びます。

Redis では、定期操作は redis.c/serverCron によって実装され、主に次の操作を実行します。

時間、メモリ使用量、データベース使用量など、サーバーのさまざまな統計情報を更新します。

データベース内の期限切れのキーと値のペアを削除します。

不合理なデータベースのサイズを変更します。

接続に失敗したクライアントを閉じてクリーンアップします。

AOF または RDB 永続化操作を実行しようとしました。

サーバーがマスター ノードの場合は、スレーブ ノードで定期的な同期を実行します。

クラスター モードの場合は、クラスター上で定期的に同期と接続テストを実行します。

Redis は、serverCron を時間イベントとして実行して、定期的に自動的に実行されるようにします。また、serverCron は Redis サーバーの実行中に定期的に実行する必要があるため、周期的な時間イベントです。サーバーがシャットダウンされるまで定期的に実行されます。

Redis 2.6 バージョンでは、プログラムは、serverCron が 1 秒あたり 10 回実行され、平均して 100 ミリ秒に 1 回実行されると規定しています。 Redis 2.8 以降、ユーザーは hz オプションを変更することで、serverCron の 1 秒あたりの実行数を調整できます。

これは、スケジュールされた削除とも呼ばれます。ここでの「定期的」とは、Redis によって定期的にトリガーされるクリーンアップ戦略を指し、src/redis.c にある activeExpireCycle(void) 関数によって完了します。

serverCron は、redis イベント フレームワークによって駆動される配置タスクです。このスケジュールされたタスクは、activeExpireCycle 関数を呼び出します。データベースごとに、制限時間 REDIS_EXPIRELOOKUPS_TIME_LIMIT 内にできるだけ多くの期限切れのキーを削除します。制限の理由これは、長期的なブロックが Redis の通常の動作に影響を与えるのを防ぐためです。この能動的削除戦略は、受動的削除戦略のメモリの不便さを補います。

したがって、Redis は有効期限が設定されたキーのバッチを定期的にランダムにテストし、処理します。テストされた期限切れのキーは削除されます。一般的な方法では、Redis は次の手順を 1 秒あたり 10 回実行します。

(1)随机测试100个设置了过期时间的key

(2)删除所有发现的已过期的key

(3)若删除的key超过25个则重复步骤1

这是一个基于概率的简单算法,基本的假设是抽出的样本能够代表整个key空间,redis持续清理过期的数据直至将要过期的key的百分比降到了25%以下。这也意味着在任何给定的时刻已经过期但仍占据着内存空间的key的量最多为每秒的写操作量除以4.

Redis-3.0.0中的默认值是10,代表每秒钟调用10次后台任务。

除了主动淘汰的频率外,Redis对每次淘汰任务执行的最大时长也有一个限定,这样保证了每次主动淘汰不会过多阻塞应用请求,以下是这个限定计算公式:

#define ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 25 /* CPU max % for keys collection */ 
... 
timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100;
ログイン後にコピー

hz调大将会提高Redis主动淘汰的频率,如果你的Redis存储中包含很多冷数据占用内存过大的话,可以考虑将这个值调大,但Redis作者建议这个值不要超过100。我们实际线上将这个值调大到100,观察到CPU会增加2%左右,但对冷数据的内存释放速度确实有明显的提高(通过观察keyspace个数和used_memory大小)。

可以看出timelimit和server.hz是一个倒数的关系,也就是说hz配置越大,timelimit就越小。换句话说是每秒钟期望的主动淘汰频率越高,则每次淘汰最长占用时间就越短。这里每秒钟的最长淘汰占用时间是固定的250ms(1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/100),而淘汰频率和每次淘汰的最长时间是通过hz参数控制的。

从以上的分析看,当redis中的过期key比率没有超过25%之前,提高hz可以明显提高扫描key的最小个数。假设hz为10,则一秒内最少扫描200个key(一秒调用10次*每次最少随机取出20个key),如果hz改为100,则一秒内最少扫描2000个key;另一方面,如果过期key比率超过25%,则扫描key的个数无上限,但是cpu时间每秒钟最多占用250ms。

当REDIS运行在主从模式时,只有主结点才会执行上述这两种过期删除策略,然后把删除操作”del key”同步到从结点。

maxmemory

当前已用内存超过maxmemory限定时,触发主动清理策略:

volatile-lru:只对设置了过期时间的key进行LRU(默认值)

allkeys-lru : 删除lru算法的key

volatile-random:随机删除即将过期key

allkeys-random:随机删除

volatile-ttl : 删除即将过期的

noeviction : 永不过期,返回错误当mem_used内存已经超过maxmemory的设定,对于所有的读写请求,都会触发redis.c/freeMemoryIfNeeded(void)函数以清理超出的内存。注意这个清理过程是阻塞的,直到清理出足够的内存空间。所以如果在达到maxmemory并且调用方还在不断写入的情况下,可能会反复触发主动清理策略,导致请求会有一定的延迟。

当mem_used内存已经超过maxmemory的设定,对于所有的读写请求,都会触发redis.c/freeMemoryIfNeeded(void)函数以清理超出的内存。注意这个清理过程是阻塞的,直到清理出足够的内存空间。所以如果在达到maxmemory并且调用方还在不断写入的情况下,可能会反复触发主动清理策略,导致请求会有一定的延迟。

清理时会根据用户配置的maxmemory-policy来做适当的清理(一般是LRU或TTL),这里的LRU或TTL策略并不是针对redis的所有key,而是以配置文件中的maxmemory-samples个key作为样本池进行抽样清理。

maxmemory-samples在redis-3.0.0中的默认配置为5,如果增加,会提高LRU或TTL的精准度,redis作者测试的结果是当这个配置为10时已经非常接近全量LRU的精准度了,并且增加maxmemory-samples会导致在主动清理时消耗更多的CPU时间,建议:

(1)尽量不要触发maxmemory,最好在mem_used内存占用达到maxmemory的一定比例后,需要考虑调大hz以加快淘汰,或者进行集群扩容。

(2)如果能够控制住内存,则可以不用修改maxmemory-samples配置;如果Redis本身就作为LRU cache服务(这种服务一般长时间处于maxmemory状态,由Redis自动做LRU淘汰),可以适当调大maxmemory-samples。

以下是上文中提到的配置参数的说明

# Redis calls an internal function to perform many background tasks, like 
# closing connections of clients in timeout, purging expired keys that are 
# never requested, and so forth. 
# 
# Not all tasks are performed with the same frequency, but Redis checks for 
# tasks to perform according to the specified "hz" value. 
# 
# By default "hz" is set to 10. Raising the value will use more CPU when 
# Redis is idle, but at the same time will make Redis more responsive when 
# there are many keys expiring at the same time, and timeouts may be 
# handled with more precision. 
# 
# The range is between 1 and 500, however a value over 100 is usually not 
# a good idea. Most users should use the default of 10 and raise this up to 
# 100 only in environments where very low latency is required. 
hz 10 
 
# MAXMEMORY POLICY: how Redis will select what to remove when maxmemory 
# is reached. You can select among five behaviors: 
# 
# volatile-lru -> remove the key with an expire set using an LRU algorithm 
# allkeys-lru -> remove any key according to the LRU algorithm 
# volatile-random -> remove a random key with an expire set 
# allkeys-random -> remove a random key, any key 
# volatile-ttl -> remove the key with the nearest expire time (minor TTL) 
# noeviction -> don&#39;t expire at all, just return an error on write operations 
# 
# Note: with any of the above policies, Redis will return an error on write 
#       operations, when there are no suitable keys for eviction. 
# 
#       At the date of writing these commands are: set setnx setex append 
#       incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd 
#       sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby 
#       zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby 
#       getset mset msetnx exec sort 
# 
# The default is: 
# 
maxmemory-policy noeviction 
 
# LRU and minimal TTL algorithms are not precise algorithms but approximated 
# algorithms (in order to save memory), so you can tune it for speed or 
# accuracy. For default Redis will check five keys and pick the one that was 
# used less recently, you can change the sample size using the following 
# configuration directive. 
# 
# The default of 5 produces good enough results. 10 Approximates very closely 
# true LRU but costs a bit more CPU. 3 is very fast but not very accurate. 
# 
maxmemory-samples 5
ログイン後にコピー

Replication link和AOF文件中的过期处理

为了获得正确的行为而不至于导致一致性问题,当一个key过期时DEL操作将被记录在AOF文件并传递到所有相关的slave。也即过期删除操作统一在master实例中进行并向下传递,而不是各salve各自掌控。

これにより、データの不整合はなくなります。スレーブがマスターに接続されている場合、期限切れのキーをすぐにクリーンアップすることはできません (マスターによって渡される DEL 操作を待つ必要があります)。スレーブは、データ セット内の期限切れステータスを管理および維持する必要があります。スレーブがマスターに昇格するとマスターと同様に動作でき、期限切れ処理も独立して実行されます。

以上がRedisデータの有効期限設定の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

関連ラベル:
ソース:cnblogs.com
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート