Floney

[플로니] 자산 내역 중복 데이터 트러블 슈팅기 (동시성 제어 실전편)

딤섬뮨 2024. 8. 4. 16:21
728x90

오랜만에 플로니 관련 글을 써본다.
 
플로니는 공유 가계부이다. 따라서, 한 자원에 여러 명이 접근하게 된다. 그렇기에 동시성 제어가 필수였다.
런칭 전에도, 동시성 제어를 예상하고, 대비해놨었다.
그럼에도 불구하고, 서비스를 운영하다 보니 예상치 못한 곳에서 에러가 터지기 마련이었다.
이제는 실전이다.ㅋ
 
2023.10.08 - [Floney] - [플로니] 데이터 동시성 제어하기
 

 

[플로니] 데이터 동시성 제어하기

런칭 때가 되니..끝이라고 생각했지만, 처리해야할 게 하나둘 생긴다 오늘은 이러한 이슈를 생각해봐야한다. 만약, 동시에 가계부 내역을 고쳐서 동시성 문제가 생기면 어떻게 하나요???? 우리의

sienna1022.tistory.com

NonUniqueResultException 발생

런칭한 지 얼마 지나지 않아, NonUniqueResultException이라는 에러가 발생했다.

우리는 자산이라는 도메인을 가지고 있었다. 자산 데이터는 사용자에게 6개월의 누적합을 보여주게 되어 있었다.


 
그 당시 시나리오는 다음과 같았다.
 

예시 시나리오

  1. 사용자가 2023년 1월 1일에 수입 5000원을 추가한다.
  2. 코드는 1/1일부터 한 달 단위로 5년 치 자산 내역(총 60개월)을 조회한다.
  3. 5년 치 데이터를 기반으로 다음과 같은 쿼리를 실행한다:
    • 2023년 2월 자산 데이터가 이미 있다면, 2월 자산 데이터에 5000원을 더해 업데이트한다.
    • 2023년 2월 데이터가 없다면, 2023년 2월 5000원로 자산 데이터를 삽입한다.

 

문제 원인

트랜잭션 격리 수준으로 인한 중복 데이터 생성
 
우리의 DB 트랜잭션 격리 레벨은 Repeatable Read였다.
Repeatable Read의 가장 큰 특징은 트랜잭션이 커밋되기 전까지는 다른 트랜잭션에 영향을 주지 않는다는 것이다.
 
예시로 다음과 같다.
 

 

  • 문제 상황
    • 사용자가 2023년 1월 1일에 가계부 내역을 추가하여 트랜잭션 1이 시작됨
    • 트랜잭션 1: 1월 자산 데이터 확인 후 insert
  • 동시성 문제
    • 트랜잭션 1이 커밋되기 전에 같은 날짜에 또 다른 가계부 내역 추가 시 트랜잭션 2 시작
    • 트랜잭션 2: 1월 자산 데이터 조회 시 트랜잭션 1이 커밋되지 않았으므로 데이터 없음으로 판단, insert 시도
  • 결과
    • 트랜잭션 1과 트랜잭션 2 모두 커밋되면 중복 데이터 생성

 
 

 

해결 방안

우리는 낙관적 락과 비슷한 Upsert 처리를 통해 해당 동시성 에러를 해결했다.
 
 

  • Upsert 사용
    • PK 혹은 Unique Key 중복 시, insert가 아닌 update를 실행하는 MySQL Upsert 쿼리(ON DUPLICATE KEY UPDATE)를 사용했다.
  • 복합 Unique Key 설정
    • date와 book을 기준으로 복합 unique key를 설정했다.
    • MySQL에서 제공하는 upsert 문법을 통해 트랜잭션 2번이 커밋 시 이미 트랜잭션 1번으로 인해 데이터가 존재하므로,
    • Insert 대신 update가 실행된다.

런칭 전에, 그렇게 동시성 이슈가 많이 날까? 의문이였다.
하지만, 정말 예기치 못한 곳에서 동시성 이슈가 난다는 것을 통해, 운영이 얼마나 공부와 다른가 싶었다.
 
해당 이슈를 볼 때, 정말 DB에 관한 지식이 늘었었다. 리얼MySQL 책 사랑합니다.
실제 트랜잭션 재현을 위해 직접 터미널을 켜서 쿼리를 날려보며 확인 해본게 도움이 컷고 무엇보다 팀원이 함께 트러블 슈팅할 때 정말 많은 도움을 주었었다.
 

728x90