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'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'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 サイトの他の関連記事を参照してください。