Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

영속화에 red lock 및 트랜잭션 적용 #350

Merged
merged 11 commits into from
Dec 1, 2024

Conversation

ezcolin2
Copy link
Collaborator

@ezcolin2 ezcolin2 commented Dec 1, 2024

🔖 연관된 이슈

📂 작업 내용

redis, page 연산에 RedLock 적용

스케줄러가 redis의 값을 데이터베이스에 영속화하는 과정은 다음과 같습니다.

  1. redis에서 해당 페이지의 title과 content를 가져온다.
  2. postgres에 해당 페이지의 title과 content를 갱신한다.
  3. redis에서 해당 페이지 정보를 삭제한다.

그런데 만약 위 과정이 시작된 후 1번을 진행하고 2번을 시작하기 전에 해당 페이지 정보를 postgres에 삭제하면 문제가 발생하게 됩니다.

그래서 위 과정이 일어나는 동안 redis와 page 정보를 변경하는 것을 막기 위해 RedLock을 적용했습니다.

redis service와 page service의 조회를 제외한 메소드에 RedLock을 적용하여 lock을 획득했을 때만 연산을 진행하도록 구현했습니다.

redis.service.ts

  async setField(key: string, field: string, value: string) {
    // 락을 획득할 때까지 기다린다.
    const lock = await this.redisLock.acquire([`user:${key}`], 1000);
    try {
      return await this.redisClient.hset(key, field, value);
    } finally {
      lock.release();
    }
  }

transaction 적용

  1. redis에서 해당 페이지의 title과 content를 가져온다.
  2. postgres에 해당 페이지의 title과 content를 갱신한다.
  3. redis에서 해당 페이지 정보를 삭제한다.

위와 같은 영속화 과정에서 redis의 모든 값들은 postgres에 성공적으로 반영되어야 합니다.

이를 위해 트랜잭션을 적용했습니다.

postgres update는 데이터베이스에서 제공해주는 트랜잭션을 사용했고 redis는 실패했을 경우 이전 데이터를 직접 넣어주는 애플리케이션 단에서 처리를 했습니다.

실패했을 때 로직

            // 실패하면 postgres는 roll back하고 redis의 값을 살린다.
            this.logger.error(err.stack);
            await queryRunner.rollbackTransaction();
            title && (await this.redisService.setField(key, 'title', title));
            content &&
              (await this.redisService.setField(key, 'content', content));

redis 전체 데이터 조회할 때 keys 연산 사용

redis의 keys 연산은 redis의 모든 key를 조회할 때 사용합니다.

redis는 싱글 스레드로 동작하기 때문에 keys 연산을 사용하면 조회하는 동안 다른 연산을 블록킹하는 문제가 있습니다.

redis 공식 문서에서는 상용 서버에서 keys를 사용하지 말고 scan 명령어를 사용할 것을 추천하지만 저희 프로젝트에서는 keys 연산을 사용하기로 결정했습니다.

이유는 다음과 같습니다.

첫 번째, 저희 프로젝트는 실시간 동시 문서 편집이기 때문에 redis에 굉장히 많은 갱신 연산이 발생하고 스케줄러를 통해 주기적으로 redis의 값을 데이터베이스로 영속화 합니다.

그리고 영속화하는 과정은 redis의 값을 데이터베이스에 넣고 변경 사항이 반영되고 나서 redis의 값을 삭제하는 방식으로 이루어집니다.

그런데 scan을 사용하면 하나의 문서에서 끊임없이 변경 사항이 발생할 때 문제가 생길 수 있습니다.

scan을 통해 일부 데이터를 영속화 한 뒤 삭제하고 그 데이터에 변경 사항이 발생하면 redis에 같은 key 값이 등록이 됩니다.

그렇다면 한 번의 사이클에서 이미 영속화를 한 key 값에 대해서 갱신이 여러 번 발생할 수 있습니다.

어차피 스케줄러가 주기적으로 데이터를 영속화하기 때문에 한 번의 사이클 기준으로 하나의 key에 대해서 한 번의 갱신만 일어나도 된다고 판단했습니다.

두 번째, redis에 저장되는 데이터 양 자체가 많지 않습니다.

redis에서 데이터를 가져와서 영속화를 한 다음 redis의 값을 삭제하기 때문에 스케줄러의 주기 동안 변경 사항이 발생하지 않은 페이지는 redis에 저장되지 않습니다.

그래서 keys 연산을 통해 O(N) 시간 복잡도 동안 redis 접근을 막더라도 큰 성능 저하가 발생하지 않을 것이라고 생각했습니다.

요약

  • redis와 postgres에 대한 연산에 트랜잭션을 적용하여 원자성 적용
  • red lock을 사용하여 데이터 영속화하는 동안 다른 트랜잭션의 영향을 받지 않도록 구현

📢 리뷰 요구사항 (선택)

postgres와 redis 연산 사이 트랜잭션 적용하는 게 어렵네요...

postgres는 데이터베이스에서 제공해주는 트랜잭션을 사용했고 redis는 애플리케이션 단에서 실패했을 때 값을 다시 세팅해주는 방식으로 사용했는데 더 좋은 방법이 있을까요?

