Django Web Framework/Django 일반

Django의 트랜잭션 Lock - SW의 Lock과 DB의 Lock 정리

bluebamus 2024. 5. 14. 23:07

 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

https://hyun-am-coding.tistory.com/entry/django-transaction%EC%9E%A5%EA%B3%A0-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98

 

django transaction(장고 트랜잭션)

트랜잭션 트랜잭션이란 데이터베이스 관리 시스템 또는 유사한 시스템에서 상호작용의 단위입니다. 여기서 유사한 시스템이란 트랜잭션이 성공과 실패가 분명하고 상호 독립적이며, 일관되고

hyun-am-coding.tistory.com

https://medium.com/@doosikbae/django-db-transaction-1%ED%8E%B8-request%EC%99%80-db-transaction-%EB%AC%B6%EA%B8%B0-feat-atomic-requests-5bf976d2a33c

 

Django DB Transaction 1편 — Request와 DB Transaction 묶기(Feat. ATOMIC_REQUESTS)

Introudction

medium.com

https://medium.com/@doosikbae/django-db-transaction-2%ED%8E%B8-%EB%AA%85%EC%8B%9C%EC%A0%81%EC%9C%BC%EB%A1%9C-transaction%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0-feat-savepoint-53075301b3f5

 

Django DB Transaction 2편 — 명시적으로 transaction활용하기. (feat. savepoint)

Introduction

medium.com

https://medium.com/@doosikbae/django-db-transaction-3%ED%8E%B8-db-transaction-test-%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0-e7363a084834

 

Django DB Transaction 3편 — DB Transaction Test 코드 작성하기.

Introduction

medium.com

https://techblog.yogiyo.co.kr/db-concurrency-%EC%96%B4%EB%94%94%EA%B9%8C%EC%A7%80-%EC%95%8C%EA%B3%A0-%EC%9E%88%EB%8B%88-559bfc4f59ee

 

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