이 시리즈는 데이터베이스 동시성 문제를 겪은 경험과 그 해결책에 대한 내용이 담겨 있습니다.
해결 방법 - 2
- lentHistory 테이블은 그대로 둡니다.
lent_history_id | user_id | cabinet_id | started_at | ``` |
- cabinet에 version 컬럼, activeLentCount 컬럼 추가합니다.
cabinet_id | status | version | max_user | active_lent_count | ``` |
이렇게 테이블을 수정하고 기존 동시성 문제 시나리오를 다시 살펴보겠습니다.
cabinet 동시성 문제 1
전제: N번 공유사물함은 최대 3명이 빌릴 수 있습니다. 현재 2명이 대여 중입니다.
lent_history_id | user_id | cabinet_id | started_at | ``` |
1 | 1 | N | 2023-5-15 | ``` |
2 | 2 | N | 2023-5-16 | ``` |
cabinet_id | status | version | max_user | active_lent_count | ``` |
N | AVAILABLE | 3 | 3 | 2 | ``` |
상황: 2명의 유저가 동시에 N번 사물함을 대여 시도합니다.
예상 결과: 2명 중 1명은 대여 성공, 나머지 1명은 대여 실패해야 합니다.
사용자 A(userId=3)가 N번 사물함 대여 시도
|
사용자 B(userId=4)가 N번 사물함 대여 시도
|
실제 결과 : 2명의 유저 중 1명은 대여 성공, 나머지 1명은 대여 실패합니다.
실패의 이유는 update where 문에 의해 수정된 행이 0개 일 때, 코드상에서 예외를 발생시키게 해두었고,
이 예외로 인해 트랜잭션 전체가 rollback 되었기 때문입니다.
lent_history_id | user_id | cabinet_id | started_at | ``` |
1 | 1 | N | 2023-5-15 | ``` |
2 | 2 | N | 2023-5-16 | ``` |
3 | 3 혹은 4 | N | 2023-5-20 | ``` |
cabinet_id | status | version | max_user | active_lent_count | ``` |
N | FULL | 4 | 3 | 3 | ``` |
cabinet 동시성 문제 2
전제: N번 공유사물함은 최대 3명(maxUser=3)이 빌릴 수 있습니다. 현재 0명이 대여 중입니다.
lent_history_id | user_id | cabinet_id | started_at | ``` |
cabinet_id | status | version | max_user | active_lent_count | ``` |
N | AVAILABLE | 1 | 3 | 0 | ``` |
상황: 3명의 유저가 동시에 N번 사물함을 대여 시도.
예상 결과: 3명 모두 사물함 대여 성공하고, cabinet.status는 FULL이 돼야 합니다.
사용자 A가 N번 사물함 대여 시도
|
사용자 B가 N번 사물함 대여 시도
|
사용자 C가 N번 사물함 대여 시도
|
실제 결과 : 3명의 유저 중 1명은 대여 성공, 나머지 2명은 대여 실패합니다.
실패의 이유는 update where 절에 의해 수정된 행이 0개 일 때, 코드상에서 예외를 발생시키게 해두었고,
이 예외로 인해 트랜잭션 전체가 rollback 되었기 때문입니다.
이전 글에서 논의했던 것처럼 위는 이상적인 결과가 아닙니다.
3명 모두 대여를 할 수 있음에도 불구하고, 동시에 대여 시도를 했다는 이유로 2명은 대여를 실패하니까요.
하지만 이전 글에서 결론 내린것처럼 우리 서비스에 적절한 차선책이라고 생각합니다.
lent_history_id | user_id | cabinet_id | started_at | ``` |
1 | 1 혹은 2 혹은 3 | N | 2023-5-20 | ``` |
cabinet_id | status | version | max_user | active_lent_count | ``` |
N | AVAILABLE | 2 | 3 | 1 | ``` |
cabinet 동시성 문제 3
전제: N번 공유사물함은 최대 3명(maxUser=3)이 빌릴 수 있습니다. 현재 2명(=X, Y)이 대여 중입니다.
lent_history_id | user_id | cabinet_id | started_at | ``` |
1 | 1 | N | 2023-5-15 | ``` |
2 | 2 | N | 2023-5-16 | ``` |
cabinet_id | status | version | max_user | active_lent_count | ``` |
N | AVAILABLE | 3 | 3 | 2 | ``` |
상황: X가 N번 사물함 반납 시도하고, 동시에 유저 A가 N번 사물함을 대여 시도합니다
예상 결과: X는 반납 성공, A는 대여 성공합니다. 그리고 cabinet.status = AVAILAVBLE 이 돼야 합니다.
사용자 X가 N번 사물함 반납 시도
|
사용자 A가 N번 사물함 대여 시도
|
실제 결과: (X는 반납 성공, A는 대여 실패) 혹은 (X는 반납 실패, A는 대여 성공) 하는 결과가 나옵니다.
실패의 이유는 update where 절에 의해 수정된 행이 0개 일 때, 코드상에서 예외를 발생시키게 해두었고,
그로 인해 트랜잭션 전체가 rollback 되었기 때문입니다.
생각해 볼 만한 점
위 해결방법은 낙관적 락을 활용한다는 면에서 이전 해결방법과 같습니다.
다만, 이전 해결방법에선 2개의 테이블에 각각 version 컬럼이 있었지만, 현 해결방법은 1개의 테이블에만 version 컬럼이 있습니다.
- 장점
- JPA의 @Version 어노테이션을 활용할 수 있습니다.
- 이 어노테이션은 update 쿼리를 실행할 때, 기존 행의 version 값을 자동으로 1증가 시켜서 update 해주는 기능입니다.
- insert 할 때는 이러한 version 탐색, 자동 증가 기능을 사용할 수 없습니다.
그래서 이전 해결방법에서 코드상에서 version을 직접 찾고 증가시키는 수동적인 작업을 해주었습니다. - 현 해결방법에선 코드상에서 직접 version을 다룰 필요 없이, @Version 어노테이션을 활용하기만 하면 됩니다.
- 이렇게 라이브러리의 기능을 활용하여 코드가 깔끔 해졌습니다.
- JPA의 @Version 어노테이션을 활용할 수 있습니다.
- 단점
- JPA의 @Version 어노테이션은 version 값을 자동으로 1증가 시켜서 update 해주는 기능을 수행합니다.
- 하지만 해당 엔터티의 속성이 변경이 될 때, 이 기능이 작동합니다.
- 하지만 기존 대여 로직에서, 매 대여마다 cabinet의 속성 변경이 일어나지는 않습니다.
- 특수한 상황에서(대여자수가 0 혹은 max 될 때)만 cabinet의 속성 변경이 일어납니다.
- 따라서 매 대여(혹은 반납)마다 cabinet 속성 변경이 일어나서 @Version이 작동하게 하고 싶었습니다.
- 그래서 cabinet 테이블에 activeUserCount 라는 컬럼을 추가하였습니다.
- 이렇게 되면 매 대여(혹은 반납)마다 cabinet 속성 변경이 일어나서 @Version이 작동하게 됩니다.
- 데이터 관점에서는 필요없는 컬럼이지만 동시성 문제를 해결하기 위해 추가된 컬럼이라 단점인 것입니다.
- 데이터 정합성 문제
- activeLentCount 값을 읽을 때, 아래 두 가지 방법으로 읽어 올 수 있습니다.
- lent_history 테이블에서 SELECT count(*) FROM lent_history WHERE cabinet_id=N and is_active=true;
- cabinet 테이블에서 SELECT active_lent_count FROM cabinet WHERE cabinet_id=N;
- 이는 데이터의 중복이라고 볼 수 있습니다. 중복은 곧 데이터 정합성 문제로 이어질 수 있습니다.
- 예를 들어, 관리자가 mysql workbench 로 DB에 접근해서
반납 처리를 위해 lent_history 테이블 에서만 값을 수정하고, cabinet 테이블은 실수로 수정 하지 않을 수 있습니다.
그럼 위에 보인 2개 쿼리의 실행 결과는 서로 달라지게 됩니다. 데이터가 정합적이지 않게 된 것입니다.
- activeLentCount 값을 읽을 때, 아래 두 가지 방법으로 읽어 올 수 있습니다.
- JPA의 @Version 어노테이션은 version 값을 자동으로 1증가 시켜서 update 해주는 기능을 수행합니다.
- 아주 결정적으로 데드락이 발생합니다.
- 이는 다음 글에서 자세히 다루겠습니다.
결국 이 해결방법은 장점만 본다면 좋지만, 데드락이 발생하는 심각한 문제 때문에 사용할 수 없는 방법입니다.
다음 글에서 이 시나리오에서 왜 데드락이 발생했는지에 대해 다루겠습니다...
'데이터베이스' 카테고리의 다른 글
[transaction 동시성 문제⑥] 테스트 (0) | 2023.08.07 |
---|---|
[transaction 동시성 문제⑤] 데드락 - 2 (0) | 2023.07.28 |
[transaction 동시성 문제④] 데드락 - 1 (0) | 2023.07.15 |
[transaction 동시성 문제②] 해결 - 1 (0) | 2023.07.12 |
[transaction 동시성 문제①] 원인 (0) | 2023.07.11 |