Cache 란, 미래에 사용 될 데이터를 빠르게 조회할 수 있는 곳(물리적 장소 또는 소프트웨어로 구현된 위치)에 두고서 사용하는 것을 말한다. 보통 원본 데이터는 다른곳에 있고 그것으로부터 copy된 데이터가 Cache에 존재한다.
데이터를 최대한 효율적으로 이용해야 한다!
Cache는 임시 데이터이다. 원본이 다른 곳에 있어야 하고, 언제든 유실될 수 있음을 고려하고 시스템을 설계해야 한다.
Cache 저장소는 용량이 (상대적으로) 적고, 용량의 비용이 비싸다. (Cost-Effectiveness)
Cost-Effectiveness를 위해서 Cache hit ratio 를 측정하고 높일 수 있는 방법을 고민해야 한다. (Efficient use of data)
데이터를 디스크에 저장하지 않고, 메모리에만 저장한 뒤, 빠른 데이터 조회 및 저장을 제공하는 데이터베이스이다.
일반 데이터베이스는 디스크 IO를 기반으로 동작하고, 효율을 높이기 위해서 memory를 사용한다. 따라서 빠른 조회를 위해서는 데이터베이스가 메모리와 디스크 IO를 활용하는 방식을 이해하면서 설계와 구현을 해야한다. 이것은 어려울 뿐더러, 모든 쿼리에 항상 적용할 수있는 절대적인 규칙 같은 것이 없다.
하지만, in-memory database는 모든 데이터를 메모리에서 다루고, 저장과 조회 방식도 그것에 맞추어져 있기 떄문에 일반적으로 Disk IO 기반의 데이터베이스보다는 조회속도가 빠르다.
다만, 메모리가 디스크보다 가격이 비싸고, 메모리는 용량의 확장에도 제한이 있기 때문에 위에서 이야기한 특징을 고려해서 선택적으로 사용해야한다.
Memcached 는 key-value 형식의 데이터 저장 및 조회를 제공하는 in-memory database이다.
멀티 스레드로 데이터를 다룬다. 때문에 트래픽이 몰려도 Memcached의 응답 속도가 상대적으로 안정적이다.
내부적으로 slab allocator를 사용하고 있어서, memory fragmentation 문제가 상대적으로 적다. 단, 데이터 변경이 잦은 경우에는 fragmentation이 발생하기 쉽다.
Memcached는 한 번 저장된 후, 변경되지 않는 정보를 저장할 때 유용하다.
Redis에 비하면 메타 데이터를 적게 사용하기 때문에 메모리 사용량이 상대적으로 낮다.
메모리 단편화 (memory fragmentation)
RAM에서 메모리의 공간이 작은 조각으로 나뉘어져 사용가능한 메모리가 충분히 존재 하지만 할당(사용)이 불가능한 상태를 보고 메모리 단편화가 발생했다고 한다.
Redis 는 다양한 자료구조를 API로 제공하는 in memory database이다. (기본적으로 key-value이다)
Redis 는 싱글 스레드이다. 한 번에 하나의 client만 자료를 저장/조회 하기 때문에 데이터 lock없이 빠르게 데이터를 조회할 수 있다. 대신 트래픽이 몰리면 응답속도가 불안정한 경우가 있다.
실제 데이터의 양보다 더 많은 메모리가 필요하다. (메타데이터, CoW 사용 여부 등에 영향) In-Memory Database 종류 중에서 가장 기능이 풍부하고 레퍼런스도 많다.
원하는 경우 Disk 에 persistent 하게 데이터를 백업 가능하다. (대신 성능의 손해를 고려해야한다.)
In-Memory First Document Database 이다.(NoSQl과 유사) 데이터를 Document 단위로 저장하고, Document내의 필드도 key-value로 조회할 수 있다. Memory first이기 때문에 조회속도도 빠르고, 분산 클러스터 아키텍처로 구성되어 있어서 확장성도 좋다.
Secondary-index, SQL 등 다른 in-memory database에서 지원하지 않는 기능들도 지원한다.
Document Database 처럼 사용할 수 있지만, 특정 자료구조를 원하는 경우에는 Redis, memcached보다 느릴 수 있다.
Client 라이브러리가 충분히 효율적이지 않고, 무료 버전의 안정성과 기능에 한계가 있다.
데이터 엔지니어링에서는 대용량 트래픽을 주로 다루므로 클라이언트 요청 처리에 멀티스레드를 사용하는 memcached나, document(multi-model)을 지원하는 Couchbase가 필요할 수 있다.
하지만 다음과 같은 이유로 Redis는 In-Memory DB 선택시 가장 우선적인 고려대상이 된다.
Redis는 CacheDB의 기본적인 목표인 가장 빠른 조회속도를 내기 가장 쉽다. 자료구조의 설계와 사례가 용도에 따라 명확하기 때문에 몇 가지 주의사항만 지켜주면 일반적으로 빠른 속도를 낸다.
분산 클러스터 모드를 지원하므로 확장성도 제공할 수 있다.
자료구조와 기능이 다양하다.
Self-Managed 하기에 레퍼런스가 충분히 많고, managed service 도 여러 회사에서 지원한다.
Redis 의 모든 자료구조는 Key-value 형식이다. 저장과 조회는 key를 기준으로 한다. Key 는 binary sequence로 binary-safe하다. 즉, string(empty string포함) 이나 어떤 파일을 binary로 변환한 값이나 상관이 없다. 앞에서부터 byte단위로 비교한다. Key 설계와 관련해서 다음과 같은 것을 고려해야한다.
Key의 길이(크기)가 크다면, 메모리를 더 많이 차지할 뿐만 아니라, key 비교 연산 등에서도 비용이 많이 든다.
Key의 크기는 1K(1024bytes)가 넘지 않도록 하는 것이 좋다.
Key에 사용되는 데이터가 크다면, hashing을 통해서 key의 unique함을 보장하면서도 특정 길이로 줄이는 방법을 지향한다.
SHA1(160bit, 20bytes), SHA256(256bits, 32bytes) 등을 추천한다.
Key의 최대 사이즈는 512MB 이다. (Default 설정)
Avoid long key 가이드를 따르기 위해서 일부 사용자는 너무 짧은 키를 고안한다.
ex) user:1000:followers(user id 1000 사용자의 follower를 저장하는 자료구조)
를 표현하기 위해서 u1000fw와 같은 식으로 줄이는 경우
BUT !
메모리 면에서 크게 차이가 없음
비교연산 등에서도 큰 차이가 없음
줄인 key 의 의미를 파악할 수 없는 경우 개발, 운영 과정에서 버그나 문제를 야기할 수 있음 이것은 성능보다 더 큰 문제를 야기할 수 있음
의미를 알아보기 힘든 축약어로 한 자료구조의 키를 설계하면, Redis 클러스터를 이용하는 경우 key의 분산을 위해서 다른 키도 이와같이 설계해야 함. 그렇지 않으면 데이터의 분산이 골고루 이루어 지지 않고, 클라이언트의 요청이 한 노드에 몰려서 부하가 분산이 되지 않기 때문이다.
여러 종류의 키를 이런식으로 설계하면 결국에는 어떤 키가 어떤 의미인지 따로 문서 등이 없다면 알아보기 힘들게 되고, 이것은 심각한 버그나 문제를 야기할 수 있다. 따라서 Key가 사이즈가 크지도 않은데 일부러 축약어 등으로 과도하게 줄이는 것은 바람직하지 않다.
키안에 포함되어 있는 정보가 의미가 있다면, schema를 가지는 것은 좋은 방법이다.
object-type:id
a. user:1000
dash(-), dot(.) 으로 구분한 multi word
a. comment:4321:reply.to
b. comment:4321:reply-to
이 방법은 키의 내용과 의미가 일치하고, 비교연산에서도 같은 자리를 비교하게 되므로 비용 효율적이다.
Redis의 String 은 bytes를 그대로 저장한다. 즉, text 뿐만 아니라 어떤 object나 binary를 bytes 로 serialized 한 형태로 저장한다.
하나의 String 은 512MB를 넘을 수 없다.
SET
지정한 key에 원하는 value 를 저장한다. 아래는 함께 쓰일 수 있는 옵션들
EX
seconds -- 지정한 시간(초, seconds)만큼 지난 뒤에 expirePX
milliseconds -- 지정한 시간(밀리초, milliseconds)만큼 지난 뒤에 expireEXAT
timestamp-seconds -- 지정한 시간(unix timestamp seconds)에 expirePXAT
timestamp-milliseconds -- 지정한 시간(unix timestamp milliseconds)에 expireNX
-- SETNX와 같음XX
-- Key가 존재하면 Set.KEEPTTL
-- Key가 존재하는 시간. EX, PX 류와 동시사용 불가.GET
-- Set하기 이전의 string을 리턴. 이전에 존재하지 않았으면 nil을 리턴. SET
이전에 존재하던 값 (value)이 String이 아니면 Set을 취소하고 error를 리턴SETNX
(Not Exist) 지정한 key가 존재하지 않으면, key와 value를 저장한다.
GET
지정한 key에 저장된 value를 가져온다.
MGET
여러개의 key에 저장된 값을 한 번에 가져온다.
// Redis 서버 시작
MacBookPro ~ % brew services start redis
// Redis 서버 접속
MacBookPro ~ % redis-cli
// mykey를 "abcd"로 설정
127.0.0.1:6379> set mykey "abcd"
OK
// my key 가져오기
127.0.0.1:6379> get mykey
"abcd"
// mykey를 10초 동안 "xxx"로 설정
127.0.0.1:6379> set mykey "xxx" ex 10
OK
// mykey 가져오기
127.0.0.1:6379> get mykey
"xxx"
// 10초 뒤 가져오기
127.0.0.1:6379> get mykey
(nil)
지정된 key에 대해서 숫자를 atomic(한번에 하나의 key만)하게 증가/감소 시키고 그 결과를 응답한다. 분산환경에서 덧셈 뺄셈 연산을 lock free로 구현할 때 많이 쓰인다.
INCR
지정한 key의 value를 1씩 증가시킨다.
INCRBY
지정한 key의 value로 정수형 숫자를 가지고, 그 숫자를 atomic하게 숫자를 증가/감소 시킨다.
INCRBYFLOAT
지정한 key의 value로 소숫점 숫자를 가지고, 그 숫자를 atomic하게 숫자를 증가/감소 시킨다.
// counter 를 0 으로 SET
127.0.0.1:6379> set counter 0
OK
127.0.0.1:6379> get counter
"0"
// increment 1 counter
127.0.0.1:6379> incr counter
(integer) 1
127.0.0.1:6379> get counter
"1"
// increment 1 counter
127.0.0.1:6379> incr counter
(integer) 2
127.0.0.1:6379> get counter
"2"
// counter -3 증가
127.0.0.1:6379> incrby counter -3
(integer) -1
127.0.0.1:6379> get counter
"-1"
지정한 Key에 리스트를 저장할 수 있다.
왼쪽(Left)이 Head, 오른쪽(Right)이 Tail 이다.
리스트에 들어갈 수 있는 최대 갯수는 2^32 - 1 (4,294,967,295)개 이다.
LPUSH
새 원소를 Head에 추가한다.
RPUSH
새 원소를 Tail에 추가한다.
LPOP
Head의 원소를 지우고 리턴한다.
RPOP
Tail의 원소를 지우고 리턴한다.
LLEN
리스트의 길이를 리턴한다.
LMOVE
atomic하게 한 리스트의 원소들을 다른 리스트로 옮긴다.
LTRIM
지정한 range 만큼의 원소들을 남기고 나머지를 지운다.
BLPOP
LPOP
과 같다. 만약 리스트가 비어있다면 새로운 원소가 들어올 때까지 기다린다. (blocking) 단, timeout발생 이전까지 기다린다.BLMOVE
LMOVE
와 같다. source의 리스트가 비어있다면 새로운 원소가 들어올 때까지 기다린다.Redis에서 Set 자료구조를 표현할 수 있다.
정렬되지 않은 unique string 데이터(member)에 대한 집합을 저장할 때 쓴다.
대부분 command의 시간복잡도가 O(1) 이지만, 그렇지 않은 command의 경우 주의해서 사용해야한다.
하나의 Set에 들어갈 수 있는 member의 사이즈는 최대 2^32 - 1 (4,294,967,295) 이다.
SADD
Set 에 새로운 member를 추가한다.
SREM
Set 에서 특정 member를 삭제한다.
SMEMBERS
Set에 저장된 모든 member를 리턴한다.
SISMEMBER
주어진 String이 Set에 존재하는지 확인한다.
SINTER
주어진 두 개 이상의 Set에 모두 존재하는 member들을 리턴한다. (교집합)
SCARD
Set의 size(cardinality)를 조회한다.