오랜만에 플로니 관련 글을 써본다.
플로니는 공유 가계부이다. 따라서, 한 자원에 여러 명이 접근하게 된다. 그렇기에 동시성 제어가 필수였다.
런칭 전에도, 동시성 제어를 예상하고, 대비해놨었다.
그럼에도 불구하고, 서비스를 운영하다 보니 예상치 못한 곳에서 에러가 터지기 마련이었다.
이제는 실전이다.ㅋ
2023.10.08 - [Floney] - [플로니] 데이터 동시성 제어하기
NonUniqueResultException 발생
런칭한 지 얼마 지나지 않아, NonUniqueResultException이라는 에러가 발생했다.
우리는 자산이라는 도메인을 가지고 있었다. 자산 데이터는 사용자에게 6개월의 누적합을 보여주게 되어 있었다.
그 당시 시나리오는 다음과 같았다.
예시 시나리오
- 사용자가 2023년 1월 1일에 수입 5000원을 추가한다.
- 코드는 1/1일부터 한 달 단위로 5년 치 자산 내역(총 60개월)을 조회한다.
- 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 책 사랑합니다.
실제 트랜잭션 재현을 위해 직접 터미널을 켜서 쿼리를 날려보며 확인 해본게 도움이 컷고 무엇보다 팀원이 함께 트러블 슈팅할 때 정말 많은 도움을 주었었다.
'Floney' 카테고리의 다른 글
정산 공유 가계부, 안드로이드 버전 플로니 출시 (4) | 2024.09.29 |
---|---|
[플로니] Mysql에서는 이모지를 몇 글자로 인식하나요?🦄🍀🍀 (3) | 2024.01.16 |
[플로니] 에러에 대비하는 서비스 만들기 (1) | 2024.01.05 |
[플로니] 드디어 런칭했습니다 + 회고 .. 앞으로 나는 (12) | 2023.12.03 |
[플로니] 가계부 금액 타입 정하기 (double vs decimal) (4) | 2023.11.12 |