개발놀이터

MySQL에 사는 유령 본문

CS 지식/데이터베이스

MySQL에 사는 유령

마늘냄새폴폴 2024. 11. 9. 17:34

MySQL의 InnoDB 스토리지 엔진의 기본 격리수준은 REPEATABLE READ입니다. 하지만 REPEATABLE READ에서 발생할 수 있는 문제 중 하나인 Phantom Read를 피할 수는 없는데요. 

 

MySQL은 각종 locking 매커니즘을 이용해서 Phantom Read를 막을 수 있었다고 MySQL팀은 설명했습니다. 하지만 무조건 Phantom Read가 발생하지 않는 것은 아닙니다. 

 

이번 포스팅에선 MySQL에 사는 유령에 대해서 공부해보고 정리해봤습니다. 

 

Phantom Read

우선 Phantom Read에 대해서 간단하게 짚고 넘어가도록 하겠습니다. 

 

어떤 상황이 Phantom Read를 발생시킬까요?

 

  1. A트랜잭션이 SELECT 쿼리를 날린다
  2. B트랜잭션이 이후 INSERT 쿼리를 날린다
  3. A트랜잭션이 다시 SELECT 쿼리를 날린다
  4. A트랜잭션에서 B트랜잭션이 삽입한 데이터가 보인다. 

이 경우 우리는 Phantom Read가 발생했다. 라고 말합니다. 

 

 

하지만 Phantom Read는 얼핏 보기엔 굉장히 자연스러워 보이기도 합니다. 

 

왜냐하면 데이터가 추가되었으니까요. 

 

하지만 트랜잭션의 관점에서 이는 위험할 수 있습니다. RDBMS는 트랜잭션의 ACID아래서 굉장히 높은 신뢰성을 가지고 군림해왔습니다. 그 중 I에 의하면 서로 다른 트랜잭션은 격리되어야하죠. 

 

격리되지 않으면 일관성이 깨지게 되는데 예를 들어서 20개를 예상했는데 21개가 나온다거나 하는 상황이 발생할 수 있고 이것은 잠재적으로 문제가 될 수 있습니다. 

 

MySQL의 record lock, gap lock, next key lock

하지만 MySQL에서는 record lock, gap lock과 그것을 발전시킨 next key lock을 이용해서 Phantom Read가 발생하지 않는다고 알려져있습니다. 

 

https://coding-review.tistory.com/302

 

Phantom Read 부정합문제 해결방안 In Mysql

이번 포스팅에서는 Mysql에서 Phantom Read 부정합 문제를 어떻게 해결하고 있는지에 대해서 알아보도록 하겠습니다. 굳이 Mysql에서 라고 글을 쓴 이유는 공식문서를 여러개 찾아보던 중에 PostgreSQL과

coding-review.tistory.com

제가 예전에 정리한 글인데 여기에 자세히 나와있습니다. 

 

이번 포스팅에선 간략하게만 짚고 넘어가겠습니다. 

 

record lock은 row에 락을 거는 것이고 gap lock은 범위에 락을 거는 것입니다. 그리고 이 둘을 합친 것이 next key lock이죠. 합쳤다기 보단... 기본 골자는 next key lock이고 상황에 따라 record lock을 걸 것인지 gap lock을 걸 것인지 정하는 것으로 알고있습니다. 

 

예를 들어서 where 조건으로 하나만 걸었으면 record lock이 되고 between 같은 조건으로 여러개를 걸었다면 gap lock이 되는 그런 식이죠. 

 

정말 Phantom Record가 안보일까?

 

우선 두개의 터미널을 준비하고 데이터를 하나 넣어놨습니다. 

 

그리고 양쪽 모두에 트랜잭션을 시작하고 실습해보겠습니다. 

 

 

한쪽에서는 트랜잭션을 시작하고 Bob이라는 행을 추가했을 때 데이터가 두개 보이는 모습입니다. 

 

 

하지만 다른 한쪽에선 데이터가 보이지 않네요? 

 

이로써 Phantom Read가 발생하지 않는다는 것을 알 수 있습니다. 

 

 

MySQL에 사는 유령

INSERT를 하지 않은 쪽에서 쿼리를 재실행했을 때 데이터가 보이지 않았습니다. 

 

그럼 이건 어떨까요? 

 

 

INSERT를 한 쪽에서 commit을 하고 

 

 

오잉? 기존 쿼리는 안보이는데 for update 쿼리를 날리니까 데이터가 보이네요? 

 

for update 쿼리는 흔히 알려지기로 락을 획득하는 쿼리라고 알려져있습니다. 보통 exclusive lock을 획득할 때 for update 쿼리를 사용하죠. 

 

https://coding-review.tistory.com/421

 

JPA의 동시성 컨트롤 (낙관적 락, 비관적 락)

이번 포스팅에선 JPA가 어떻게 동시성 문제를 컨트롤하는지에 대해서 포스팅해보겠습니다. 개인적으로 동시성 문제는 애플리케이션에서 일어날 수 있는 가장 최악의 버그라고 생각합니다. 그

coding-review.tistory.com

JPA에선 비관적 락을 사용하면 자동으로 날아가는 쿼리가 for update 쿼리입니다. 

 

