동시성 제어 과정에서 했던 고민과 해결

RACOONMAN98·2023년 1월 17일
0

삽질일기

목록 보기
4/5
post-thumbnail

문제상황 👀

문제상황 : 좋아요 증가요청이 동시에 왔을 때 갱신 손실이 일어났습니다.(둘 중 한개만 반영 ex. 두 명이 같은시간에 눌렀을때 '1' 만(한개의 요청만) 올라감)

해결 선택지 🎨

1. 비관적 락

비관적 락 의경우 메서드에 @lock어노테이션을 걸었을때 메서드 시작순간부터 베타 락 상태에 들어가기 때문에 동시에 접근하는 트랜잭션이 많아졌을댄 대기시간이 많이 증가할것이라 생각하였고, 또한 애플리케이션 레벨에서 명시적인 락을 걸었을때 db자체적인 lock과 더불어 예상못한 사이드이펙트를 불러올수있다고 판단하였고, 트랜잭션에 대한 이해가 충분하지 못한상황에서 사용하는것은 무리라고판단하여 후순위 선택지로 미뤘습니다.

2. 낙관적 락

낙관적 락의 경우 entity의 필드에 @version을 이용해서 버전이 다를경우 롤백처리를 하는데 이후 사용할 방법과 비교해봤을때 비즈니스 로직을 수정하기에는 시간이 없다고 판단하여 후순위로 미루었습니다

3. 분산 락

분산 락의 경우 redisson이나, mysql 의 namedlock 을 구현해서 문제를 해결할 수 있었지만 시간비용이 많이들고 redisson의 경우 마찬가지로 프로젝트 규모상 좋아요 기능 하나만을 위해 해당 의존성을 추가하는건 해당 지식을 공부할 시간적 비용이 많지않다고판단했습니다.

4. 메시지 큐를 이용한 이벤트 발급방식 처리

rabbitmq를 사용해서 Event-Driven방식으로 요청을 mq에서 구독하여 해결하는 방법도 있었으나 전체적인 로직을 다시짜야하고, 해당 mq서비스를 공부할 시간적 여유가 없었기에 후순위로 미루었습니다.

5. 스케쥴러를 통한 궁극적 일관성 보장

스케쥴러를 통해 궁극적 일관성을 보장하는 방법도 고려해봤으나. 좋아요 같은 실시간으로 게시물의 좋아요수를 확인하는 시스템의
목적이 희미해 질것같아서 후순위로 미루었습니다.

선택방법 🤷‍♂️

따라서 update 쿼리를 통한 DB 자체적으로 걸어주는 베타 락 을 이용해서 해결하는 방법을 시도하였습니다.
mysql에서 update 는 트랜잭션이 끝나고 락을 해제하기 전까지 베타 락(Exclusive Lock, X-Lock ,다른 세션이 해당자원에 접근하는것을 막음.
mysql에서 이 베타 락의 row에는 조회 트랜잭션은 접근이 가능하다.)을 얻게됩니다.
따라서 저는 좋아요 증가요청의 동시성 제어를 위해
post에있는 likes 테이블에 update쿼리를 날려서 베타락을 얻게한다음
다른 트랜잭션이(좋아요 증가 트랜잭션)이 락을 풀기전까지는 접근못하게 해서 트랜잭션을 커밋합니다.
update가 끝난후에는 x-lock획득에 실패해서
x-lock획득을 대기하고 있던 다른 좋아요 요청이 x-lock을 획득하고 커밋하여 update하는 방식으로
동시성을 제어하는데에 성공했습니다. ,
서로 자원공유 문제는 없기때문에 데드락 문제도 피할 수 있었습니다.

다른 문제 👀

하지만 like의 숫자를 증가시키려는 update쿼리와
게시글을 수정하는(update 쿼리) 트랜잭션이 동시에 일어날경우 트랜잭션간 충돌이 일어나서
둘 중 하나의 트랜잭션은 롤백이 되어서 요청이 처리가 되지않게되는걸 테스트코드로 확인했습니다.
(산넘어 산)

✔ 해결

따라서 저는 post의 likes컬럼과 likes식별을 위한 postid값을 가진 like_count테이블을 생성해서
update시에 post의 해당row에 x-lock이 걸리는 걸 회피하는 방식을 택했습니다.

또 다른 문제 👀

왜계속문제가 생기는건데
이렇게 구현하고 난뒤에 likes_count에 어떻게 postid의 likes정보를 넣어야하나
고민에 빠졌습니다. 포스트 등록시에 likes_count테이블에 row를 추가하자니,
게시글 테이블의 postid가 Auto increament 였기 때문에 게시글 생성전에 response값도 없는데 자동으로 생기는걸 테이블에 넣어줄수도없고,,,

✔ 해결

그러던중 upsert[ insert+ update ]를 구글링도중 찾았고,
해당 sql문은 값이없으면 넣고, 키값이 같았을경우 원하는 값을 update할수있는
좋은 친구였습니다.
따라서 해당방법으로 likes_count테이블에 성공적으로 해당postId의 row를 추가할수있었습니다.

또 또 다른 문제 👀

아.
그 후에 getAll()요청을 했을 때 다른 테이블에 있는 likes의 숫자를 어떻게 모든게시물에 넣을건지가 다음 문제였습니다.

✔ 해결

저는 좋아요 숫자를 post의 get요청에 likes를 넣어주기위해 껍데기만 있는 post.likes 컬럼에
likes_count.likes의 숫자를 넣어주는 쿼리문을 작성하였고 해당 쿼리문은 select 이기때문에 mysql에선
베타락()에 걸린 row여도 정보 조회에는(SELECT)제약이 없기 때문에 likes_count.likes를 가져오는데에는 문제가없었습니다.
가져온 likes숫자를 stream 처리하여 첫번째의 likes_count.likes의 UPDATE 요청과 동일한 트랜잭션(베타락을 보장받는)에서 post.likes를
update할 수 있었습니다.
이후 테스트 결과 save,update 요청을 두 개의 likes 증가 요청과 동시에 실행시킨 결과
모두 갱신손실 없이 정상적으로 db에 반영되어 문제를 해결할수있었습니다.

🔨 한계점.....

  • 한계점으로는 쓰인 sql문이 특정 DB(mysql) 에 종속되어있다는게 한계점입니다.
  • 매번 좋아요 요청시 업데이트를위해 DB에 접근해야하는데 캐시레이어를 도입해서
    (할수있을진 모르겠지만 생각상으로는)
    좀더 효율적으로 I/O시간을 줄일 수 있을것같습니다.
profile
공부일기

0개의 댓글