quicoment / qc-mq-processing-server

qc-mq-processing-server
0 stars 0 forks source link

redis에 댓글을 저장하는 방법 #7

Closed sypark9646 closed 3 years ago

sypark9646 commented 3 years ago

기존 방식의 경우 게시물 당 comments 를 가져오기 쉽지 않은 설계인 것 같네요..

// POST in MySQL //
{
  "id": postID1,
  "title": "title"
  "content": "content",
  "password": "password",
  ...
}

// Comments in Redis //

"commentID1": {
  "id": commentID1,
  "postId": postID1,
  "content": "content",
  ...
}

"commentID2": {
  "id": commentID2,
  "postId": postID1,
  "content": "content",
  ...
}

"commentID3": {
  "id": commentID3,
  "postId": postID1,
  "content": "content",
  ...
}
AMYMEME commented 3 years ago

음 저도 레디스 설계 해보고 코멘트 달겠습니다

AMYMEME commented 3 years ago

찾아보니 redis가 파이어베이스처럼 url 형식의 collection을 가지고 있지는 않네요.. 댓글 공감 기능순으로 정렬한다면 sorted set을 사용하면 편할 것 같은데 좀 더 찾아보겠습니다

sypark9646 commented 3 years ago

아하 지금 저장 방식은 그대로 사용하고 게시글 postID = postID1 에 해당하는 commentID 모음을 sorted set으로 만들어 두자는 말씀이시죠??

"postID1": { "commentID1", "commentID2", "commentID3"} // sorted set

"commentID1": {
  "id": commentID1,
  "postId": postID1,
  "content": "content",
  ...
}

"commentID2": {
  "id": commentID2,
  "postId": postID1,
  "content": "content",
  ...
}

"commentID3": {
  "id": commentID3,
  "postId": postID1,
  "content": "content",
  ...
}
sypark9646 commented 3 years ago

생각해보니.. redis 에 key pattern으로 get 하는 기능이 있었네요 수정 기능 구현 시에는 set "postID1:commentID1" ~~ 이런식으로 하고 게시물에 해당하는 댓글들을 가져올땐 get postID1:[A-Za-z0-9]* (이 정규표현식이 맞는지는 모르겠지만..ㅎㅎ) 이런식으로 하면 될 것 같아요!

"postID1:commentID1": {
  "id": commentID1,
  "postId": postID1,
  "content": "content",
  "password": "password1",
}

"postID1:commentID2": {
  "id": commentID2,
  "postId": postID1,
  "content": "content",
  "password": "password2",
}

"postID1:commentID3": {
  "id": commentID3,
  "postId": postID1,
  "content": "content",
  "password": "password3",
}

"commentID1": 10
"commentID2": 15
"commentID3": 23

그리고 공감 처리는 따로 INCR commentID1 이런식으로 할 수 있도록 따로 뺄려고 하는데 어떠신가요? (그런데 이렇게 되면 댓글 정보를 가져올때 따로 처리를 해줘야 하는게 있어서 귀찮으려나..)

sypark9646 commented 3 years ago

앗 그리구 commentID 는 qc-server에서 만들어서 주는 것 맞나용??

AMYMEME commented 3 years ago

앗 그리구 commentID 는 qc-server에서 만들어서 주는 것 맞나용??

엇 아뇨 snoflake가 go이길래 go에서 하는 줄 알았습니다ㅜㅠ 저희쪽에서 만들어서 드릴까요?

sypark9646 commented 3 years ago

엇 아뇨 snoflake가 go이길래 go에서 하는 줄 알았습니다ㅜㅠ 저희쪽에서 만들어서 드릴까요?

아니용!!ㅋㅋㅋㅋㅋ go 에서 해도 됩니다!! 혹시나 여쭤봤어요

AMYMEME commented 3 years ago

엇 아뇨 snoflake가 go이길래 go에서 하는 줄 알았습니다ㅜㅠ 저희쪽에서 만들어서 드릴까요?

아니용!!ㅋㅋㅋㅋㅋ go 에서 해도 됩니다!! 혹시나 여쭤봤어요

네넵

AMYMEME commented 3 years ago

지금 저장 방식은 그대로 사용하고 게시글 postID = postID1 에 해당하는 commentID 모음을 sorted set으로 만들어 두자는 말씀이시죠??

말씀해주신 지금의 방식이라는 게 hash를 사용하는 방식인가요? 일단은 저걸 말한 건 맞습니다. 처음에는 저는 post id를 key로 두고 value는 comment id만 저장해서 sorted set으로 저장하면서 동시에 hash에 comment 내용들을 넣는 방식을 생각했습니다.

