이 시리즈는 데이터베이스 동시성 문제를 겪은 경험과 그 해결책에 대한 내용이 담겨 있습니다.
참고
대여처리는 (userId, cabinetId 포함한 각종 정보를 담은) row를 lent_history 테이블에 추가하는 작업입니다.
해결 방법
- lent_history에 version 컬럼 추가
- 두 컬럼 {version, cabinetId}에 composite unique index
lent_history_id | user_id | cabinet_id | version | started_at | ``` |
- cabinet에 version 컬럼 추가
cabinet_id | status | version | max_user | ``` |
이렇게 테이블을 수정하고 기존 동시성 문제 시나리오를 다시 살펴보겠습니다.
cabinet 동시성 문제 1
전제: N번 공유사물함은 최대 3명이 빌릴 수 있습니다. 현재 2명이 대여 중입니다.
lent_history_id | user_id | cabinet_id | version | started_at | ``` |
1 | 1 | N | 1 | 2023-05-15 | ``` |
2 | 2 | N | 2 | 2023-05-16 | ``` |
상황: 2명의 유저가 동시에 N번 사물함을 대여 시도합니다.
예상 결과: 2명 중 1명은 대여 성공, 나머지 1명은 대여 실패해야 합니다.
사용자 A(userId=3)가 N번 사물함 대여 시도
|
사용자 B(userId=4)가 N번 사물함 대여 시도
|
실제 결과 : 2명의 대여 시도자 중 1명은 대여 성공, 1명은 대여 실패합니다.
실패의 이유는 composite unique index 제약조건 위반으로 트랜잭션 전체가 rollback 되었기 때문입니다.
(위에서 insert into 구문은 '대여처리'에 해당합니다)
lent_history_id | user_id | cabinet_id | version | started_at | ``` |
1 | 1 | N | 1 | 2023-05-15 | ``` |
2 | 2 | N | 2 | 2023-05-16 | ``` |
3 | 3 혹은 4 | N | 3 | 2023-05-20 | ``` |
cabinet 동시성 문제 2
전제: N번 공유사물함은 최대 3명(maxUser=3)이 빌릴 수 있습니다. 현재 0명이 대여 중입니다.
lent_history_id | user_id | cabinet_id | version | started_at | ``` |
cabinet_id | status | version | max_user | ``` |
N | AVAILABLE | 1 | 3 | ``` |
상황: 3명의 유저가 동시에 N번 사물함을 대여 시도합니다.
예상 결과: 3명 모두 사물함 대여 성공하고, cabinet.status는 FULL이 돼야 합니다.
사용자 A가 N번 사물함 대여 시도
|
사용자 B가 N번 사물함 대여 시도
|
사용자 B가 N번 사물함 대여 시도
|
실제 결과 : 3명의 유저 중 1명은 대여 성공, 나머지 2명은 대여 실패합니다.
실패의 이유는 update where 문에 의해 수정된 행이 0개 일 때, 코드상에서 예외를 발생시키게 해두었고,
이 예외로 인해 트랜잭션 전체가 rollback 되었기 때문입니다.
이는 이상적인 결과는 아닙니다.
3명 모두 대여를 할 수 있음에도 상황임에도 불구하고, 동시에 대여 시도를 했다는 이유로 2명은 대여를 실패하니까요.
(이에 대해선 맨 밑에서 좀 더 얘기하겠습니다.)
lent_history_id | user_id | cabinet_id | version | started_at | ``` |
1 | 1 혹은 2 혹은 3 | N | 1 | 2023-5-20 |
cabinet_id | status | version | max_user | ``` |
N | AVAILABLE | 2 | 3 | ``` |
cabinet 동시성 문제 3
전제: N번 공유사물함은 최대 3명(maxUser=3)이 빌릴 수 있습니다. 현재 2명(=X, Y)이 대여 중입니다.
lent_history_id | user_id | cabinet_id | version | started_at | ``` |
1 | 1 | N | 1 | 2023-5-15 | ``` |
2 | 2 | N | 2 | 2023-5-16 | ``` |
cabinet_id | status | version | max_user | ``` |
N | AVAILABLE | 3 | 3 | ``` |
상황: X가 N번 사물함 반납 시도하고, 동시에 유저 A가 N번 사물함을 대여 시도합니다
예상 결과: X는 반납 성공, A는 대여 성공합니다. 그리고 cabinet.status = AVAILAVBLE 이 돼야 합니다.
사용자 X가 N번 사물함 반납 시도
|
사용자 A가 N번 사물함 대여 시도
|
실제 결과: (X는 반납 성공, A는 대여 실패) 혹은 (X는 반납 실패, A는 대여 성공) 하는 결과가 나옵니다.
실패의 이유는 update where 문에 의해 수정된 행이 0개 일 때, 코드상에서 예외를 발생시키게 해두었고,
그로 인해 트랜잭션 전체가 rollback 되었기 때문입니다.
생각해 볼 만한 점
다음의 문제가 해결되었습니다.
- 예를들어 maxUser=3 이지만 3명을 초과한 인원이 사물함을 빌리는 이른바 초과 인원 문제.
- 대여, 반납에 의해 cabinet.status가 적절히 업데이트 되어야 하는데, 잘못 업데이트 되는 문제.
하지만 위 해결방법에는 단점이 있습니다.
- lent_history에서 version
- SELECT max(version) FROM lent_history WHERE cabinet_id=N;
- 위 쿼리를 통해 max_version 값을 읽어 들이고 +1 한 값을 새 row가 insert 될 때 넣어해줘야 합니다.
- 이러한 version 관련 코드가 비지니스 로직에 추가되어야 합니다.
- cabinet에서 version
- lent_history와 유사한 방식으로 해줘야 합니다.
- 하지만 cabinet에서는 insert 쿼리가 아니라, update 쿼리입니다.
- 따라서, JPA의 @Version을 사용하면 비지니스 로직에 version 관련 코드가 들어가지 않아도 됩니다.
(단점이라고 했는데, 단점이라기 보다 해결 가능한 단점이라고 보면 좋을 거 같습니다)
- 현재 3명이 빌릴 수 있는 상태에서 3명이 동시에 대여를 시도하면, 1명만 대여 성공하고 2명은 대여 실패합니다.
- 만약 사용자수가 많고, 사물함 대여에 대한 경쟁이 심한 상황이라면 이런 상황은 바람직 하지 않습니다.
- 선착순, 티켓팅 서비스에선 절대 이렇게 하면 안 될거 같습니다.
비관적 락을 사용하면 어떨까요?
- for update 키워드를 사용하여 "DB로부터 현재 대여중인 인원수를 읽음" 부터 락을 걸고 들어갑니다.
- 그래서 동시에 대여를 API를 날려도, 오직 하나의 트랜잭션만 실행되고, 나머지 트랜잭션은 대기상태가 됩니다.
- 이렇게 하면 위에서 얘기한 선착순 문제도 해결되고, version 값을 직접 다루지 않아도 됩니다.
하지만 비관적 락 방법에도 생각해 봐야 할 부분이 있습니다.
- 한 사물함의 대여 기간은 14일입니다. 대여, 반납이 빈번히 일어나지 않는 것입니다.
따라서 동시성 문제가 발생할 가능성이 낮습니다. 실제로 서비스 출시 이후 6개월 동안 동시성 문제는 단 한 번 발생하였습니다.
(동시성 문제-1 이 발생하였습니다.) - 동시성 문제가 발생할 가능성이 낮음에도 비관적 락을 사용하는 것은 불필요하게 DB 성능을 낮추는 것으로 볼 수 있습니다.
낙관적 락 그리고 비관적 락 모두 장,단점이 있습니다.
저희 서비스는 동시성 문제가 발생한 횟수가 매우 적다는 것에 근거하여 낙관적 락을 사용하는게 합리적이라고 판단하였습니다.
다음 글에서 계속...
'데이터베이스' 카테고리의 다른 글
[transaction 동시성 문제⑥] 테스트 (0) | 2023.08.07 |
---|---|
[transaction 동시성 문제⑤] 데드락 - 2 (0) | 2023.07.28 |
[transaction 동시성 문제④] 데드락 - 1 (0) | 2023.07.15 |
[transaction 동시성 문제③] 해결 - 2 (0) | 2023.07.13 |
[transaction 동시성 문제①] 원인 (0) | 2023.07.11 |