-
[MySQL] 트랜잭션 격리레벨 SERIALIZABLE과 데드락DB 2024. 1. 21. 20:03
동시성 문제를 해결하던 중 트랜잭션 격리 레벨을 최대로 올리면 (성능은 차치하더라도) 해결할 수 있을거라 생각했다.
왜냐하면 트랜잭션이란 여러 실행 단위를 하나의 단위로 보장해주고, 격리 레벨을 직렬화 한다면 한 단위씩 실행된다고 추론했기 때문이다.
하지만 결과적으로 데드락이 발생했다.
물론 데드락으로 인해 하나의 트랜잭션이 실패하고 다른 트랜잭션은 성공한다면 동시성 문제는 발생하지 않지만 실패한 요청의 보장이 어렵다. 실패 확률이 높고 성능이 느린 서비스는 사용자에게 극심한 불편을 주기 때문에 이 방법은 쓰지 않도록 하자.
하지만 SERIALIZABLE 이전의 격리 레벨에서는 데드락이 발생하진 않았는데, 갑자기 왜 발생한걸까?
[ 데드락이란? ]
데드락은 두 개 이상의 자원이 상대방의 작업이 끝나기 만을 기다리는 상태가 지속되다 아무것도 못하고 종료되는 상태를 말한다.
자세한건 TBD 참고하자
[ SERIALIZABLE이란? ]
트랜잭션 격리 레벨 중 가장 엄격한 SERIALIZABLE 은 여러 트랜잭션이 한 레코드에 동시에 접근할 수 없도록 한다.
이 수준에서는 읽기를 할 때에도 SELECT ~ FOR SHARE 로 공유락(읽기락)을 건다.
공유락이 걸려있다면 쓰기락을 거는 것이 불가능하므로 다른 트랜잭션의 레코드 변경을 막아 트랜잭션의 순차적인 처리를 유도한다.
하지만 (성능 문제를 떠나서) 이상적으로 순차 보장을 할 것 같은 공유락으로 인해 데드락이 발생할 수 있다.
공유락은 다른 트랜잭션과 공유할 수 있기 때문이다.
[ SERIALIZABLE 에서 데드락이 발생하는 원인 분석 ]
예제는 상품 구매 이다.
application에서 상품 재고를 확인 -> 재고가 있다면 -> 구매 를 한 트랜잭션으로 묶었다.
사용된 쿼리는 아래와 같다.
더보기더보기#1. 재고를 포함한 상품 정보 조회 SELECT * FROM PRODUCT WHERE productId = #{productId} #2. 상품 재고 수정 UPDATE PRODUCT SET purchasedStock = #{stock} WHERE productId = #{productId}
@transactional(isolation = isolation.serializable) public void purchase(PurchaseRequest request) {}
이때 발생한 예외는 아래와 같다.
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.dao.DeadlockLoserDataAccessException: ### Error updating database. Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction ### The error may exist in class path resource [mappers/productMapper.xml] ### The error may involve flab.just10minutes.product.repository.ProductDao.updatePurchasedStock-Inline ### The error occurred while setting parameters ### SQL: UPDATE PRODUCT SET status = ?, purchasedStock = ? WHERE productId = ? ### Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction ; Deadlock found when trying to get lock; try restarting transaction] with root cause
정확한 원인을 파악하기 위해 MySQL CLI로 들어가 아래의 명령어를 쳐보자.
innodb에서 발생한 로그를 실시간 순으로 보여준다.
show engine innodb status\G
여러 내용이 있지만 LATEST DETECTED DEADLOCK 에서 최근 발생한 데드락의 상태를 보자.
요약본
트랜잭션 id 45778와 45779가 PRODUCT 테이블의 같은 레코드에 S lock을 걸고 있었다.
서로 같은 레코드에 X lock을 걸려고 대기하다가 InnoDB가 트랜잭션 id 45779를 롤백하고 트랜잭션 id 45778를 적용했다.상세 보기
더보기더보기------------------------ LATEST DETECTED DEADLOCK ------------------------ 2023-05-10 13:57:46 140528273409792 *** (1) TRANSACTION: TRANSACTION 45778, ACTIVE 0 sec starting index read mysql tables in use 1, locked 1 LOCK WAIT 8 lock struct(s), heap size 1128, 243 row lock(s) MySQL thread id 40, OS thread handle 140528727430912, query id 198673 <서버IP> flab updating UPDATE PRODUCT SET status = 'ONSALE', purchasedStock = 240 WHERE productId = 1 #데드락이 발생한 쿼리를 보여준다. *** (1) HOLDS THE LOCK(S): RECORD LOCKS space id 86 page no 4 n bits 72 index PRIMARY of table `test`.`PRODUCT` trx id 45778 lock mode S locks rec but not gap Record lock, heap no 2 PHYSICAL RECORD: n_fields 12; compact format; info bits 0 #데드락 발생 당시 트랜잭션 1이 보유한 락을 보여준다. #해석하면 아래와 같다. #PRODUCT 테이블의 primary index 영역의 #page no 4번의 n bits 72 부분을 #lock mode S(Shared Lock)으로 잡고 있다. RECORD LOCKS space id 86 page no 4 n bits 72 index PRIMARY of table `test`.`PRODUCT` trx id 45778 lock_mode X locks rec but not gap waiting Record lock, heap no 2 PHYSICAL RECORD: n_fields 12; compact format; info bits 0 #트랜잭션 1이 S락을 건 채로 다음 락을 위해 대기하고 있던 내용이다. #해석하면 아래와 같다. #PRODUCT 테이블의 primary index 영역의 page no 4번 n bit 72 부분에 #lock mode X(Exclusive Lock)을 걸려고 기다리고 있다. #... 생략 : 2번 트랜잭션 내용(1번과 같이 S 락을 건 후 X 락을 걸기위해 기다리고 있다.) *** WE ROLL BACK TRANSACTION (2) ------------ TRANSACTIONS ------------ Trx id counter 45785 Purge done for trx's n:o < 45785 undo n:o < 0 state: running but idle History list length 36 #결국 (2)번 트랜잭션이 롤백 되었음을 알려준다.
MySQL에서 트랜잭션을 적용하기 위해서는 InnoDB를 사용해야 한다.
InnoDB는 두 개의 락을 사용하는데 특징은 아래와 같다.
- s-lock(공유락, 읽기락)
1. s-lock 끼리는 동시에 접근이 가능하다.
2. s-lock 이 걸린 레코드에 x-lock 을 사용할 수 없다.
3. SELECT ~ FOR SHARE로 사용할 수 있다.
- x-lock(베타락, 쓰기락) :
1. x-lock 이 걸린 레코드는 다른 트랜잭션이 x, s-lock 모두 걸 수 없다.
2. s-lock 이 걸린 레코드에 x-lock 을 사용할 수 없다.
3. update 질의문에는 x-lock이 걸린다.
4. SELECT ~ FOR UPDATE로 사용할 수 있다.2번 내용은 중요한 내용이라 두 번 강조했다.
s-lock 은 여러 트랜잭션이 동시에 걸 수 있다. x-lock 은 x, s-lock 모두 걸리지 않아야 걸 수 있다.
즉, 여러 트랜잭션이 s-lock을 건 상태에서 한 트랜잭션만 x-lock을 얻기 위해 대기하다가 InnoDB가 임의의 트랜잭션을 롤백 시키고 하나만 통과 시킨다.
[ 결론 ]
- SERIALIZABLE은 최고 수준의 격리 레벨이지만 읽기 시 레코드 락을 걸기 때문에 데드락 상태에 빠지기 쉽다.
- SERIALIZABLE은 성능상의 이유로도 안쓰는 것이 좋다.
'DB' 카테고리의 다른 글
[Lock] X-Lock은 순서를 보장할까?(Transaction Scheduling : CATS) (0) 2024.09.12 [Lock] 비관적 락(Pessimistic Lock) 이란? (0) 2024.01.22 [Lock] 낙관적 락(Optimistic Lock) 이란? (1) 2024.01.22 [MySQL] 트랜잭션 격리 레벨 REPEATABLE READ와 동시성 (0) 2024.01.21