postId가 키여서 postId당 댓글 id를 가져오는 건 편해도, 어차피 hash에서 commentId 일일이 가져올 바에야 말씀해주신 key pattern 방식이 차라리 나을 것 같기도 합니다

그리고 comment id 생성 시에 BOF가 날거라 생각했는데, 찾아보니 golang의 uint64 값이 꽤 커서 걱정안해도 될 수준이긴 하네요.. uuid생성하셔도 되고 uint64로 해도 될것 같습니다!

그래서 필드 패턴은 post:${post-id}:comment:${comment-id}가 괜찮다고 생각했습니ㅣ다..만


패턴 매칭이 시간이 얼마나 걸릴지 모르겠네요.. KEYS 커맨드는 모두 스캔해야하기 때문에 거의 금기되어있는 것 같고, 키 패턴 매칭에 쓰이는 HSCAN도 페이지 단위로 읽어서 일정 개 이상의 comment를 가져오지 않으면 다음 커서를 읽어야 할 수도 있을 것 같고.. 무엇보다 hash 에 json 전체를 저장한다면 공감이나 시간 순 정렬을 어떻게 할 수 있을지 모르겠네요

 # post 1에 comment 3개 넣기 (대충 comment id 1,2,3으로 설정)
(redis-cli)> hmset quicoment post:1:comment:1 "{\"timestamp\": \"2021-08-00 00:00:00.000000\", \"postId\": 1,  \"id\": 1,  \"content\": \"content\",  \"password\": \"password1\" }" post:1:comment:2 "{\"timestamp\": \"2021-08-01 00:00:00.000000\", \"postId\": 1,  \"id\": 2,  \"content\": \"content\",  \"password\": \"password2\" }"  post:1:comment:3 "{\"timestamp\": \"2021-08-05 00:00:00.000000\", \"postId\": 1,  \"id\": 3,  \"content\": \"content\",  \"password\": \"password3\" }" 
# post 2에 comment 2개 넣기 (대충 comment id 1,2,3으로 설정)
(redis-cli)> hmset quicoment post:2:comment:1 "{\"timestamp\": \"2021-08-01 00:00:00.000000\", \"postId\": 2,  \"id\": 1,  \"content\": \"content\",  \"password\": \"password1\" }" post:2:comment:2 "{\"timestamp\": \"2021-08-05 00:00:00.000000\", \"postId\": 2,  \"id\": 2,  \"content\": \"content\",  \"password\": \"password2\" }" 
# post 1의 comment 모두 불러오기
(redis-cli)> hscan quicoment 0 match post:1:comment*
# output
1) "0"
2) 1) "post:1:comment:3"
   2) "{\"timestamp\": \"2021-08-05 00:00:00.000000\", \"postId\": 1,  \"id\": 3,  \"content\": \"content\",  \"password\": \"password3\" }"
   3) "post:1:comment:1"
   4) "{\"timestamp\": \"2021-08-01 00:00:00.000000\", \"postId\": 1,  \"id\": 1,  \"content\": \"content\",  \"password\": \"password1\" }"
   5) "post:1:comment:2"
   6) "{\"timestamp\": \"2021-08-00 00:00:00.000000\", \"postId\": 1,  \"id\": 2,  \"content\": \"content\",  \"password\": \"password2\" }"

이렇게 키패턴 검색이 가능하긴 한데.. 따로 sorted set을 만들지 않는 이상 json을 통째로 저장하면 hash에서의 sort로 정렬이 가능할지는 잘 모르겠네요


음 그래서 계속 생각해봤는데 모든 댓글을 하나의 해시(quicoment라는 키)에 저장하기 때문에 패턴검색을 쓰기도 하고, 어차피 게시물이 삭제된다면 관련 댓글도 지워야 하는데, del key를 하면 모든 value를 지울 수 있으니까 하나의 키에 하나의 post를 대응하는 게 더 좋을 것 같기도 합니다.

고민끝에 sorted set(zset)을 두 개 이용하는건 어떨까요?? post:1:like에는 score를 공감순으로 놓고, score는 float 타입이어야 해서post1:timestamp는 timestamp를 localdatetime이 아닌 long타입의 값으로 바꿔(제가 전달하겠습니다) score로 저장하면..

post:1:like

