Cache(캐시)

캐시(Cache)는 데이터나 값을 미리 복사해두는 임시 저장소로, 데이터에 대한 접근을 더 빠르게 만드는 역할을 한다.

  • 원본 데이터베이스(DB)에서 데이터를 가져오는 과정이 복잡하거나 계산이 필요한 정보

  • 데이터가 자주 변경되지 않고, 반복적으로 조회되는 정보

캐시로서의 레디스

레디스는 인메모리(In-Memory) 데이터 저장소로서 캐시를 구현하는 데 다음과 같은 강력한 이점을 제공한다.

  • 빠른 속도: 모든 데이터가 메모리에 있어 평균 1ms 미만의 매우 빠른 읽기/쓰기 성능 보장

  • 다양한 자료구조: 단순 Key-Value를 넘어 리스트, 해시, Sorted Set 등 다양한 자료구조를 지원하여 복잡한 캐싱 요구사항 구현

  • 높은 안정성: 자체적으로 복제(Replication)와 센티널(Sentinel) 같은 고가용성 솔루션을 제공한 안정적인 서비스 운영

  • 유연한 확장성: 클러스터링을 통해 데이터를 여러 서버에 분산하여 저장 공간과 처리량을 수평적인 확장(Scale-out)

캐싱 전략

읽기 전략 - Look Aside

가장 일반적으로 사용되는 캐싱 패턴으로, 애플리케이션이 데이터 흐름을 직접 제어한다.

  1. 애플리케이션에서 먼저 레디스 캐시에서 데이터를 조회

  2. 데이터가 캐시에 존재하면(Cache Hit) -> 즉시 해당 데이터를 반환

  3. 데이터가 캐시에 없으면(Cache Miss) -> 원본 DB에서 데이터를 조회

  4. DB에서 가져온 데이터를 캐시에 저장한 후, 애플리케이션에 반환

  5. 이후 동일한 데이터 요청은 캐시에서 처리

  • 장점

    • 실제로 사용되는 데이터만 캐시에 저장되므로 메모리를 효율적으로 사용 가능

    • 레디스에 장애가 발생해도 DB에서 데이터를 직접 조회하여 서비스 장애 방지

  • 단점

    • 최초 데이터 조회 시에는 항상 Cache Miss가 발생하여 DB 조회가 필수적

    • 대량의 요청이 동시에 Cache Miss를 일으키면 DB에 부하가 집중

단점을 방지하기 위해 최초에 데이터베이스 데이터를 캐시에 넣어주는 작업을 진행하기도 하는데, 캐시 워밍(cache warming)이라고 한다.

쓰기 전략과 캐시의 일관성

원본 데이터가 변경되었으나 캐시에는 이전 데이터가 남아있는 상태를 캐시 불일치(Cache Inconsistency)현상과 성능을 고려하여, 다음과 같은 쓰기 전략을 고려할 수 있다.

  1. Write-Through: 데이터베이스에 데이터를 저장과 동시에 항상 캐시에도 업데이트시키는 방식

    • 캐시에 항상 최신 데이터를 가져 불일치를 최소화

    • 매번 쓰기 연산이 발생하므로 저장 시 속도가 느릴 수 있음

    • 다시 사용될 만한 데이터가 아닌 경우 리소스 낭비

  2. Write-Around + Cache Invalidation: 데이터를 쓸 때는 DB에만 직접 업데이트하고, 해당 데이터의 캐시는 삭제(Invalidation)하는 방식

    • 새로운 데이터 저장보다 특정 데이터 삭제가 더 빠르기 때문에 쓰기 성능이 향상

    • 데이터 변경 후 첫 읽기 요청은 반드시 Cache Miss가 발생

  3. Write-Back(Write-Behind): 데이터를 쓸 때 캐시에만 먼저 저장하고, 특정 주기나 조건에 따라 캐시의 데이터를 DB에 비동기적으로 일괄 저장하는 방식

    • 쓰기 요청을 메모리에서 빠르게 처리하므로 쓰기 성능이 매우 뛰어남

    • 캐시에만 데이터가 저장된 상태에서 장애가 발생하면 데이터가 유실 위험

캐시에서의 데이터 흐름

캐시는 메모리 용량이 한정적이므로, 불필요한 데이터를 삭제하고 중요한 데이터를 유지하는 정책이 필수적이다.

