Redis 와 캐싱 · 캐시 패턴(look-aside·write-through) · 실습
cache-aside 구현하고 적중률 재기
목표
cache-aside 를 직접 구현해 미스와 히트의 비용 차이를 숫자로 확인하고, 네거티브 캐싱과 버전 키 무효화까지 붙인다.
왜 중요한가
cache-aside 는 가장 흔한 패턴이라 코드는 어디서든 복사할 수 있습니다. 정작 어려운 것은 세부입니다. 쓰기 시 캐시를 갱신할지 삭제할지, 없는 데이터를 캐시할지, 그리고 전체를 한 번에 무효화해야 할 때 무엇을 할지입니다. 첫 번째 — 갱신이 아니라 삭제가 정답인 이유는 두 쓰기가 엇갈릴 때 갱신은 옛 값을 남길 수 있지만 삭제는 그렇지 않기 때문입니다. 두 번째 — 존재하지 않는 키를 반복 조회하는 트래픽은 캐시를 통과해 원본을 그대로 때립니다. 이것을 캐시 관통이라고 하고, 짧은 TTL 의 네거티브 캐싱으로 막습니다. 세 번째 — 상품 카테고리가 통째로 바뀌었을 때 관련 키를 전부 찾아 지우는 것은 의존성 추적 지옥입니다. 버전 키는 그것을 버전 숫자 하나 올리는 일로 바꿉니다.
단계
1. /opt/app/slowdb.py 를 127.0.0.1:8151 에 띄운다. GET /item/1 이 200 을 주고 GET /stats 가 {"queries":<n>} 를 준다.
2. /root/ca/cache.py 에 get_item(id) 를 만든다. 캐시 미스면 원본을 조회해 item:<id> 키에 300초로 저장한다. /root/ca/miss.out 에 source=origin ms=<정수> 를 적고 ms 는 100 이상이어야 한다.
3. 같은 항목을 다시 조회한다. /root/ca/hit.out 에 source=cache ms=<정수> origin_queries_delta=0 을 적고 ms 는 50 미만이어야 한다.
4. /root/ca/update.py 는 원본을 갱신한 뒤 캐시 키를 삭제한다. 소스에 갱신값을 캐시에 다시 쓰는 코드가 있으면 안 된다. 실행 후 EXISTS item:1 이 0 이어야 한다.
5. 존재하지 않는 item:9999 를 조회하면 원본이 404 를 주고, 그 결과를 60초 수명으로 캐시한다. 두 번째 조회에서 원본 조회 수가 늘지 않아야 하고 TTL item:9999 가 1 이상 60 이하여야 한다.
6. /root/ca/bench.py 로 100회 조회(고유 키 20개)를 돌리고 /root/ca/hitratio.txt 에 hits=<n> misses=<n> ratio=<소수> 를 적는다. ratio 는 0.75 이상이어야 한다.
7. 캐시 키를 v<버전>:item:<id> 형태로 바꾸고 버전을 ver:item 키로 관리한다. 버전을 올린 뒤 조회하면 원본 조회 수가 늘어야 한다. /root/ca/version.out 에 old_ver=<n> new_ver=<n> refetched=true 를 적는다.
참고
- 원본 조회 수 확인:
curl -s http://127.0.0.1:8151/stats - 네거티브 캐싱의 TTL 은 정상 캐시보다 훨씬 짧게 둡니다 — 데이터가 곧 생길 수 있기 때문입니다.
- 흔한 실수 1: 쓰기 시 캐시를 갱신하는 것 — 동시 쓰기에서 옛 값이 남을 수 있습니다.
- 흔한 실수 2: 네거티브 캐싱을 안 해 존재하지 않는 ID 를 던지는 요청이 원본을 그대로 때리는 것(캐시 관통).
단계 7개
- 느린 원본 띄우기
- 캐시 미스 경로 만들기
- 두 번째 조회를 캐시로 처리하기
- 갱신 시 캐시를 삭제하기
- 없는 키를 네거티브 캐싱하기
- 적중률 측정하기
- 버전 키로 전체 무효화하기