# zset에 post 1 comment 4개 저장
(redis-cli) > zadd post:1:like 2 "{\"timestamp\": \"2021-08-05 00:00:00.000000\", \"id\": 1, \"content\": \"content1\", \"password\": \"password1\"}"
(redis-cli) > zadd post:1:like 0 "{\"timestamp\": \"2021-08-05 00:00:01.000000\", \"id\": 2, \"content\": \"content2\", \"password\": \"password2\"}"
(redis-cli) > zadd post:1:like 0 "{\"timestamp\": \"2021-08-05 00:00:04.000000\", \"id\": 3, \"content\": \"content3\", \"password\": \"password3\"}"
(redis-cli) > zadd post:1:like 3 "{\"timestamp\": \"2021-08-05 00:01:04.000000\", \"id\": 4, \"content\": \"content4\", \"password\": \"password4\"}"
# 공감 순 정렬
(redis-cli) > zrevrange post:1:like 0 -1 with score
# output 
1) "{\"timestamp\": \"2021-08-05 00:01:04.000000\", \"id\": 4, \"content\": \"content4\", \"password\": \"password4\"}"
2) "3"
3) "{\"timestamp\": \"2021-08-05 00:00:00.000000\", \"id\": 1, \"content\": \"content1\", \"password\": \"password1\"}"
4) "2"
5) "{\"timestamp\": \"2021-08-05 00:00:04.000000\", \"id\": 3, \"content\": \"content3\", \"password\": \"password3\"}"
6) "0"
7) "{\"timestamp\": \"2021-08-05 00:00:01.000000\", \"id\": 2, \"content\": \"content2\", \"password\": \"password2\"}"
8) "0"

# 공감 순 정렬, 3개까지만
(redis-cli) > zrevrange post:1:like 0 2
# output
1) "{\"timestamp\": \"2021-08-05 00:01:04.000000\", \"id\": 4, \"content\": \"content4\", \"password\": \"password4\"}"
2) "{\"timestamp\": \"2021-08-05 00:00:00.000000\", \"id\": 1, \"content\": \"content1\", \"password\": \"password1\"}"
3) "{\"timestamp\": \"2021-08-05 00:00:04.000000\", \"id\": 3, \"content\": \"content3\", \"password\": \"password3\"}"

post:1:timestamp

# zset에 post 1 comment 3개 저장
(redis-cli) > zadd post:1:timestamp 1628089200 "{\"timestamp\": \"2021-08-05 00:00:00.000000\", \"id\": 1, \"content\": \"content1\", \"password\": \"password1\"}"
(redis-cli) > zadd post:1:timestamp 1628089201 "{\"timestamp\": \"2021-08-05 00:00:01.000000\", \"id\": 2, \"content\": \"content2\", \"password\": \"password2\"}"
(redis-cli) > zadd post:1:timestamp 1628089204 "{\"timestamp\": \"2021-08-05 00:00:04.000000\", \"id\": 3, \"content\": \"content3\", \"password\": \"password3\"}"
# 시간 순 정렬
(redis-cli) > zrevrange post:1:timestamp 0 -1
# output 
1) "{\"timestamp\": \"2021-08-05 00:00:04.000000\", \"id\": 3, \"content\": \"content3\", \"password\": \"password3\"}"
2) "{\"timestamp\": \"2021-08-05 00:00:01.000000\", \"id\": 2, \"content\": \"content2\", \"password\": \"password2\"}"
3) "{\"timestamp\": \"2021-08-05 00:00:00.000000\", \"id\": 1, \"content\": \"content1\", \"password\": \"password1\"}"
AMYMEME commented 3 years ago

음 근데 그러면 시간 정렬 때 공감 수가 안보이긴 하네요..

sypark9646 commented 3 years ago

고민이네요.. get 하기 쉬워지려면 update할게 많아지고, update 하기 쉽게 설계하려면 get 할때 따로 처리가 필요하고ㅠㅠ

패턴 매칭이 시간이 얼마나 걸릴지 모르겠네요.. KEYS 커맨드는 모두 스캔해야하기 때문에 거의 금기되어있는 것 같고, 키 패턴 매칭에 쓰이는 HSCAN도 페이지 단위로 읽어서 일정 개 이상의 comment를 가져오지 않으면 다음 커서를 읽어야 할 수도 있을 것 같고..

앗 맞아요 커서를 두고 쓰더라구요..ㅠㅠ

무엇보다 hash 에 json 전체를 저장한다면 공감이나 시간 순 정렬을 어떻게 할 수 있을지 모르겠네요

제가 생각했던 방식은

