[입 개발] Redis 에서 Key의 Expire는 어떻게 처리되는가?

Redis를 사용하다보면 Key 의 Expire가 어떻게 처리되는지 궁금할 때가 있습니다. 결론부터 말하자면, Redis에서는 Key가 두 가지 방법으로 Expire가 될 수 가 있습니다.

  1. memcached 처럼 key에 대한 접근이 발생할 때
  2. activeExpireCycle 에 의한 삭제
  3. command 처리 전에 memory가 부족할 때 메모리 정책에 따라서 삭제

memcached의 경우 expire 된 key의 경우 그 시점에 지워지지 않고 실제로 해당 key에 접근이 되는 시점에 없다라고 돌려주게 됩니다. Redis도 기본적으로 get등의 operation이 발생할 때 key의 expire가 되었는지를 체크하게됩니다. 이 때 getExpire라는 함수를 이용해서 expire time을 가져오게 됩니다.

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);
}

그런데 dictFind를 보면 db->expires에서 key를 찾습니다. 그렇다는 얘기는 expire되는 key들을 따로 관리한다는 의미가 될것 같습니다. 그럼 어디서 이것을 변경하게 될까요? getExpire대신에 setExpire라는 함수가 있습니다.

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);
}

실제로 모든 key는 db->dict에 저장되고 db->expire에서 expire값을 가진 key만 추가로 관리합니다. 그래서 실제로 dbDelete에서는 두 군데서 해당 포인터를 제거합니다.

그런데 이런 작업 이외에도 expire 된 key가 지워집니다. aof rewrite시에 실제로 보고된 적은 없지만, expire된 key가 타이밍에 따라서 지워지지 않고 aof에 저장될 수 있는 버그를 수정하면서, 중간중간 계속 key가 먼저 지워져서 고민했던 적이 있는데 이 부분이 바로 activeExpireCycle 때문입니다.

100ms 마다 actvieExpireCycle이 database들을 돌면서 랜덤하게 expire된 데이터를 삭제합니다. timeout이 있고, 이 시간마다 다음 작업할 DB를 기억하고 있다가 완료될 때 까지 작업을 하게 됩니다. timelimit은 다음과 같이 정해집니다.

timelimit = 1000000*REDIS_EXPIRELOOKUPS_TIME_PERC/server.hz/100;

마지막으로 memory정책에 따라서 command 처리전에 메모리 확보를 위해서 key를 지우게 됩니다.
이 작업은 freeMemoryIfNeeded 에서 진행하게 됩니다.