1. 미들웨어이야기/06. redis

rdb 테이블을 redis로

ioseph 2018. 6. 16. 00:47

관계형 테이터베이스 테이블을 redis로 옮기기


1. redis 자료형

  • 문자열, String: 일반적인 key - value, hash로 보면 된다. range, between 이런 것 없다. 그냥 오직 그 key
  • 목록, List: 그냥 배열, push & pop 용, 순서가 색인, 중복된 value, 목록 이름이 key. 하나의 목록을 샤딩할 수는 없다.
  • 집합, Set:: 중복된 value를 가질 수 없는 집합 - RDB 인덱스의 value가 된다. insert = sadd, delete = srem , index key update = smove
  • 해시, hash: RDB의 테이블, key가 테이블이름, field가 Primary Key, value가 json 칼럼들값들, 하나의 해시는 샤딩할 수 없다.
  • 정렬된 집합, sorted set: btree 인덱스 구현용, 하나의 집합을 샤딩할 수 없다. 아래에서 설명
여기서 보듯이 scale out 관점에서 문자열 자료형(key-value)을 제외하고는 모두 scale out에 유연하지 않다. 즉, 문자열 자료형을 외 자료형을 사용한다면, 분산 처리는 응용프로그램에서 독자적으로 구현해야한다. 이 부분에 대해서는 좀 더 고민해 보고 다음 글에서 다루기로 하겠다.

2. RDB 테이블을 redis로.

위에서 살펴 보았듯이, 하나의 테이블은 하나의 해시로 만들면 된다.

문제는 하나의 해시는 redis 서버에서 안쓰면 버린다는 expire 설정을 하는 것도 비효율적이다. 버렸는데, 다시 필요하다면, RDB 에서 테이블 전체를 다시 가져와야하기 때문이다.
즉, 어느 테이블을 redis 서버로 가져올 것인가는 결국 그 redis 서버에서 사용할 수 있는 메모리 크기와 직접적으로 관련된다. swap 메모리를 사용해 가면서까지 굳이 RDB 테이블을 모두 redis로 가져오겠다는 생각은 안하는 것이 좋을 것이다. 

여기서는 커뮤니티 게시판을 서비스를 대상으로 rdb를 redis로 옮기는 것을 살펴보고자 한다.

기존 rdb의 erd는 다음과 같다.


2.1. 사용자들 테이블

사용자들 테이블은 그룹들 테이블과 M:M 관계이다. redis와 같은 key-value 형태의 데이터베이스에서는 이런 다대다 관계는 문자열 키로 값은 그냥 json array로 푸는 것이 제일 편하다.

SET 사용자들:1 '{"로그인ID": "gildong", "이름": "홍길동", "비밀번호": sha256(passwd), "이메일": "user@gmail.com", "사용여부": "Y", "groups"=[1,2,3]}'

그룹들 테이블은 단순하게,

SET 그룹들:1 관리자
SET 그룹들:2 일반사용자
SET 그룹들:3 공동저자
....
이런 형태로 문자열 키로 지정할 수도 있고,

HSET 그룹들 1 관리자
HSET 그룹들 2 일반사용자
....
이런 형태의 hash 테이블 형태로 구현할 수도 있다.

redis 서버의 key 관리 측면에서는 hash 테이블 형이 유용하겠지만, 훗날 분산환경을 위해서는 위의 문자열 키 형태가 더 유용하다.

한편, 관리 차원에서 전체 회원수를 알기 위해서,

SCAN 0 MATCH "사용자들:*" count 100000000000

이런 형태의 명령을 사용하는 것은 정말 위험한 짓이다. 이런 경우를 대비에서 "totalcount::사용자들" 같은 문자열 키를 하나 할당하고, 그 값으로 전체 사용자 수를 기록하고 있는 것이 바람직하다. 이 값은 회원 가입이 있을 때, INCR, 탈퇴할 때, DECR 명령을 이용해서 증,차감 하면 될 것이다.

2.1.1. 사용자들 테이블의 인덱스들

redis에서 그 키에 대한 값을 구할 때, '=' 연산만 가능하다.
즉 >, < , between 같은 연산이 필요하다면, 하나의 정렬된 집합 키(RDBMS의 인덱스가 된다)를 만들고, ZRANGE 명령을 이용해야 한다.

사용자들 테이블에는 총 3개의 인덱스가 정의되어있다.

하나는 기본키이며, 이 키를 사용해서는 오직
= 연산으로만 검색하겠다고 결정해서, 사용자들:1, 사용자들:2... 같은 문자열키를 사용했다.
즉,
SELECT * FROM 사용자들 WHERE 사용자번호 BETWEEN 100 AND 200
같은 쿼리를 사용하지 않겠다는 것이 전제 되어 있다.

두번째 인덱스는 로그인ID에 대한 유니크 인덱스이다.
로그인ID로 검색하는 것을 오직 = 연산만 허용하겠다면,
앞에서 언급한 HSET을 이용한 hash 테이블로도 충분하다.

HSET 사용자들_로그인ID_색인 a@emai.com 1

이 hash 테이블의 키는 email, 값은 사용자 번호가 될것이다.

즉,
SELECT * FROM 사용자들 WHERE 이메일 = 'a@email.com'
형태의 쿼리 작업을 redis로 푼다면,

다음 절차를 거친다.
  1. HGET 사용자들_로그인ID_색인 a@email.com
  2. GET 사용자들:1
한편,
SELECT * FROM 사용자들 WHERE 이메일 like 'a@email%'
형태의 쿼리를 인덱스를 사용해서 처리하고자 한다면,
상황은 좀더 복잡해진다.

이것을 구현하려면, redis 정렬된 집합을 사용해야한다.