음 근데 그러면 시간 정렬 때 공감 수가 안보이긴 하네요..

저는 일단 공감 수는 자주 바뀔 수 있는 값이라 따로 빼는게 좋지 않을까 싶은 생각이 들어요. 정렬한 commentID에 해당하는 공감수는 따로 가져와서 조합하는게 낫지 않을까 하는 생각이 드는데 어떻게 생각하시나요?,?

sypark9646 commented 3 years ago

앗 nosql 이 읽기작업보다(join) 쓰기작업이 효율적이어서 commentID 만 저장하는 방법보다는 객체 그 자체를 저장하는 방법을 생각하신것인가요? (그런데 업데이트 하는 경우 단순 쓰기작업이 아니라 읽기+쓰기 의 조합이긴 하네요)

정렬한 commentID에 해당하는 공감수는 따로 가져와서 조합하는게 낫지 않을까 하는 생각이 드는데 어떻게 생각하시나요?,? => 이 방식은 안좋을수도 있겠네요,,,

sorted set(zset)을 두 개 이용하는건 어떨까요??

제일 최선인 방법인 것 같습니다..!! timestamp 는 안주셔도 될 것 같아요 snowflake id로 대체할 수 있어보입니당 업데이트할 때 트랜잭션 처리를 잘 해야겠군용...

AMYMEME commented 3 years ago

음 근데 갑자기 생각난건데, 저번에 댓글 공감 기능을 한 명만 누를 수 있게 하기 위해 comment:{commentId}:like set에 회원id를 넣는 방식을 논의했던 것 같은데, 제가 헷갈리는 건가 싶네요

앗 nosql 이 읽기작업보다(join) 쓰기작업이 효율적이어서 commentID 만 저장하는 방법보다는 객체 그 자체를 저장하는 방법을 생각하신것인가요?

ㅜㅜ그건 아니라 객체를 저장하지 않고 comment-id만 저장한 후에 comment 내용을 몽땅 가져올 수 있으면 좋을 것 같은데요.. 딱히 다른 방식이 생각나지 않아서요..

따로 레디스에 저장했다가 ttl을 설정해서 일정 시간이 지나면 db같은 곳에 저장하는 방식은 좀 복잡할까요?? 계속 늘어나는 comment양을 redis가 감당할 수 있을지 모르겠네요 😢

sypark9646 commented 3 years ago

음 근데 갑자기 생각난건데, 저번에 댓글 공감 기능을 한 명만 누를 수 있게 하기 위해 comment:{commentId}:like set에 회원id를 넣는 방식을 논의했던 것 같은데, 제가 헷갈리는 건가 싶네요

네 그렇게 할까 했었는데 그러면 세션이나 로그인 기능을 구현해야할 것 같아서요..!! 저는 이 방식도 괜찮습니당

따로 레디스에 저장했다가 ttl을 설정해서 일정 시간이 지나면 db같은 곳에 저장하는 방식은 좀 복잡할까요??

좋아요!! 저도 어제 찾아보니까 ttl 설정하고 다른 DB에 스냅샷 넘기는 방식이 좋을 것 같았어용 아직 sync쪽은 서치하지 못했지만,,, 그런데 궁금한 게 있는데 만약 레디스에 있는 정보를 mysql로 백업하면 댓글 리스트를 가져올땐 mysql을 사용하고 자주 바뀌는 값(공감)은 레디스에서 가져오는 것인가요?.?

AMYMEME commented 3 years ago

그런데 궁금한 게 있는데 만약 레디스에 있는 정보를 mysql로 백업하면

댓글 리스트를 가져올땐 mysql을 사용하고 자주 바뀌는 값(공감)은 레디스에서 가져오는 것인가요?.?

댓글 페이지네이션 할 때 레디스를 사용하는 식이 되지 않을까요? 자주바뀌는 공감 score도 레디스에서 가져오면서 뷰에 20개씩 보여준다면 스코어 높은 순으로 스코어 값과 20개의 comment id들만 레디스에서 가져오고, 그 20개 코멘트의 static한 내용은 db에서 가져온다고 이해했습니다 저는..!

sypark9646 commented 3 years ago

앗 그렇군요..!!

그럼 정리하자면 말씀하신대로 sorted set(zset)을 아래 두 가지를 사용하도록 하고, 일정 시간마다 mysql에 백업을 한다.

AMYMEME commented 3 years ago

음.. sorted set key를 post:{postID}:comment:{commentID}이걸로 쓰면 post별로 가져올 때는 패턴 매칭으로 하나요?

