-
update_or_create()하는데 갑자기 Dead Lock 너는 왜나오냐?! (feat. transaction)💻 프로그래밍/Django 2020. 10. 26. 20:01
안녕하세요! 운동하는 개발자 제이입니다!
오늘은 Dead Lock과 Transaction에 대해서 알아보려고 합니다!
회사에서 종종 위 이미지와 같은 센트리 리포팅을 볼 수 있었는데, Deadlock (교착상태)에 대해서 개념은 알고 있었지만 실제로 코드상에서 왜 Deadlock이 나는지 정확한 이유를 알진 못했습니다.
시니어 개발자분께서 transacion log를 보여주시면서 설명은 해주셨는데, 그냥 그렇구나 하고 넘어갔던 것 같아요(이러면 안 됩니다 여러분 ㅎㅎ) 최근에 DB에 대해서 좀 더 관심이 생겨서 이 Deadlock에 대해서 실제 django orm을 보면서 알아보려고 해요.
1. Dead-lock 그리고 Transaction
일단 간단히 Deadlock이 뭔지 transaction이 뭔지 알아보겠습니다.
📌 교착 상태 (膠着狀態, 영어: deadlock)
두 개 이상의 작업이 서로 상대방의 작업이 끝나기 만을 기다리고 있기 때문에 결과적으로 아무것도 완료되지 못하는 상태를 가리킨다. (출처 : 위키백과) 간단히 말하자면 두개의 작업에 동일한 리소스가 할당되어, 어느 한쪽의 작업이 끝날 때까지 무한정 기다리게 되는 것을 말합니다.📌 데이터베이스 트랜잭션 (Database Transaction)
데이터베이스 관리 시스템 또는 유사한 시스템에서 상호작용의 단위이다. 여기서 유사한 시스템이란 트랜잭션이 성공과 실패가 분명하고 상호 독립적이며, 일관되고 믿을 수 있는 시스템을 의미합니다. 이론적으로 데이터베이스 시스템은 각각의 트랜잭션에 대해 원자성(Atomicity), 일관성(Consistency), 독립성(Isolation), 영구성(Durability)을 보장한다. 이 성질을 첫 글자를 따 ACID라 부른다. 그러나, 실제로는 성능향상을 위해 이런 특성들이 종종 완화되곤 한다.(출처 : 위키백과)어떤 시스템들에서는 트랜잭션들은 논리적 작업 단위(LUW, Logical Units of Work)로 불립니다.
Data Base에서는 이런 ACID 원칙을 엄격하게 지키면 동시성이 떨어지기 때문에 이걸 조절(?)할 수 있는 transaction isolation level이 있습니다. 이 level에 따라서 lock을 거는 범위가 달라집니다.
isolation level에는 UNCOMMITTED, READ COMMITTED, REPEATABLE READ, and SERIALIZABLE. 이 있습니다.
각각에 대해 자세하게 설명하고 싶지만, 다른 분이 잘 정리해주신 내용이 있어서 이 글을 공유하겠습니다.
(참고: https://suhwan.dev/2019/06/09/transaction-isolation-level-and-lock/)
본문에서는 MySQL innoDB 기본 설정인 REPEATABLE READ에 대해서 간단하게 설명하고 예제를 실행하겠습니다.
📌 REPEATTABLE READ
1. InnoDB의 기본 default isolation level입니다.
2. lock을 사용하는 select나 update, delete 쿼리를 실행할 때 REPEATTABLE READ transaction은 gap lock을 사용한다. 현재 내가 조작하려는 row를 다른 transaction이 조작 못하게 합니다.
3. gap locks와 next-key locks도 사용합니다. next-key locks은 “supremum”라 불리는 임시의 레코드 다음에 있는 갭을 잠급니다."supremum" 레코드 레코드는 인덱스에서 실제 존재하는 값들 보다 더 큰 값을 가집니다. “supremum” 은 진짜 인덱스 레코드가 아닙니다. 그래서 이 next-key locks는 가장 큰 인덱스 값 다음에 오는 Gap만 잠급니다.
(참고: https://singun.github.io/2019/03/10/mysql-innodb-locking/)
Deadlock과 transaction에 대해서 더 설명하려면 분량이 너무 많아지기 때문에 위 블로그들을 참고하길 바랍니다 ㅎㅎ 저희는 여기서 중요한 포인트만 가져와서 django ORM 동작에서 확인해보겠습니다.
2. update_or_create()를 해보자!
먼저 예제를 한번 보겠습니다.
1234567891011121314151617181920212223242526272829303132# -*- coding: utf-8 -*-from multiprocessing import Processfrom django.db import transactionfrom apps.users.models import UserTEST_DATA = {'password': '12345676','name': '테스트 유저','phone': '01012345678',}def test_function(username=None):if username is None:returnwith transaction.atomic(): # 트랜젝션을 건다 (중간에 잘못되면 롤백하기 위해)User.objects.select_for_update().update_or_create(username=username,defaults=TEST_DATA)def run():# 동시 실행 테스트를 위해 멀티 프로세싱 사용th1 = Process(target=test_function, args=('fitware_jay1',))th2 = Process(target=test_function, args=('fitware_jay2',))th1.start()th2.start()th1.join()th2.join()test_function()의 내용을 보면 transaction을 걸고 User 모델에 username인 데이터가 있으면 select_for_update()에 의해 해당 row에 lock을 걸고 update, 모델이 없는 경우 create를 하게 되며, 실제로 SELECT .... WHERE `users_user`.`username` = 'fitware_jay1' FOR UPDATE; args=('fitware_jay1',) 이런 식의 쿼리가 나가게 됩니다.
언뜻 보면 크게 문제가 되지 않을 수도 있지만, test_funstion()가 동시에 여러 번 실행하는 경우 문제가 생깁니다.
📝 시나리오: fitware_jay1, fitware_jay2 username을 가진 User가 존재하지 않을 때 동시에 두 함수가 호출되는 경우 (테스트 스크립트에서는 멀티 프로세스로 진행했습니다.)
시나리오대로 동작하는 경우 과연 어떻게 결과가 나올까요?
두둥 탁!! Deadlock이 발생했습니다. 이번 글에서 설명하고자 하는 포인트입니다!!! 과연 Deadlock이 왜 발생했을까요?
mysql에 접속해서 SHOW ENGINE INNODB STATUS 명령어로 InnoDB의 상태(output)를 확인해 보겠습니다.
명령어를 치면 여러 정보들이 나오는데 그중 LATEST DETECTED DEADLOCK 정보를 확인합니다. 첨부된 이미지에서 보다시피 서로 다른 transaction이 같은 row에 lock을 걸고 있는 걸 확인할 수 있습니다. 그래서 Deadlock이 걸렸던 것입니다. 그리고 실제로 둘 중 하나의 transaction 만 정상적으로 처리되었습니다.
위 동작에 대해서 포인트를 설명하자면,
1. InnoDB에서는 Predicate Locking을 지원하지 않아 Supremum record(존재하지 않는 record)에 lock을 겁니다.
(* row = record)
2. Supremum record는 Gap Lock을 사용하는데 Gap Lock은 같은 record에 여러 transaction이 걸 수 있습니다. (Insert 만 안됩니다)
3. 2번의 내용 때문에 서로 Lock이 해제되기를 기다리다가 Dead Lock에 빠지고 MySQL은 이것을 감지하고 하나의 transaction만 남기고 나머지를 강제로 Roll Back 시킵니다.(참고: https://youngminz.netlify.app/posts/get-or-create-deadlock#mysql-internal)
이게 결론은 두 개의 요청이 update_or_create() 하다가 User가 없어서 둘 다 새로 User를 생성하려고 동일한 Supremum record에 lock을 걸어 Dead Lock이 발생한 것입니다.
그럼 어떻게 해결해야 할까요?
Lock을 걸지 않으면 됩니다. -> update_or_create() 쓰지 마세요!! ㅋㅋㅋ <- 오늘 진짜 진짜 말하고 싶었던 내용!!! update_or_create()는 Dead Lock을 발생시킬 수 있습니다!!(정정 2022.04.25)
update_or_create()를 사용해도 됩니다. 다만 select_for_update()를 함께 사용하면 안됩니다. select_for_update()는 gap lock이 걸려 dead lock을 유발할 수 있습니다.3. Dead Lock 없이 update_or_create() 대체하기
12345678910111213141516171819202122232425262728293031323334353637# -*- coding: utf-8 -*-from multiprocessing import Processfrom django.db import transactionfrom apps.users.models import UserTEST_DATA = {'password': '12345676','name': '테스트 유저','phone': '01012345678',}def test_function(username=None):if username is None:returnwith transaction.atomic(): # 트랜젝션을 건다 (중간에 잘못되면 롤백하기 위해)try:user = User.objects.filter(username=username).get() # 먼저 select로 User를 가져온다.user.__dict__.update(TEST_DATA) # 인스턴스의 네임스페이스들을 업데이트user.save() # 변경된 내용 저장except User.DoesNotExist: # User가 존재하지 않는 경우TEST_DATA['username'] = usernameUser.objects.create(**TEST_DATA) # 새로운 Userdef run():th1 = Process(target=test_function, args=('fitware_jay1'))th2 = Process(target=test_function, args=('fitware_jay2'))th1.start()th2.start()th1.join()th2.join()변경된 코드는 이렇습니다!!! 코드를 보면 lock을 거는 게 아니라 get()으로 User가 존재하는지 체크를 하고 존재하면 save(), 존재하지 않는 경우 create() 해줍니다!! 이렇게 하면 Dead Lock 없이 update_or_create() 기능을 구현할 수 있습니다.
3. 마치며
오늘 update_or_create()를 보셨다시피 ORM을 너무 믿어서는 안 됩니다! 실제로 우리가 생각하는 데로 동작이 되지 않을 수가 있어요 ㅎㅎ 사실 저도 SQL에 대해서 잘 알지 못했고, ORM으로만 쓰다 보니... 내가 제대로 모르고 쿼리를 날리고 있었구나라는 생각을 하게 되더라고요. 그래서 요즘에 DataBase에 더 관심을 가지게 되었고 이런 부분에 대해서 좀 더 신경 쓰려고 노력하는 것 같습니다!
사실 DataBase, Transaction, Dead Lock에 대해 깊은 내용을 다루기는 분량도 그렇고, 제가 아직 공부할 범위가 너무 많아서 일부분에 대해서만 설명했는데, 꼭 위 세 가지에 제가 링크로 남긴 블로그들 보시면서 공부해보시길 추천드립니다!! ㅎㅎ 그럼 오늘도 좋은 하루 되시고 즐거운 코딩 하세요!!
[참고 블로그]
https://youngminz.netlify.app/posts/get-or-create-deadlock
https://suhwan.dev/2019/06/09/transaction-isolation-level-and-lock/
'💻 프로그래밍 > Django' 카테고리의 다른 글
Django 비즈니스 로직을 분리하는 4가지 방법 (1) 2021.02.13 Django, Nginx CORS 설정 (2) 2021.01.17 [개념] Django는 Web Server가 아니라구요!! (1) 2020.08.10 Django 에서 Celery로 비동기 worker 실행하기 (0) 2019.12.23 [Pytest] TDD를 해야하는 이유 (django TDD 적용하기) (0) 2019.12.08