for update 쿼리를 날리니까 Phantom Read가 발생했습니다. 방금 MySQL에서는 gap lock인지 뭐시긴지로 Phantom Read가 발생하지 않는다면서요? for update는 뭐가 다른걸까요? 

 

FOR UPDATE 쿼리

exclusive lock을 획득하기위해 for update 쿼리를 날린다면 그 즉시 최근 데이터를 불러오게 설정되어있습니다. 

 

MySQL팀에서는 이것이 역설적이게도 일관성을 유지하는 방법이라고 하는데요. Phantom Read가 발생하면 일관성이 깨질 수도 있다고 하지 않았나? 갑자기 이게 뜬금없이 무슨소리야?

 

이런 상황을 가정해보겠습니다. 

 

  1. Tom이라는 데이터가 존재한다.
  2. A트랜잭션과 B트랜잭션이 시작한다. 
  3. A트랜잭션에서 SELECT쿼리를 날린다
  4. B트랜잭션에서 INSERT쿼리를 날린다
  5. A트랜잭션에서 업데이트를 하고싶다. 

만약 for update 쿼리에 최근 데이터가 조회되지 않는 상황이라면 다음과 같은 문제가 발생할 수 있습니다. 

 

이름이 Tom이고 나이가 20살인 청년의 돈은 100원이 있다. B트랜잭션에서 Bob이라는 청년을 추가했다. 나이는 똑같이 20살에 돈은 100원. 

 

나이가 20살인 사람의 돈을 1000원으로 올리고 싶은 상황

 

Tom의 돈은 1000원이 됐는데 Bob의 돈은 아직 100원입니다. 

 

정말 일관성이 깨져버렸네요? 

 

즉, 이런 상황 때문에라도 for update 쿼리는 최신 데이터를 가져와야합니다. 이것이 일관성을 유지하는 방법이었던 것이죠. 

 

 

FOR UPDATE쿼리의 문제

for update 쿼리가 단순히 Phantom Read만 발생시킨다는 것이 문제가 아닙니다. gap lock의 특성 때문에 for update쿼리는 문제가 될 수 있습니다. 

 

만약 for update 쿼리를 실행할 때 존재하지 않는 행을 for update로 lock을 걸어버리면 문제가 발생할 수 있습니다. 

 

위의 예시의 경우 id가 1, 2 만 존재하는 상황인데 만약 제가 아래와 같은 쿼리를 날린다면?

 

SELECT * FROM USER u WHERE u.id = 3 FOR UPDATE;

 

id가 3인 행은 존재하지 않습니다. 이런 경우 gap 락의 특성상 조건문에 걸려있는 값 (3) 부터 무한행까지 락이 걸려버립니다. 

 

이 말은 exclusive lock을 3~무한 까지 걸어버리겠다는 의미이고 다시 말해서 해당 트랜잭션이 커밋을 하기 전까지 어떤 값도 조회 혹은 삽입, 변경, 삭제가 불가능 하다는 말입니다. 

 

이 상황은 서버의 굉장한 성능 저하가 발생할 수 있습니다. 또한, 데드락의 문제도 빈번하게 발생하겠지요. 

 

그래서 이 상황을 해결하기 위해서 for update는 무작정 사용하면 안됩니다. 

 

SELECT * FROM USER u WHERE u.id = 3;

if row {
	SELECT * FROM USER u WHERE u.id = 3 FOR UPDATE;
    // INSERT, UPDATE, DELETE 작업
}
else {
	// 락을 획득하지 않고 바로 작업
}

 

이런식으로 단계를 거쳐서 사용해야합니다. 

 

마치며

저도 제 프로젝트에서 단순히 비관적 락 (FOR UPDATE) 을 사용하는게 좋다고 생각했던 이유가 재고가 2개 남았는데 3명이 동시에 누른다면 한명은 주문이 되면 안되고 주문 데이터가 두개만 쌓여야해! 그 상황을 백퍼센트 막을 수 있는 비관적 락을 채택해야해!

 

라고 생각했지만 이번에 공부하면서 생각이 조금은 바뀌었습니다. 제가 참고한 글에서도 for update를 쓰려면 진짜 이곳에서 써야하는 것인지 생각해보고 위와 같은 뎁스를 두어서 안전하게 사용해야하는 조금은 위험한 쿼리인 것 같습니다. 

 

저를 포함한 주니어 개발자분들을 낚기 딱 좋은 내용이었습니다. 단순히 MySQL은 next key lock으로 Phantom Read 발생 안해! 라고 알고 계셨던 분들은 이번 내용이 도움이 많이 되었을 것 같습니다. 

 

이번 포스팅은 여기서 마무리지어보도록 하겠습니다. 긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요!

 

출처

https://mysqlquicksand.wordpress.com/2019/12/20/select-for-update-on-non-existent-rows/

 

SELECT … FOR UPDATE on non-existent rows

TL; DR SELECT … FOR UPDATE has a (not so) surprising side effect on non-existent rows: it could cause a (serious) performance penalty and even prevent you from inserting new rows at all. Lock…

mysqlquicksand.wordpress.com

https://planetscale.com/blog/mysql-isolation-levels-and-how-they-work

 

MySQL isolation levels and how they work — PlanetScale

Learn about the various isolation levels used by MySQL to allow concurrency in your database.

planetscale.com