Django-da Race Condition Muammosi: Real Case Study va Yechim

Muallif: Javohir Abdullayev · · Django

Django-da Race Condition Muammosi: Real Case Study va Yechim

Kirish: Kichik Xato Qanday Qilib Katta Zararga Olib Keldi?

Tasavvur qiling, siz elektron tijorat (e-commerce) yoki chipta sotish platformasini boshqaryapsiz. Tizimda aksiyadagi mahsulotdan faqat 1 dona qolgan. Bir soniya ichida ikki xil xaridor 'Sotib olish' tugmasini bosadi. Natijada nima sodir boladi? Ikkala foydalanuvchining ham kartasidan pul yechiladi, bazada esa mahsulot soni 0 orniga -1 bolib qoladi. Sizda esa bitta mahsulot va ikkita norozi mijoz bor.

Ushbu holat dasturlashda Race Condition (poyga holati) deb ataladi. Ushbu case study orqali biz real loyihada ushbu muammo qanday paydo bolganini, nima uchun oddiy Python tekshiruvlari buni toxtata olmasligini va Django yordamida uni qanday professional tarzda bartaraf etishni organamiz.

Muammoning Anatomiyasi: Read-Modify-Write Sikli

Odatda yangi boshlagan dasturchilar balansni tekshirish va yechish logikasini quyidagicha yozishadi:

def transfer_funds(account_id, amount):
    account = Account.objects.get(id=account_id)
    if account.balance >= amount:
        account.balance -= amount
        account.save()
        return True
    return False

Ushbu kod lokal kompyuterda yoki past yuklamada mukammal ishlaydi. Ammo haqiqiy serverda (masalan, Gunicorn yoki Uvicorn bir nechta worker bilan ishlaganda) bir vaqtning ozida ikkita sorov kelsa, quyidagi xronologiya sodir boladi:

  • Sorov A: Bazadan balansni oqiydi (balans = 100 000 som).
  • Sorov B: Bir vaqtda xuddi shu hisobni oqiydi (balans hali ham 100 000 som).
  • Sorov A: 80 000 som yechishni tekshiradi (100 000 >= 80 000 - togri), yangi balansni 20 000 qilib saqlaydi.
  • Sorov B: U ham ozidagi eski malumot (100 000 som) orqali 80 000 som yechishni tekshiradi va u ham muvaffaqiyatli deb hisoblab, balansni 20 000 som deb qayta saqlaydi.

Natijada, hisobdan 160 000 som sarflandi, biroq bazada balans 20 000 som bolib qoldi. Bu loyiha egasi uchun togridan-togri moliyaviy yoqotishdir.

Yechim 1: Django F() Obektlari Orqali Atomik Yangilanish

Eng sodda holatlarda (masalan, kesh hisoblagichlari yoki oddiy ayirish amallari) Python xotirasidagi qiymatga tayanmasdan, yangilashni bevosita malumotlar bazasiga topshirish lozim. Buni Djangodagi F() ifodalari yordamida amalga oshirish mumkin:

from django.db.models import F

Account.objects.filter(id=account_id, balance__gte=amount).update(
    balance=F('balance') - amount
)

Bu yerda SQL darajasida UPDATE account SET balance = balance - 80000 WHERE id = 1 AND balance >= 80000 buyrugi bajariladi. Agar bir vaqtda ikkita sorov kelsa, malumotlar bazasi ikkinchi sorov kelganda shart bajarilmasligini tushunadi va 0 ta qatorni yangilaydi. Bu oddiy holatlar uchun juda samarali.

Yechim 2: Pessimistik Bloklash (select_for_update)

Biroq hamma biznes logika faqat bitta update() buyrugiga sigmaydi. Kop hollarda siz hisobni tekshirishingiz, tashqi tolov API chaqirishingiz, hisob-faktura yaratishingiz va shundan song balansni ozgartirishingiz kerak boladi. Bunday murakkab holatlarda Pessimistik bloklash (Pessimistic Locking) usuli qollaniladi.

Django-da bu vazifani select_for_update() metodi va tranzaksiyalar (transaction.atomic) bajaradi:

from django.db import transaction
from django.core.exceptions import ValidationError

def secure_withdraw(account_id, amount):
    with transaction.atomic():
        account = Account.objects.select_for_update().get(id=account_id)
        if account.balance < amount:
            raise ValidationError('Mablag yetarli emas!')
        
        account.balance -= amount
        account.save()
        
        # Boshqa muhim biznes amallar
        TransactionHistory.objects.create(
            account=account,
            amount=amount,
            action_type='withdraw'
        )
    return account

Bu kod qanday ishlaydi?

select_for_update() SQL darajasida SELECT ... FOR UPDATE buyrugini yuboradi. Ushbu qator PostgreSQL yoki MySQL bazasida bloklanadi (lock qoyiladi). Boshqa har qanday parallel sorov ushbu qatorni oqish yoki ozgartirish uchun birinchi tranzaksiya tugashini (commit yoki rollback) kutishga majbur boladi. Shu tariqa ikkinchi sorov doimo birinchi sorov amalga oshirgan eng songgi balansni koradi.

Deadlock (Turgunlik) Xavfi va Undan Himoyalanish

Bloklashlar ishlatilganda yangi xavf yuzaga keladi: Deadlock. Masalan, 1-foydalanuvchi A hisobdan B hisobga pul otkazayotganda, bir vaqtning ozida 2-foydalanuvchi B hisobdan A hisobga pul otkazsa, har ikkala tranzaksiya bir-birining qatorlarini bloklab qotib qolishi mumkin.

Buning oldini olish uchun 3 ta asosiy qoidaga amal qilish shart:

  1. Resurslarni doimo bir xil tartibda bloklang: Masalan, har doim kichik ID raqamiga ega obyektni birinchi, katta ID raqamdagi obyektni keyin bloklang.
  2. nowait parametrini qollang: select_for_update(nowait=True) orqali navbatda cheksiz kutishning oldini olib, tezda xatolik qaytarish mumkin.
  3. Tranzaksiyalarni qisqa tuting: Tranzaksiya ichida ogir HTTP sorovlar yoki vaqt oluvchi hisob-kitoblarni amalga oshirmang.

Xulosa

Race condition kam uchraydigan, lekin oqibati juda qimmatga tushadigan arxitektura xatosidir. Agar siz tolov tizimlari, chipta bron qilish yoki inventar boshqaruvi bilan ishlayotgan bolsangiz, oddiy shart operatorlariga tayanmang. Django-ning F() ifodalari va select_for_update() vositalaridan orinli foydalanish loyihangizni yuqori yuklamalarda ham ishonchli va barqaror qiladi.

Teglar: #Django #Python #PostgreSQL #Backend #Database