만료 시간(TTL) 설정

레디스에서는 키에 만료 시간(Time To Live, TTL)을 설정하여 지정된 시간이 지나면 데이터가 자동으로 삭제되도록 할 수 있다.

  • EXPIRE key seconds: 키의 만료 시간을 초 단위 설정

  • SET key value EX seconds: 키를 저장함과 동시에 만료 시간 설정

  • TTL key: 키의 남은 만료 시간을 조회(영구 키는 -1, 존재하지 않으면 -2)

한 번 설정 된 만료 시간은 키의 이름을 바꾸거나 데이터를 조작하더라도 만료 시간은 변경되지 않지만, 새로운 값으로 키를 덮어 쓰면 만료 시간이 초기화된다.

EXPIRE key 100
INCR key
TTL key
-- 100, 그대로 유지
RENAME key newkey
TTL newkey
-- 100, 그대로 유지
SET key 200 EX 100
SET key 300
TTL key
-- -1, 만료 시간 초기화

만료 시 삭제 정책

레디스에서 키가 만료됐더라도 바로 메모리에서 삭제되는 것이 아니라, passive 방식과 active 방식 두 가지 방식으로 삭제된다.

  • passive: 클라이언트가 키에 접근하고자 할 때 만료됐을 경우 메모리에서 수동적으로 삭제

    • 사용자가 다시 접근하지 않는 키가 존재할 수 있어 메모리를 낭비할 수 있음

  • active: 일정 주기마다 TTL이 존재하는 키 중 일정 갯수만큼 랜덤하게 뽑아낸 뒤, 만료된 키를 삭제(1초에 10번 씩 / 20개씩 랜덤으로)

메모리 한계 도달 시 삭제 정책

설정한 최대 메모리(maxmemory)에 도달했을 때, 어떤 키를 삭제할지 결정하는 정책(maxmemory-policy)이다.

  • Noeviction(기본값): 데이터가 가득 차더라도 데이터를 삭제하지 않고 에러를 발생

    • 로직에 따라 장애 상황으로 이어질 수 있어, 캐시로 사용할 때 권장하지 않는 설정값

    • 데이터의 관리를 캐시에게 맡기는게 아닌, 애플리케이션에서 데이터를 관리하는 것이 좋을 때 고려 가능

  • LRU eviction: 가장 오랫동안 사용되지 않은 키를 삭제

    • volatile-lru / allkeys-lru 알고리즘 존재(레디스 공식 문서에서는 allkeys-lru를 권장)

  • LFU eviction: 가장 적게 사용된 키를 삭제

    • volatile-lfu / allkeys-lfu 알고리즘 존재

  • Random eviction: 랜덤하게 키를 삭제(사용하지 않는 것을 권장)

    • 알고리즘을 사용하지 않아 삭제될 키 값을 계산하지 않아 부하가 적음

  • volatile-ttl: 만료 시간이 있는 키 중 만료 시간이 가장 적게 남은 키를 삭제

캐시 스탬피드

캐시 스탬피드는 특정 키가 만료되는 순간, 수많은 요청이 동시에 해당 키를 조회하여 Cache Miss를 일으키고, 그 요청들이 모두 DB로 몰려들어 DB에 과부하를 주는 현상이다.

  1. 애플리케이션에서 특정 데이터를 조회

  2. 캐시에 데이터가 없어 데이터베이스에서 데이터를 조회

  3. 데이터베이스에서 데이터를 조회하여 캐시에 저장하는 동안 다른 요청이 발생

  4. 아직 캐시에 데이터가 저장되지 않은 상태에서 다른 요청에 의해 데이터베이스에서 다시 조회

  5. 한꺼번에 많은 요청이 왔다면 데이터베이스에 많은 쿼리가 발생하여 부하가 걸릴 수 있음

캐시 스탬피드 방지 방법

캐시 스탬피드를 방지하기 위한 가장 간단한 방법은 만료 시간을 충분히 길게 설정하는 것이며, 다른 방법으로는 다음과 같은 방법들이 존재한다.

  1. 선 계산: 캐시가 만료되기 전 랜덤한 확률로 캐시를 갱신

  2. PER 알고리즘: 랜덤 기반이나, 만료 시간이 가까워질수록 갱신 확률을 높이는 방법

참고자료

Last updated

Was this helpful?