(Transaction) lost update problem (isolation level, deadlock, update lock)
포인트 읽기 - 포인트 차감 순으로 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을 걸어준다.
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_mem1-1 획득>
SELECT<read_lock_for_mem1-2 획득>
UPDATE<write_L1 획득하기 위해 read_lock_for_mem1-2 해제 대기>
UPDATE<wirte_L2 획득 위해 read_lock_for_mem1-1 해제 대기. deadlock 발생>
http://www.gurubee.net/lecture/23962.가. Lock 종류 부분에 이러한 문제가 잘 나와 있다.
sol
위와 같은 문제를 해결하기 위해서는 고립 수준은 기본으로 두고, 처음에 SELECT 할 때 부터 read lock(shared lock) 대신 write lock(update lock)을 획득하면 된다.
write lock은 배타적 lock이므로, 어떤 트랜잭션이 실행 중이라면 다른 트랜잭션은 기존 트랜잭션이 lock을 release할 때 까지 pending 상태가 된다.
MySQL에서 update lock을 획득하기. FOR UPDATE
DB Lock이 걸리는 범위?
- WHERE에서 key column(index)에 대해 거르면 해당 row에 대해서만 lock이 걸린다.
- index가 걸려있다면, 선택한 row들만 lock이 걸린다.
- index가 안걸려 있어 전체 테이블을 서치하는 경우 테이블 전체에 lock이 걸린다.
app 단 lock VS DB 단 lock
이런 문제를 DB단이 아니라 앱단에서 lock을 걸어서 해결하는 것도 가능하다.
주로 redis로 특정 key에 대해서만 lock을 거는 방식을 많이 사용하게 된다.
- 보통 인메모리
Map<Key, Lock>
보다는 redis를 활용한다. - SELECT - UPDATE 작업이 어떤 Key와 관계된다면, redis를 사용해 같은 회원에 대한 동시 작업은 막고, 서로 다른 회원에 대한 동시 작업은 가능하게끔 만들 수 있다.
- e.g., 회원 정보 업데이트같이 회원 키로 SELECT하고 해당 레코드를 UPDATE하는 상황
- 여러 테이블에 걸쳐 일어나는 tx 작업에 사용 할 수 있다.
- 물론 DB lock도 여러 테이블에 걸쳐 사용 가능하다.
- 보통 tx 시작하면서 모든 관련 테이블의 관련 row에 update lock을 걸고 시작하는게 아니라, 접근하면서 차례차례 lock을 획득하게 되므로 이 것이 DB lock을 사용하면서 문제가 되지 않나 생각 들 수 있는데, 잘 생각해 보면 갱신 손실 같은 문제가 생기지 않는다.
- API call 같이 DB와 무관한 작업에 대한 동시성을 제어 할 수 있어 활용도가 높다.
- 구조가 심플한 것도 장점이다.
SELECT - UPDATE 로직의 무결성을 유지하자.
별도의 로직이 필요 없는 경우라면 다음과 한 쿼리로 바로 더해버려도 되지만…
1
UPDATE user_point SET user_point + :point WHERE id = 1;
SELECT 해서 뭔가를 가져오고 검증한 다음에 UPDATE를 해줘야 되는 경우, SELECT - UPDATE 사이에 읽어온 row가 DB에서 변경되지 않도록 보장해주어야만 한다.
PROPAGATION
은ISOLATION
과는 다르다!
이는 트랜잭션 내에서 또 다른 트랜잭션을 만나는 경우 어떻게 처리할 것인지를 결정한다.