(Transaction) lost update problem (isolation level, deadlock, update lock)
포인트 읽기 - 포인트 차감 순으로 Mysql DB 작업이 발생하는 상황이었다.
1
2
3
4
5
6
7
8
@Transactional
public foo bar() {
// SELECT
MemberRestMileageInfo member = getMemberMileageByKey(memberKey);
...
// UPDATE
memberInfoMapper.update(member);
}
이런 상황에서 같은 memberKey
를 대상으로 빠르게 두 번 요청하면 갱신 손실 문제가 발생한다.
1
2
3
4
5
6
7
T1 T2
start transaction
start transaction
SELECT<point : 10000>
SELECT<point : 10000>
UPDATE<point : 5000>
UPDATE<point : 9000>
즉, 먼저 들어온 트랜잭션이 UPDATE 하기 전에 다른 트랜잭션이 SELECT를 해버리면, 먼저 들어온 트랜잭션 T1의 UPDATE는 덮어써져서 사라져버린다.
그래서 두 트랜잭션이 동시에 실행되지 않도록 하기 위해, 처음에는 고립 수준(isolation level)을 SERIALIZABLE
로 조정했다. 그러나 이는 deadlock을 유발했다.
트랜잭션 고립 수준을 SERIALIZABLE 로 설정했을 때 발생하는 deadlock 문제
SERIALIZABLE
은 SELECT로 가져온 row에 대해 read lock(shared lock)을 걸어준다.
read lock만 가지고 있기 때문에, 이후 UPDATE를 만나면 write lock을 획득하려 시도한다.
read lock 걸려있는 row에 대해서, 타 스레드에서 read는 가능하고, write는 lock이 풀릴 때 까지 불가. pending하게 된다.
1
2
3
4
5
6
7
T1 T2
start transaction
start transaction
SELECT<read_lock_for_member1-1 획득>
SELECT<read_lock_for_member1-2 획득>
UPDATE<write_L1 획득하기 위해 read_lock_for_member1-2 해제 대기>
UPDATE<wirte_L2 획득 위해 read_lock_for_member1-1 해제 대기. deadlock 발생>
- T1, T2가 모두 member1에 대한 shared lock을 가지고 있는 상태에서, exclusive lock을 획득하려고 시도하니 데드락에 걸린다.
- http://www.gurubee.net/lecture/2396 - 2.가. Lock 종류 부분에 이러한 문제가 잘 나와 있다.
sol
SELECT FOR UPDATE
- 위와 같은 문제를 해결하기 위해서는 고립 수준은 기본으로 두고, 처음에 SELECT 할 때 부터 read lock(shared lock) 대신 write lock(update lock)을 획득하면 된다.
- write lock은exclusive lock이므로, 어떤 트랜잭션이 실행 중이라면 다른 트랜잭션은 기존 트랜잭션이 lock을 release할 때 까지 pending 상태가 된다.
- MySQL에서 update lock을 획득하기. FOR UPDATE
참고
PROPAGATION
은ISOLATION
과는 다르다!
PROPAGATION
: 트랜잭션 내에서 또 다른 트랜잭션을 만나는 경우 어떻게 처리할 것인지를 결정한다.
ISOLATION
: 트랜잭션에서 접근하는 데이터의 고립 수준을 결정한다. (e.g.,READ_COMMITTED
)
This post is licensed under CC BY 4.0 by the author.