Data Engineering/Server

CacheDB - Redis로 빠르게 데이터 불러오기

cstory-bo 2023. 12. 28. 14:03

이번에는 CacheDB인 Redis를 이용해서
데이터를 빠르게 조회하는 방법에 대해 알아볼 예정이다.

Cache란?

우선 Cache는 데이터가 미래에 사용될 것이라고 예상하여 미리 빠르게 조회할 수 있도록 따로 저장해두는 것을 말한다.

여기서
Cache hit 은 조회하는 데이터를 캐시에서 발견했을 때

Cache miss 는 발견하지 못했을 때

Hit rate(ratio)는 access시도 횟수 대비 hit를 성공한 비율이다.

Cache의 사용 사례

  • 캐시는 CPU 칩 내부에서 디스크의 접속 시간을 줄이고자 내부의 캐시메모리로 메모리 IO에 의한 속도를 줄일 때 사용된다.
  • 데이터베이스에서 인덱싱 데이터는 메모리에 올려두고 관리하기 때문에 인덱싱 기준으로 조회하면 조회 속도가 빠르다.
  • 웹 브라우저에서도 최근에 접속한 사이트나 이미지를 캐시에 두고 관리하여 재방문 시 빠르게 접속할 수 있다.
  • 메타데이터나 설정정보 같은 수정이 거의 없는 데이터를 메모리에 올려두고 재사용을 반복하며 주기적으로 업데이트하는 방법을 사용한다.

Cache 사용 시 유의사항

  • 휘발메모리에 올라가는 임시 데이터이기 때문에 유실되면 안되는 데이터는 원본을 따로 두어야한다.
  • 용량이 상대적으로 작고 비싸다. 그래서 hit rate를 측정하며 최대한 효율적으로 쓰기 위해 노력해야한다.

Cache DB

다른말로 In-memory DB라고도 부른다.

캐시처럼 데이터를 디스크가 아닌 메모리에만 저장하고
빠르게 데이터를 조회 및 저장 기능을 제공하는 데이터베이스이다.

종류로는
Memcached, Couchbase 그리고 Redis가 있다.

Memcached는
key-value 형식으로 데이터를 저장 및 조회하며
멀티 쓰레드로 동작해서 트래픽이 몰려도 비교적 속도면에서 안정적이다.
slab allocator로 할당하기 때문에 memory fragmentation 문제가 상대적으로 적지만 데이터 변경이 잦으면 발생하기 쉽다.
slab allocator(슬랩 할당자)-미리 지정된 블럭 단위로 할당
Redis보다 메타 데이터를 적게 사용하여 메모리 사용량이 상대적으로 낮아서 좋다.
=> 그래서 변경이 적은 데이터에 유리하다.

Couchbase는
데이터를 Document 단위로 저장하고 필드도 key-value로 조회한다.
조회속도가 빠르고 분산 클러스터 아키텍처로 구성되어 있어 확장성도 좋다.
다른 in-memory db에서 지원하지 않는 기능(Secondary-index, SQL 등)도 지원한다.
하지만 Client 라이브러리가 충분히 효율적이지 않고 무료버전의 안정성과 기능에 한계가 있다.

마지막으로 사용할 Redis는
다양한 자료구조를 API로 제공하고 key-value 형식으로 지원하는 in-memory db이다.
싱글쓰레드로 동작하기에 데이터 lock 없이 빠르게 데이터를 조회할 수 있다.
대신 memcached와는 다르게 트래픽이 몰리면 속도가 불안정할 수 있다.
메타 데이터 등으로 인해 실제 데이터의 양보다 더 많은 메모리가 필요하다는 단점이 있지만
가장 기능이 풍부하고 레퍼런스가 많다는 장점이 있다.

처음 시작하기에 메모리의 양보다는 참고할 레퍼런스가 많고
자료구조 설계와 사례가 용도에 따라 명확하기에, CacheDB의 기본적인 목표인 가장 빠른 조회속도를 내기 가장 쉽다.
분산 클러스터 모드도 지원하여 확장성도 제공하고 기능이 다양하기에 선택하였다.

Redis

Redis cli로 아래 링크를 따라 설치하였다.
https://redis.io/docs/install/install-redis/install-redis-on-mac-os/

 

Install Redis on macOS

Use Homebrew to install and start Redis on macOS

redis.io

Redis의 대표적인 자료 형식과 기능

** 무엇보다 Redis의 공식문서 참고하는 것이 가장 좋다. 예제까지 이해하기 쉽게 잘 나와있다.

Redis의 Key

레디스는 key-value 형식으로 이루어져있으며
저장과 조회는 모두 key로 진행된다.

Key는 binary sequence로 binary-safe 하기에 어떤 문자열이나 문서든 binary로 변환한 값이기만 하면 된다.

대신,

Avoid Long Key
길이가 길지 않는 것이 좋다.
메모리 뿐만 아니라 key 비교 연산에도 좋지 않다.

Avoid Too Short Key
길이를 너무 줄이는 것도 좋지않다.
어느정도는 크게 연산이나 메모리에 차이가 없으며
key의 의미를 파악할 수 없게되면 개발이나 운영에 문제를 야기할 수 있기 때문이다.

Try to stick with schema
key 안에 의미 있는 정보가 포함되어 있다면, schema를 가지는 것도 좋다.
예를 들면, object-type:id (user:1000) 또는 ash(-), dot(.) 으로 구분한 multi word(comment:4321:reply.to)
이렇게 하면 키를 알아보기도 좋으며 형식도 일치되어 비교연산하기도 좋다.


Redis의 String

레디스는 string을 bytes 그대로 저장한다. 대신 512MB를 넘을 수 없다.

