Redis의 데이터 만료 전략에 대한 자세한 설명

小云云
풀어 주다: 2023-03-17 21:54:01
원래의
3102명이 탐색했습니다.

Redis의 데이터 만료에 대해서는 누구나 어느 정도 이해하고 있을 것이라 생각합니다. 이 글에서는 Redis의 데이터 만료 전략을 주로 소개하고 있으며, 이를 샘플 코드를 통해 자세히 소개하고 있어 도움이 필요한 모든 분들의 이해와 학습에 도움이 될 것으로 생각합니다. 참고로 모두에게 도움이 되었으면 좋겠습니다.

1. Redis의 키 만료 시간

EXPIRE 키 초 명령을 사용하여 데이터의 만료 시간을 설정합니다. 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"
로그인 후 복사

이름 바꾸기를 사용하면 키 값만 변경됩니다

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 키를 만료하는 방법에는 세 가지가 있습니다.

  1. 수동 삭제: 만료된 키를 읽거나 쓸 때 지연 삭제 전략이 실행되어 직접 삭제됩니다. 만료된 키 삭제

  2. 활성 삭제: 지연 삭제 전략은 콜드 데이터가 제때 삭제된다는 것을 보장할 수 없으므로 Redis는 만료된 키 일괄 처리를 정기적으로 적극적으로 제거합니다

  3. 현재 사용된 메모리가 maxmemory 제한을 초과하면 활성 정리 전략이 실행됩니다.

수동 삭제

키가 작동될 때만(예: GET) REDIS는 키가 만료되었는지 수동적으로 확인합니다. 만료되면 삭제하고 NIL을 반환합니다.

1. 이 삭제 전략은 CPU에 친화적입니다. 삭제 작업은 필요한 경우에만 수행되며, 만료된 다른 키에 불필요한 CPU 시간이 낭비되지 않습니다.

2. 그러나 이 전략은 메모리에 친화적이지 않습니다. 키가 만료되었지만 작동되기 전에는 삭제되지 않으며 여전히 메모리 공간을 차지합니다. 만료된 키가 많이 존재하지만 거의 액세스되지 않는 경우 메모리 공간이 많이 낭비됩니다. 만료IfNeeded(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 서버에게는 확실히 좋은 소식이 아닙니다.

Active 삭제

먼저 시간 이벤트에 대해 이야기해 보겠습니다. . 지속적으로 실행되는 서버의 경우, 서버를 건강하고 안정적인 상태로 유지하기 위해 서버는 정기적으로 자체 리소스와 상태를 확인하고 정리해야 합니다.

Redis에서는 일반적인 작업은 redis.c/serverCron에 의해 구현됩니다. 주로 다음 작업을 수행합니다.

  • 시간, 메모리 사용량, 데이터베이스 사용량 등 서버의 다양한 통계 정보를 업데이트합니다.

  • 데이터베이스에서 만료된 키-값 쌍을 정리합니다.

  • 불합리한 데이터베이스 크기를 조정하세요.

  • 연결이 실패한 클라이언트를 닫고 정리하세요.

  • AOF 또는 RDB 지속성 작업을 수행해 보세요.

  • 서버가 마스터 노드인 경우 슬레이브 노드의 정기적인 동기화를 수행합니다.

  • 클러스터 모드인 경우 클러스터에서 정기적인 동기화 및 연결 테스트를 수행합니다.

Redis는 serverCron을 시간 이벤트로 실행하여 가끔씩 자동으로 실행되도록 하며, Redis 서버가 실행되는 동안 serverCron은 정기적으로 실행되어야 하기 때문에 순환 시간 이벤트입니다. serverCron은 항상 주기적으로 실행됩니다. 서버가 종료될 때까지.

Redis 2.6 버전에서 프로그램은 serverCron이 초당 10번, 평균 100밀리초마다 한 번씩 실행되도록 규정하고 있습니다. Redis 2.8부터는 hz 옵션을 수정하여 초당 serverCron 실행 횟수를 조정할 수 있습니다. 자세한 내용은 redis.conf 파일의 hz 옵션 설명을 참조하세요. 여기서 "주기적"은 Redis를 나타냅니다. 정기적으로 트리거되는 정리 전략은 src/redis.c에 있는 activeExpireCycle(void) 함수에 의해 완료됩니다.

serverCron은 redis 이벤트 프레임워크에 의해 구동되는 위치 지정 작업입니다. 이 예약된 작업은 activeExpireCycle 함수를 호출하여 각 db에 대해 제한된 시간 REDIS_EXPIRELOOKUPS_TIME_LIMIT 내에 만료된 키를 최대한 많이 삭제합니다. 시간을 제한하는 이유는 과도한 Long을 방지하기 위한 것입니다. -term 차단은 Redis의 정상적인 작동에 영향을 미칩니다. 이 능동 삭제 전략은 수동 삭제 전략의 메모리 비친화성을 보완합니다.

따라서 Redis는 만료 시간이 설정된 키 배치를 정기적으로 무작위로 테스트하고 처리합니다. 테스트된 만료된 키는 삭제됩니다.

일반적인 방법은 Redis가 다음 단계를 초당 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 중국어 웹사이트의 기타 관련 기사를 참조하세요!

관련 라벨:
원천:php.cn
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