Django의 트랜잭션 Lock - SW의 Lock과 DB의 Lock 정리
1. SW의 Lock과 DB Lock 정의
- Software의 Lock은 Django 기준으로 Database에 Query를 요청하기 전, 후로 Lock을 사용해 race condition의 문제를 방지한다.
- 단점으로 문제 발생으로 인한 종료 및 재시작시 Lock 상태가 손실된다.
- Lock 상태를 계속 추적하여 지속적인 처리를 유지해야 하는 경우는 적절하지 않다.
- 게시판에 글을 쓰는 작업 등의 문제 발생시 다시 처리를 요구해도 문제가 없는 경우 사용할 수 있다.
- 기능적인 문제보다 정책적인 문제로 중요성이 다뤄질 수 있으며, 중간 저장 등의 방법으로 사용자의 문제를 개선할 수 있다.
- Databse의 Lock의 Django 기준으로 Databse에 요청한 Query가 도착한 시점부터 Lock이 실행되어 race condition의 문제를 방지한다.
- 장점으로 문제가 발생하여 Django가 종료 및 재실행 되더라도 Lock 상태가 유지된다.
- 단점으로 Databse에서 제공하는 Lock 중 어떤 것을 사용하는지에 따라 Lock 상태가 계속 유지되어 Databse의 row, table 단위의 지속적인 접근 제한이 유지되어 심각한 문제가 발생될 수 있다.
- SW Lock보다 유연하지 못하고 처리속도도 낮다. 하지만 장비의 스케일업과 config 설정으로 개선될 수 있다.
- 결제와 같이 처리 과정의 원자적 보호가 유지되어야 하는 경우 사용이 적합하다.
2. Django의 ORM 함수 중 Lock을 사용하는 함수들
1) select_for_update()
- select_for_update() 메서드는 트랜잭션 내에서 행(row)에 대한 잠금을 얻어, 선택된 행이 트랜잭션이 완료될 때까지 다른 트랜잭션에서 그 행을 수정하지 못하게 한다. 이는 "SELECT ... FOR UPDATE" SQL 문을 사용하여 구현된다.
from django.db import transaction
# 트랜잭션 블록 안에서 사용
with transaction.atomic():
# select_for_update()를 사용해 특정 객체 잠금
my_object = MyModel.objects.select_for_update().get(id=1)
# 이제 my_object를 안전하게 수정할 수 있음
my_object.some_field = '새로운 값'
my_object.save()
2) select_for_update(skip_locked=True)
- select_for_update()는 skip_locked 옵션도 제공한다. 이 옵션은 현재 다른 트랜잭션에 의해 잠긴 행을 건너뛰고, 잠금을 획득할 수 있는 행만 선택한다.
with transaction.atomic():
# 잠긴 행을 건너뛰고 잠금을 획득할 수 있는 행만 선택
rows = MyModel.objects.select_for_update(skip_locked=True).filter(some_field='value')
for row in rows:
# 처리 로직
pass
3) select_for_update(nowait=True)
- select_for_update(nowait=True) 옵션을 사용하면, 잠금을 얻을 수 없을 때 즉시 실패하고 django.db.utils.OperationalError 예외를 발생시킨다. 이는 잠금 대기 시간을 피하고자 할 때 유용할 수 있다.
with transaction.atomic():
try:
obj = MyModel.objects.select_for_update(nowait=True).get(pk=1)
# 업데이트 로직
except django.db.utils.OperationalError:
# 잠금을 얻을 수 없는 경우의 처리 로직
4) F() 객체
- F() 객체를 사용하면, 데이터베이스 레벨에서 필드 값을 직접적으로 수정할 수 있다. 이 방법은 레이스 컨디션을 방지하는 데 도움이 될 수 있다.
from django.db.models import F
# F() 객체를 사용해 데이터베이스 레벨에서 필드 값을 업데이트
MyModel.objects.filter(id=1).update(some_field=F('some_field') + 1)
5) update()
- update() 메서드는 쿼리셋의 모든 객체에 대해 지정된 필드 값을 업데이트한다. 이 메서드는 단일 SQL 문을 사용하여 대량의 업데이트를 수행하기 때문에 효율적이다. 하지만, update()는 모델의 save() 메서드를 호출하지 않기 때문에, pre_save나 post_save 시그널을 트리거하지 않는다.
# update()를 사용해 모든 객체에 대한 특정 필드 업데이트
MyModel.objects.filter(some_field='기존 값').update(some_field='새로운 값')
6) version or updated_at field 사용으로 낙관적 접근으로 동시성 해결 방법
- version 필드 혹은 updated_at 필드를 사용하여 update시 해당 필드가 변경 사항이 없다면, 수행하는 방식으로 동시성을 해결할 수 있다.
- django-concurrency 라이브러리에서 다양한 version field를 제공하고 있다.
- filter를 이용해 처음 필드 정보를 가져올 때 확인한 version을 다시 넣어 update할 필드에 접근 시도를 하면 version이 같을 경우 필드가 검출이되어 update가 가능해지고 version이 다르다면 update가 불가능해진다.
def reserve(self):
updated = Aircraft.objects.filter(
id=self.id,
version=self.version,
).update(
remaining_seat=remaining_seat-1,
version=self.version+1,
)
return updated > 0
3. 잠금과 동시성 처리를 위한 추가 팁
- 잠금을 사용할 때는 항상 transaction.atomic() 컨텍스트 매니저를 사용하여 변경 사항을 트랜잭션 안에서 처리하는 것이 좋다.
- select_for_update()는 transaction.atomic() 블록 내에서만 사용해야 한다.
- 트랜잭션 내에서 commit 또는 rollback으로 데이터 무결성 작업을 수행할 수 있지만, 트랜잭션 외부에서는 작업이 완료되지 않았음에도 레코드 잠금이 즉시 해제되어 다른 프로세스에서 해당 레코드를 수정할 수 있게 된다. 이로 인해 무결성 문제가 발생될 수 있다.
- 잠금이 필요 없는 간단한 업데이트에는 F() 객체와 update() 메서드를 사용하는 것이 좋다.
4. django-lockmgr(Software Lock) django-db-lock(Database Lock)
- Software Lock : 메모리 상에서 락을 관리하는 방식으로, 프로세스 간 락 공유가 가능하다. 대표적인 예로 django-lockmgr, django-db-locking 등이 있다.
- django-lockmgr url : https://pypi.org/project/django-lockmgr/
- 사용 방법 :
- 시나리오: 사용자가 게시글을 작성하는 동안 다른 사용자가 동일한 게시글을 편집하지 못하도록 락을 건다.
pip install django-lockmgr
# models.py
from django.db import models
from django_lockmgr.lockmgr import LockMgr
class Post(models.Model):
title = models.CharField(max_length=100)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
# 락 관리를 위한 LockMgr 인스턴스 생성
lock_mgr = LockMgr()
# views.py
from django.shortcuts import get_object_or_404, redirect, render
from .models import Post
def edit_post(request, post_id):
post = get_object_or_404(Post, id=post_id)
# 락 획득
with post.lock_mgr.lock():
if request.method == 'POST':
# 게시글 수정 로직
post.title = request.POST['title']
post.content = request.POST['content']
post.save()
return redirect('post_detail', post_id=post.id)
else:
return render(request, 'edit_post.html', {'post': post})
- with post.lock_mgr.lock() : 블록 내에서 락이 획득되며, 블록 종료 시 자동으로 락이 해제된다.
- 락이 이미 획득된 경우 : LockMgr는 블록 진입을 차단하여 다른 사용자의 편집을 방지한다.
- DB Lock : 데이터베이스 수준에서 락을 관리하는 방식으로, 데이터베이스 트랜잭션을 활용한다. 대표적인 예로 django-db-lock, django-mysql 등이 있다.
- django-db-lock url : https://pypi.org/project/django-db-lock/
- 사용 방법 :
- 시나리오: 사용자가 주문을 생성할 때 동시에 다른 사용자가 동일한 상품을 주문하지 못하도록 락을 건다.
pip install django-db-lock
# settings.py
INSTALLED_APPS = [
# ...
'django_db_lock',
]
# models.py
from django.db import models
from django_db_lock.models import NonBlockingLock
class Product(models.Model):
name = models.CharField(max_length=100)
price = models.DecimalField(max_digits=10, decimal_places=2)
stock = models.PositiveIntegerField()
# 락 관리를 위한 NonBlockingLock 상속
class Lock(NonBlockingLock):
pass
# views.py
from django.shortcuts import get_object_or_404, redirect, render
from .models import Product
from django_db_lock.models import lock
def create_order(request, product_id):
product = get_object_or_404(Product, id=product_id)
# 락 획득
with lock(Product.Lock, product.id):
if product.stock > 0:
# 주문 생성 로직
product.stock -= 1
product.save()
return redirect('order_list')
else:
return render(request, 'out_of_stock.html')
- with lock(Product.Lock, product.id) : 블록 내에서 락이 획득되며, 블록 종료 시 자동으로 락이 해제된다.
- 락이 이미 획득된 경우 : NonBlockingLock는 블록 진입을 차단하여 다른 사용자의 주문을 방지한다.
- reference :
- transaction에 대한 정리
https://velog.io/@combi_jihoon/Transaction-django-transaction.atomic-%EC%82%AC%EC%9A%A9
데이터베이스 | Transaction(+ django @transaction.atomic 사용)
@transaction을 공부하기 전에, 먼저 transaction의 올바른 정의가 무엇인지 알아야 할 필요가 있다.트랜잭션은 데이터베이스의 상태를 변화시키는 하나의 논리적 기능을 수행하기 위한 작업의 단위
velog.io
django transaction(장고 트랜잭션)
트랜잭션 트랜잭션이란 데이터베이스 관리 시스템 또는 유사한 시스템에서 상호작용의 단위입니다. 여기서 유사한 시스템이란 트랜잭션이 성공과 실패가 분명하고 상호 독립적이며, 일관되고
hyun-am-coding.tistory.com
Django DB Transaction 1편 — Request와 DB Transaction 묶기(Feat. ATOMIC_REQUESTS)
Introudction
medium.com
Django DB Transaction 2편 — 명시적으로 transaction활용하기. (feat. savepoint)
Introduction
medium.com
Django DB Transaction 3편 — DB Transaction Test 코드 작성하기.
Introduction
medium.com
DB Concurrency 어디까지 알고 있니
django 개발자와 함께 알아가기
techblog.yogiyo.co.kr
https://chrisjune-13837.medium.com/django-row-lock-%EB%8F%99%EC%9E%91%EB%B0%A9%EC%8B%9D-a2e05bb0eb90
[Django] Row Lock 동작방식
장고 프레임워크에서 ORM의 row lock 테스트와 동작방식을 공유합니다.
chrisjune-13837.medium.com