최대한 redis와 postgres 데이터 정합성을 지키고 싶어서 트랜잭션을 적용했는데 연산이 많지 않아서 그냥 트랜잭션을 사용하지 않는 게 더 좋을 수도 있다는 생각이 드네요

이 부분에 대해서 여러분들 생각이 궁금합니다

@ezcolin2 ezcolin2 self-assigned this Dec 1, 2024
@ezcolin2 ezcolin2 linked an issue Dec 1, 2024 that may be closed by this pull request
3 tasks
@ezcolin2 ezcolin2 marked this pull request as draft December 1, 2024 05:18
@ezcolin2 ezcolin2 marked this pull request as ready for review December 1, 2024 09:26
@ezcolin2 ezcolin2 changed the title Feature be #309 영속화에 red lock 및 트랜잭션 적용 Dec 1, 2024
Copy link
Member

@Tolerblanc Tolerblanc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

성민님 주말에도 열일하시는군용 Nest IoC 컨테이너 적극 활용하시는 부분이 너무 좋네요
컨플릭트 난 부분도 수정 부탁드립니다!!

// private readonly redisClient: Redis;

constructor(
@Inject(REDIS_CLIENT_TOKEN) private readonly redisClient: Redis,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오옷 이 부분도 수정하셨군요

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 근데 redis client를 DI 컨테이너에 넣으니까 RedLockService와 RedisService에서 순환 참조가 발생하더라고요 ㅠㅠ
일단 forwardRef로 해결을 하긴 했는데 이 부분도 수정을 하면 좋을 것 같네요

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P5: 리팩터링때 크론도 분리하면 좋겠군욧

@Tolerblanc
Copy link
Member

맞다 그리고 데이터 매니저를 획득할 수 있는 상태면 .transaction 을 사용해서 코드 정리를 할 수 있을 것 같은데, 혹시 롤백 시 추가 로직이 있어서 현재처럼 작성하신 걸까요??

https://typeorm.io/transactions

@ezcolin2
Copy link
Collaborator Author

ezcolin2 commented Dec 1, 2024

맞다 그리고 데이터 매니저를 획득할 수 있는 상태면 .transaction 을 사용해서 코드 정리를 할 수 있을 것 같은데, 혹시 롤백 시 추가 로직이 있어서 현재처럼 작성하신 걸까요??

https://typeorm.io/transactions

저는 콜백 같은 거 사용하면 코드 블록이 하나 더 생기고 가독성이 좀 떨어지는 것 같아서 query runner를 사용했는데 지금 생각해보니 query runner를 사용해도 try, catch 때문에 어차피 코드 블록이 생기네요.
저 연산을 하나의 함수로 묶어서 .transaction 사용해서 콜백 함수로 넣어주는 편이 훨씬 가독성이 좋아질 것 같네요!
내일 리팩토링 해보겠습니다!

@Tolerblanc
Copy link
Member

Tolerblanc commented Dec 1, 2024

@ezcolin2
redlock 선언을 자꾸 못찾는데 왜이러는걸까요ㅜㅜ 싹 지우고 다시 설치해보고 @types/redlock 도 설치해봤는데 계속 오류가 납니다. 성민님은 잘 되시나요??

error TS2307: Cannot find module 'redlock' or its corresponding type declarations.
11 import Redlock from 'redlock';

@ezcolin2
Copy link
Collaborator Author

ezcolin2 commented Dec 1, 2024

@ezcolin2 redlock 선언을 자꾸 못찾는데 왜이러는걸까요ㅜㅜ 싹 지우고 다시 설치해보고 @types/redlock 도 설치해봤는데 계속 오류가 납니다. 성민님은 잘 되시나요??

error TS2307: Cannot find module 'redlock' or its corresponding type declarations. 11 import Redlock from 'redlock';

@Tolerblanc
아 저도 그랬었는데 node_modules 3개 전부 지우고 yarn.lock 지우고 컨테이너 지우고 이미지 지우고 볼륨 지우고 yarn install 실행하고 yarn docker:dev다시 실행하니까 되더라고요!

@Tolerblanc
Copy link
Member

@ezcolin2 redlock 선언을 자꾸 못찾는데 왜이러는걸까요ㅜㅜ 싹 지우고 다시 설치해보고 @types/redlock 도 설치해봤는데 계속 오류가 납니다. 성민님은 잘 되시나요??
error TS2307: Cannot find module 'redlock' or its corresponding type declarations. 11 import Redlock from 'redlock';

@Tolerblanc 아 저도 그랬었는데 node_modules 3개 전부 지우고 yarn.lock 지우고 컨테이너 지우고 이미지 지우고 볼륨 지우고 yarn install 실행하고 yarn docker:dev다시 실행하니까 되더라고요!

오옹 잘 동작하는군요!! 혹시 몰라서 @types/redlock만 의존성 추가하고 Approve하겠습니다~

@github-actions github-actions bot merged commit 9be1de6 into develop Dec 1, 2024
9 checks passed
@github-actions github-actions bot deleted the feature-be-#309 branch December 1, 2024 13:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

feat: redis 오래된 데이터 제거
2 participants