데드락! SERIALIZABLE과 무슨 관계일까?

홍지범·2023년 6월 12일
2

Intro

MySQL InnoDB의 기본 격리 수준인 REPEATABLE READ에서 SERIALIZABLE로 격리 수준을 올려 봤습니다.

그리고 같은 조건에서 부하 테스트를 거친 결과 아래와 같은 결과가 발생했습니다.

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

로그를 살펴보니 DeadLock이 발생했다고 합니다.
분명 격리 레벨을 SERIALIZABLE로 설정하면 다른 트랜잭션이 접근할 수 없도록 Shared Lock을 건다고 했습니다.
하지만 데드락이 발생한 이유는 무엇일까요?

정확한 상태를 보기 위해 데드락 발생 당시 MySQL의 상태를 확인 해보겠습니다.

MySQL 내부 데드락 발생 결과 분석

최근 발생한 락

------------------------
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

최근 데드락이 발생한 정보를 보여줍니다.
트랜잭션 중 update 쿼리에서 데드락이 발생한 것을 알려주고 있습니다.

트랜잭션이 보유하고 있는 락

*** (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

데드락 발생 당시 해당 트랜잭션이 보유하고 있던 락의 종류를 알려주고 있습니다.
SERIALIZABLE 격리 수준에서 read는 select ... for share로 공유 락(Shared Lock)을 겁니다.
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

데드락 발생 당시 해당 트랜잭션이 락을 걸려고 기다리고 있던 내용 입니다.
PRODUCT 테이블의 primary index 영역의 page no 4번 n bit 72 부분에 lock mode X(Exclusive Lock)을 걸려고 기다리고 있습니다.

(2)트랜잭션 상태
(1)트랜잭션 이후 로그에 나온 (2)트랜잭션도 같은 레코드에 S lock 을 걸고 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 cli에

show engine innodb status\G 

명령어를 입력했을 때 트랜잭션 id 45778와 45779가 PRODUCT 테이블의 같은 레코드에 S lock을 걸고 있었고 서로 같은 부분에 X lock을 걸려고 대기하다가 InnoDB가 트랜잭션 id 45779를 롤백하고 트랜잭션 id 45778를 적용했음을 보여줍니다.

왜 이런 결과가 발생한 것 일까요?

InnoDB 락 종류

먼저 InnoDB의 락 종류를 간단하게 살펴보겠습니다.

InnoDB Record(Row) Lock의 종류

  • Shared Lock(S lock) : 읽기 락, 공유 락으로 특정 Row를 읽을 때 사용되는 락. Shared Lock 끼리는 동시에 접근이 가능하다.
    S lock이 걸린 레코드에 Exclusive Lock을 사용할 수 없다.
  • Exclusive Lock(X lock) : 쓰기 락, 베타 락으로 특정 Row를 변경할 때 사용되는 락. Exclusive Lock이 걸린 Row는 다른 트랜잭션이 Exclusive Lock이나 Shared Lock을 걸 수 없다.
    update 쿼리는 X lock을 건다.

데드락 당시 트랜잭션과 락 상태 정리

  1. 1번 트랜잭션이 상품 테이블의 a row 재고를 수정하려 읽었습니다.(S lock)
  2. 2번 트랜잭션이 상품 테이블의 a row 재고를 수정하려 읽었습니다.(S lock)
    S lock은 공유 되므로 두 트랜잭션 모두 락을 걸 수 있습니다.
  3. 1번 트랜잭션이 상품 테이블의 a row 재고를 수정하려 시도합니다.(X lock)
    하지만 2번 트랜잭션이 S lock을 걸고 있으므로 1번 트랜잭션이 X lock을 걸 수 없어 2번 트랜잭션이 S lock을 release할 때 까지 대기합니다.
  4. 2번 트랜잭션이 상품 테이블의 a row 재고를 수정하려 시도합니다.(X lock)
    2번 트랜잭션도 마찬가지로 1번 트랜잭션이 S lock을 release할 때까지 대기합니다.

두 트랜잭션 모두 무한대기 상태에 있다가 InnoDB가 2번 트랜잭션을 롤백시키고 1번 트랜잭션을 적용 시킵니다.

결론

  • SERIALIZABLE은 최고 수준의 격리 레벨이지만 레코드(Row) 락을 걸기 때문에 데드락 상태에 빠지기 쉽다.
  • SERIALIZABLE은 성능상의 이유로도 안쓰는 것이 좋다.
profile
왜? 다음 어떻게?

1개의 댓글

comment-user-thumbnail
2024년 4월 13일

좋은 인사이트를 얻고 갑니다. 감사합니다 :)

답글 달기