Command

  • SET 지정한 key에 원하는 value 를 저장한다. 아래는 함께 쓰일 수 있는 옵션들
    • EX seconds -- 지정한 시간(초, seconds)만큼 지난 뒤에 expire
    • PX milliseconds -- 지정한 시간(밀리초, milliseconds)만큼 지난 뒤에 expire
    • EXAT timestamp-seconds -- 지정한 시간(unix timestamp seconds)에 expire
    • PXAT timestamp-milliseconds -- 지정한 시간(unix timestamp milliseconds)에 expire
    • NX -- SETNX와 같음
    • XX -- Key가 존재하면 Set.
    • KEEPTTL -- Key가 존재하는 시간. EX, PX 류와 동시사용 불가.
    • GET -- Set하기 이전의 string을 리턴. 이전에 존재하지 않았으면 nil을 리턴. SET 이전에 존재하던 값(value)이 String이 아니면 Set을 취소하고 error를 리턴
  • SETNX 지정한 key가 존재하지 않으면, key와 value를 저장한다.
    • lock을 구현할 때 유용하다.
    • 분산 환경에서 deduplication 을 적은 비용으로 구현할 수 있다.
  • GET 지정한 key에 저장된 value를 가져온다.
  • MGET 여러개의 key에 저장된 값을 한 번에 가져온다.

Strings의 SET command로 초기 숫자를 세팅하고 시작한다.

  • INCR 지정한 key의 value를 1씩 증가시킨다.
    • counter 를 lock free로 구현할 때 유용하다.
    • rate limiter를 구현할 때도 쓸 수 있다.
  • INCRBY 지정한 key의 value로 정수형 숫자를 가지고, 그 숫자를 atomic하게 숫자를 증가/감소 시킨다.
  • INCRBYFLOAT 지정한 key의 value로 소숫점 숫자를 가지고, 그 숫자를 atomic하게 숫자를 증가/감소 시킨다.

Redis의 List

지정한 key에 value로 list를 저장할 수 있다.
왼쪽이 Head, 오른쪽이 Tail이다.

Command

  • LPUSH 새 원소를 Head에 추가한다.
  • RPUSH 새 원소를 Tail에 추가한다.
  • LPOP Head의 원소를 지우고 리턴한다.
  • RPOP Tail의 원소를 지우고 리턴한다.
  • LLEN 리스트의 길이를 리턴한다.
  • LMOVE atomic하게 한 리스트의 원소들을 다른 리스트로 옮긴다.
    • Reliable Queue를 구현하는데 사용할 수 있다. (LMOVE, LREM 이용)
    • Circular List를 만들 수 있다. (source, destination을 같은 리스트로)
  • LTRIM 지정한 range 만큼의 원소들을 남기고 나머지를 지운다.
    • -1 은 마지막 원소를 뜻한다.
    • 주의: 지우는 대상이 많을 수록 시간이 오래걸린다. O(N)
  • BLPOP
    • LPOP과 같다. 만약 리스트가 비어있다면 새로운 원소가 들어올 때까지 기다린다.(blocking) 단, timeout발생 이전까지 기다린다.
  • BLMOVE
    • LMOVE와 같다. source의 리스트가 비어있다면 새로운 원소가 들어올 때까지 기다린다.

Redis의 Set

정렬되지 않으며 unique한 데이터를 가지는 집합을 의미하는 Set도 사용가능하다.

Command

  • SADD Set 에 새로운 member를 추가한다.
  • SREM Set 에서 특정 member를 삭제한다.
  • SMEMBERS Set에 저장된 모든 member를 리턴한다.
    • O(N)의 시간복잡도를 가지고, memeber가 많은 경우 레디스에 부하를 주거나 다른 commands를 느리게할 수 있으므로 주의해야한다. SMEMBERS보다는 SSCAN을 통해서 부분조회하는 것을 권장한다.
  • SISMEMBER 주어진 String이 Set에 존재하는지 확인한다.
    • SMEMBERS 와 마찬가지로 O(N)의 시간복잡도를 가진다.
  • SINTER 주어진 두 개 이상의 Set에 모두 존재하는 member들을 리턴한다. (교집합)
    • worst case O(N*M) - N은 cardinality, M은 비교대상이 되는 Set의 갯수
  • SCARD Set의 size(cardinality)를 조회한다.

Redis의 Sorted Set

정렬 기능이 있는 Set 자료구조이다.

주로 unique한 데이터를 score에 따라 정렬할 때 사용한다.

Command

  • ZADD 새로운 member를 score 값과 함께 추가한다. 이미 존재하는 member라면 score를 업데이트 한다.
  • ZRANGE 주어진 Range에 해당하는 member들을 리턴한다.
  • ZRANK 주어진 member의 rank를 리턴한다. Ranking은 0부터 시작하는 오름차순이다. (0 = lowest)
    • ZREVRANK 0이 가장 높은 Ranking (0=highest)
  • ZCARD 주어진 key에 해당하는 memeber의 수(cardinality)를 구한다.
  • ZREMRANGEBYSCORE 주어진 key에서 주어진 min, max 이내의 (inclusive) score를 가지는 member를 삭제하고, 삭제된 memeber의 수를 리턴한다.
    • 시간 복잡도 O(log(N)+M)
      • N: sorted set에 저장된 member의 수
      • M: 지워질 대상의 member의 수
    • 위 시간복잡도 때문에 대량의 데이터가 저장되어있는 자료구조일수록 성능문제가 발생할 수 있다. 따라서 적절한 수를 유지하도록 EXPIRE 또는 ZREMRANGEBYSCORE 로 관리할 필요가 있다.
      • 10만개를 넘지 않는 것이 defacto guide.