그리고 post:{postId}:like를 사용하면 post 별 댓글을 가져올 때 공감 순 정렬을 못하지 않나요??

AMYMEME commented 3 years ago

저는 sorted set 한개는 post:{postID}:comment:score로 commentid만 value로 저장하고, comment id 만 뽑아서 db쿼리를 날리자는 거였고,(score는 db 캐시를 해도 될 것 같긴 합니다.)

나머지 하나는 post:{postID}:comment:timestamp이였는데 사실 create time에 따른 순서만 보장되면 된다면, sorted set이 아닌 그냥 list를 사용하는 게 메모리 절약 측에서는 좋아보입니다.

그리고 추가적인 기능으로 혹시 또 공감한 사람 공감 못하게 하기 위해서는 comment:{commentId} set이 있으면 좋을 것 같다고 얘기한 거엿습니다!

sypark9646 commented 3 years ago

앗 키를 post:{postID}:comment:score 이걸로 주자는 의미였나요?? 키는 post:{postId}:like 이걸로 하되 score을 따로 주자는 말씀인줄 알았어요

sypark9646 commented 3 years ago

나머지 하나는 post:{postID}:comment:timestamp이였는데 사실 create time에 따른 순서만 보장되면 된다면, sorted set이 아닌 그냥 list를 사용하는 게 메모리 절약 측에서는 좋아보입니다.

이 경우는 아래와 같이 저장하자는 말씀이신가요??

"post:{postID}:comment": [
     {comment1 정보},
     {comment2 정보}
]

이렇게 되면 그런데 댓글 update 요청을 처리 할때는 리스트를 다 가져와서 아이디에 맞는걸 찾아와야하는 것 같은데 비효율적이지 않을까요?!

AMYMEME commented 3 years ago
"post:{postID}:comment": [ comment1ID, comment2ID, comment3ID]

를 원래 말했던 거였는데, 사실 시간 순 정렬은 최근 댓글 순으로 보여주는 거고 최근 댓글이 빨리 업데이트 될때만 캐시 용으로 잠깐 저장하면 되는 거라 저렇게 정보를 저장해도 될 것 같아요!

AMYMEME commented 3 years ago

댓글 update 요청을 처리 할때는 리스트를 다 가져와서 아이디에 맞는걸 찾아와야하는 것 같은데

음 그래서 comment id들만 저장하고 실제 정보는 post:{postid}:comment:cache같은 곳에 캐시용으로 저장하면 어떨까 생각했습니다.

id만 저장하면 update 시에는 id list가 바뀔 일이 없지 않나요? 아님 update하면 시간이 바뀌니 id도 바뀌나요?

sypark9646 commented 3 years ago

아하 "post:{postID}:comment": [ comment1ID, comment2ID, comment3ID]는 만료되지 않는 값이고 post:{postid}:comment:cache(?) 에 comment 정보가 있으면 레디스에서, 없으면 RDB에서 가져오는군요

혹시 post:{postid}:comment:cache 이건 어떻게 저장되는걸 생각하고 계신것인지 알 수 있을까요??

sypark9646 commented 3 years ago

그럼 최종적으로 레디스에 저장될 정보

가 맞을까요?

AMYMEME commented 3 years ago

혹시 post:{postid}:comment:cache 이건 어떻게 저장되는걸 생각하고 계신것인지 알 수 있을까요??

ㅜㅜ제가 노트북을 꺼버려서 폰으로 쓰고 있느라 좀 느리네요.

그냥 정말 단순히 캐시의 의미로 쓴건데 comment:commentId:cache가 의미상 더 맞는 것 같아요.

최근 들어온 댓글들을 저런형태로 짧은 ttl과 함께 넣어주고, 공감을 많이 받은 댓글도 좀 도 긴 ttl과 함께 저런 키로 만들어 두면 db에 다녀오는 횟수가 좀 줄지 않을까 싶어서요.

그럼 최종적으로

네 맞습니다! 위에 언급한 캐시 빼고는 키 전체를 삭제하면 삭제했지 다 만료되지 않을 정보인 것 같아요

sypark9646 commented 3 years ago

아하 이해했습니다!! 감사합니다 :)

AMYMEME commented 3 years ago

저도 첨 설계해보는 거라 더 좋은 의견 생각나시년 언제든 코멘트 올려주세요!

sypark9646 commented 3 years ago

저장 패턴 https://yunpengn.github.io/blog/2019/05/04/consistent-redis-sql/