ホームページ  >  記事  >  バックエンド開発  >  Redis のデータ有効期限戦略の詳細な説明

Redis のデータ有効期限戦略の詳細な説明

小云云
小云云オリジナル
2018-05-22 15:08:483062ブラウズ

この記事では、Redis のデータ有効期限について、主にサンプル コードを通じて詳しく紹介します。参考になれば幸いです。

1. Redis のキーの有効期限

データの有効期限を設定するには、EXPIRE key Seconds コマンドを使用します。 1 が返された場合は設定が成功したことを示し、0 が返された場合はキーが存在しないか、有効期限を正常に設定できないことを示します。キーに有効期限を設定すると、指定した秒数が経過するとキーは自動的に削除されます。有効期限が指定されたキーは、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"

rename を使用するとキーの値のみが変更されます

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 タイムスタンプの形式で保存されます (Redis2.6 以降は、ミリ秒レベルの精度で保存されます)。 )、複数のサーバーを同期する場合は、各サーバーの時刻を同期する必要があります

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

Redis キーを期限切れにする方法は 3 つあります:

  1. 受動的削除: 期限切れのキーの読み取り/書き込み時キーを指定すると、遅延削除戦略がトリガーされ、直接削除されます この期限切れのキーを削除します

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

  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 サーバーの実行中に定期的に実行する必要があるため、周期的な時間イベントです。serverCron は常に定期的に実行されます。サーバーがシャットダウンされるまで。

Redis 2.6 バージョンでは、プログラムは、serverCron が 1 秒あたり 10 回実行され、平均して 100 ミリ秒に 1 回実行されると規定しています。 Redis 2.8 以降、ユーザーは hz オプションを変更することで 1 秒あたりの serverCron の実行数を調整できます。詳細については、redis.conf ファイルの hz オプションの説明を参照してください。これはスケジュールされた削除とも呼ばれます。ここでの「定期的」とは Redis を指します。定期的にトリガーされるクリーンアップ戦略は、src/redis.c にある activeExpireCycle(void) 関数によって完了します。

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

そのため、Redis は有効期限が設定されたキーのバッチを定期的にランダムにテストし、処理します。テストされた期限切れのキーは削除されます。

典型的な方法では、Redis は次のステップを 1 秒あたり 10 回実行します。

  • 随机测试100个设置了过期时间的key

  • 删除所有发现的已过期的key

  • 若删除的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时间,建议:

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

  • 如果能够控制住内存,则可以不用修改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'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文件中的过期处理

一貫性の問題を引き起こすことなく正しい動作を得るために、キーの有効期限が切れると、DEL 操作が AOF ファイルに記録され、関連するすべてのスレーブに渡されます。つまり、期限切れの削除操作は、各スレーブによって個別に制御されるのではなく、マスター インスタンスで均一に実行され、継承されます。こうすることで、データの不整合はなくなります。スレーブがマスターに接続されている場合、期限切れのキーをすぐにクリーンアップすることはできません (マスターから渡される DEL 操作を待つ必要があります)。スレーブは、データ セット内の期限切れステータスを管理および維持する必要があります。スレーブがマスターに昇格すると、期限切れ処理も独立して実行されます。

関連おすすめ:

phpでのRedis機能の使い方まとめ

Redisクラスター構築の全記録

phpでredisを操作する一般的な方法のまとめ

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

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。