ZADD 사용자들_이메일_색인 1 a@email.com
형태로 색인자료를 만드며, email 값 앞에 있는 1은 사용자번호다.

그리고, 이 색인의 범위검색을 하려면, '[', '(' 문자를 추가해서 범위 검색을 한다.
ZRANGEBYLEX 사용자들_이메일_색인 '[a@email' '(a@emaim'
형태로 사용한다.
SQL 구문으로 바꾸면,
WHERE 이메일 >= 'a@email' and 이메일 < 'a@emiam'
이 될것이다.
즉, 이메일 like 'a@email%' 는 위와 같이 변환해서 사용해야한다.

찾으려고 하는 값인 한글인 경우는 좀 더 복잡해진다.

where 이름 like '대한민%'
이라면, '[대한민' '(대한믽' 이 될것이다. 
마지막 한 글자의 unicode 다음 글자다. 어떻게 구하는지는 스스로 공부를.

이렇게 해서, 정렬된 집합을 이용한 색인 검색이라면, 다음 절차를 거친다.
  1. ZRANGE|ZRANGEBYSCORE|ZRANGEBYLEX 사용자들_이메일_색인 최소값 최대값
  2. 위명령 결과의 배열로 해서,
    for 값 in 값배열
        ZSCORE 사용자들_이메일_색인 값
    형태로 또 사용자 번호 배열을 만들고,
  3. for 사용자번호 in 사용자번호배열
       GET 사용자들:사용자들번호
형태의 복잡한 작업을 해야한다.
물론 2번 작업을 단순화 하기 위해서, 앞에서 이야기한 이메일 기준 hash 테이블도 만들고, 그것을 조회하는 것도 한 방법일 것이다.

세번째 인덱스인 로그인ID에 대한 유니크 인덱스다.
이 인덱스도 email 인덱스 처리방식과 똑같다.

2.2 게시판 테이블

게시판 테이블도 사용자 테이블과 크게 다르지 않기 때문에 설명은 생략하고,
여기서는 드디어 작성일시 기준 비유니크 인덱스를 구현해야 한다.
즉, 같은 작성일시에, 여러개의 게시물이 존재할 수 있음을 가정한 것이다.

(쓸데 없이 옆길로 샌 이야기: 단일 redis 서버를 사용하고, 위 모델링을 실재 구현한다면,
게시물의 작성일시는 절대로 같을 수 없다. 왜냐하면, redis 서버는 단일 프로세스 단일 쓰레드로 사용자 명령을 처리하기 때문에, 모든 명령이 직렬화 된다. 즉 같은 시간이 있을 수 없다. 하지만 사용자 편의를 위해 작성일시를 초단위까지만 사용하겠다면, 충분히 같은 작성일시에 여러개의 게시물이 있을 수 있다)

이렇게 비유니크 인덱스도 정렬된 집합을 사용해서 구현한다.

2.2.1. 게시물 작성일시 색인

redis에서는 시간자료형이 없다. 작성일시 값은 결국 문자나 숫자로 저장되어야한다.
2018-07-01 12:30:25 형태의 문자로 저장하든가, 이 시간의 unixtimestamp 값으로 저장한다.

문자로 저장한다면, ZRANGEBYLEX로 범위검색을 할것이고, 숫자로 저장한다면,
ZRANGEBYSCORE로 검색하면 된다.

작성일시를 문자열로 처리하는 경우
ZADD 게시판_작성일시_색인 1 '2018-07-01 12:30:25'
형태가 될것이고, 앞에서 다룬 것과 똑같이 1은 게시판 기본키인 게시물번호다.

작성일시를 숫자로 처리한다면,
ZADD 게시판_작성일시_색인 1530621697 1
형태가 될 것이고, 1530621697 숫자는 unixtimestamp 값이고, 뒤에 1은 게시물번호가 된다.

이 색인을 이용한 =, >, <, >=, <=, between 연산은 직접 구현해야한다.


2.3 태그 테이블

태그 테이블은 단순하게 생각하면, 집합을 사용하면 된다.

SADD 태그:서울 1
SADD 태그:서울 5
SADD 태그:서울 100
....
서울이라는 태그를 지정한 모든 게시물 번호를 찾으려면,
SMEMBERS 태그:서울
형태로 게시물 번호를 요소로 하는 배열을 만들고,
그 배열의 각 요소를 기본키로해서 게시판:게시물번호 문자열 키에 대한 값을 구하면 된다.

여기서 중요한 것은 SMEMBERS는 현재, limit offset count 기능이 아직까지는 구현되지 않았기에,
한 태그의 값인 게시물번호를 모두 찾아 응용프로그램에게 넘겨준다.
그렇기 때문에, 한 키에 대한 SMEMBERS의 결과값이 많으면 많을수록 이 명령의 성능이 더 떨어진다.

3. 마무리

INSERT INTO t VALUES (.....)
=> SET t:pk 'json'

SELECT * FROM t where pk_column = pk_value
=> GET t:pk_value

SELECT * FROM t WHERE int_column >= start_value and int_column <= end_value
=> ZRANGEBYSCORE t_int_column_index start_value end_value
     loop primary_keys
        GET t:pk_value

SELECT * FROM t WHERE text_column LIKE 'find_str%'
=> ZRANGEBYLEX t_text_column_index '[find_str' '(find_sts'
     loop primary_keys
       GET t:pk_value

SELECT t.* FROM t, tags WHERE t.pk = tags.pk_value AND tags.tag = 'tag_value'
=> SMEMBERS tags tag_value
     loop pk_values
       GET t:pk_value


- posted by 김상기

'1. 미들웨어이야기 > 06. redis' 카테고리의 다른 글

redis 보안  (0) 2018.10.29
redis 대량 데이터 입력  (0) 2018.10.17
지리정보와 redis  (0) 2018.07.30