Skip to content
isdnetworks
Go back

DB 앞에 캐시를 두면서 배운 것

같은 조회가 반복되는 곳이 있어서 MySQL 앞에 Redis 를 두기로 했다. 넣는 것은 쉬웠다.

$key = "user:{$id}:profile";
if ($cached = $redis->get($key)) {
    return json_decode($cached, true);
}
$data = $db->fetchProfile($id);
$redis->setex($key, 300, json_encode($data));
return $data;

조회가 빨라졌는데 며칠 뒤 문제가 나왔다.

Table of contents

Open Table of contents

바꿨는데 안 바뀐다

사용자가 정보를 수정했는데 화면에 옛 값이 나온다. 당연했고 user:{id}:profile 키를 안 지웠다.

수정하는 곳에 삭제를 넣었다.

$db->updateProfile($id, $data);
$redis->del("user:{$id}:profile");

그런데 grep 으로 훑어 보니 수정하는 곳이 한 군데가 아니었다.

사용자가 직접 수정
관리자가 수정
배치가 일괄 수정
다른 기능의 부수효과로 수정

네 곳을 다 찾아 del 을 넣어야 했다. 하나 빠뜨리면 그 경로로 수정했을 때만 옛 값이 남는다.

넣는 곳과 지우는 곳이 멀었다

이것이 캐시의 근본적인 어려움이었다. get 을 부르는 곳은 조회 코드고 del 을 불러야 하는 곳은 수정 코드라 서로 멀다.

새 수정 경로를 만드는 사람이 캐시의 존재를 모르면 안 지운다. 그리고 안 지워도 당장은 아무 일도 안 생기고 만료될 때까지만 이상하다.

시간이 지나 저절로 맞으니 문제가 안 보인다. 오류로도 안 나타난다.

만료 시간으로 완화했다

완벽하게 지우는 것을 포기하고 setex 의 만료를 짧게 가져갔다.

5분 → 30초

del 을 빠뜨려도 최대 30초만 이상하다. 캐시 효과는 줄어들지만 같은 요청이 초당 여러 번 오는 곳이라 30초로도 효과가 컸다.

정확성과 효과의 교환인데 이 경우 정확성이 중요했다.

판단 기준 — 무엇을 캐시할지

전부 캐시하려던 것을 줄였다.

자주 읽고 드물게 바뀜   설정·코드 목록·분류 정보 → 길게
자주 바뀜               사용자 상태·재고 → 짧게 하거나 안 함
돈과 관련된 것          안 함

캐시하면 빨라진다가 아니라 옛 값이 보여도 되는가로 판단했다. 금액처럼 잠깐도 틀리면 안 되는 것은 대상에서 뺐다.

캐시가 죽었을 때와 몰려오는 요청

한 번은 Redis 가 잠깐 죽었는데 전체가 멈췄다.

$cached = $redis->get($key);   // 여기서 예외

캐시가 없으면 MySQL 로 가야 하는데 예외가 나서 요청이 죽는다. 캐시는 없어도 동작해야 한다.

try {
    $cached = $redis->get($key);
} catch (Exception $e) {
    $cached = null;   // 캐시 실패는 조회 실패가 아니다
    log_message('warning', 'cache read failed: ' . $e->getMessage());
}

대신 로그는 남긴다. 캐시가 계속 죽어 있으면 알아야 하니까.

담아 둔 값과 못 찾은 것이 구분이 안 되는 자리도 있다. 키가 없으면 false 를 돌려주는데 값 자체가 false 이거나 빈 문자열이면 같은 것이 오고 매뉴얼도 이 경우를 결과 코드로 갈라야 한다고 적고 있다.

조회 결과가 0건인 것을 그대로 담아 두면 매번 못 찾은 것으로 읽혀 MySQL 까지 간다. 캐시를 둔 자리가 오히려 가장 자주 새는 자리가 되므로 없음을 나타내는 표시를 따로 담아 두게 했다.

setex 만료가 되는 순간 여러 요청이 동시에 MySQL 로 가는 것도 있었다. 평소엔 캐시가 막아 주다가 만료되는 순간 다 통과하고 인기 있는 데이터일수록 심하다.

$ttl = 30 + rand(0, 10);

만료 시간에 무작위를 섞으니 여러 키가 동시에 만료되지 않는다.

정리


Share this post on:

Previous Post
오류만 띄우고 다음 행동을 안 줬다
Next Post
아무것도 안 고르면 빈칸이 나왔다