
transaction.atomic() چه چیزی را حفظ میکند؟یکی از اولین ابزارهایی که در Django برای مدیریت چند عملیات دیتابیس به ذهنمان میرسد، transaction.atomic() است.
اما یک سؤال مهم وجود دارد:
آیا atomic() واقعاً همهچیز را امن میکند؟
خیر.
transaction.atomic() ابزاری برای ایجاد مرز تراکنش دیتابیس (Transaction Boundary) است. یعنی چند تغییر دیتابیسی را طوری در کنار هم قرار میدهد که یا همه با موفقیت ثبت شوند، یا در صورت بروز خطا، تغییرات انجامشده داخل آن تراکنش rollback شوند.
مثلاً:
from django.db import transaction with transaction.atomic(): order = Order.objects.create( customer=customer, total=100, ) Payment.objects.create( order=order, amount=100, )
اگر ایجاد Payment با خطا مواجه شود، تغییر ایجادشده برای Order نیز میتواند rollback شود.
اما نکته مهم اینجاست:
atomic() بهتنهایی جلوی Race Condition را نمیگیرد، Row Lock ایجاد نمیکند و عملیات خارجی مثل API، پیامک یا پرداخت را rollback نمیکند.
atomic()با Race Condition چه تفاوتی دارد؟فرض کنید موجودی یک محصول برابر ۱ است:
stock = 1
دو درخواست همزمان وارد سیستم میشوند و هر دو این کد را اجرا میکنند:
with transaction.atomic(): product = Product.objects.get(pk=product_id) if product.stock > 0: product.stock -= 1 product.save()
ممکن است هر دو درخواست مقدار stock = 1 را ببینند.
در نتیجه هر دو تصور میکنند محصول موجود است.
اینجا atomic() بهتنهایی مشکل را حل نکرده است.
برای چنین سناریوهایی ممکن است به Row Locking نیاز داشته باشیم:
with transaction.atomic(): product = ( Product.objects .select_for_update() .get(pk=product_id) ) if product.stock <= 0: raise OutOfStock() product.stock -= 1 product.save(update_fields=["stock"])
اینجا دو ابزار دو وظیفه متفاوت دارند:
atomic() ↓ Transaction Boundary select_for_update() ↓ Row Locking / Concurrency Control
این دو را نباید با یکدیگر اشتباه گرفت.
atomic()به معنی Lock کردن همهچیز نیستیکی از اشتباهات رایج این است که تصور کنیم:
with transaction.atomic(): ...
یعنی تمام دادههایی که در این بخش میخوانیم به صورت خودکار lock شدهاند.
اینطور نیست.
اگر مسئله شما این است که چند درخواست همزمان روی یک رکورد تصمیمگیری میکنند، باید مشخص کنید که چه نوع Concurrency Control لازم دارید.
گاهی select_for_update() مناسب است.
گاهی استفاده از F() expression بهتر است:
from django.db.models import F updated = ( Product.objects .filter( pk=product_id, stock__gt=0, ) .update(stock=F("stock") - 1) ) if updated == 0: raise OutOfStock()
و در بعضی موارد نیز Database Constraint بهترین راهحل است.
فرض کنید ایمیل کاربر باید unique باشد.
این کد بهتنهایی کافی نیست:
if not User.objects.filter(email=email).exists(): User.objects.create(email=email)
دو درخواست همزمان میتوانند هر دو ببینند که کاربری با این ایمیل وجود ندارد.
راه درست این است که دیتابیس نیز این invariant را enforce کند:
class User(models.Model): email = models.EmailField(unique=True)
بهطور کلی:
Transactions + Database Constraints + Concurrency Control
هرکدام وظیفه متفاوتی دارند.
atomic() و Savepointatomic() را میتوان به صورت تو در تو استفاده کرد:
with transaction.atomic(): create_order() with transaction.atomic(): create_payment()
اما atomic() داخلی معمولاً یک Transaction مستقل ایجاد نمیکند.
در چنین شرایطی Django از Savepoint استفاده میکند.
یعنی چیزی شبیه این:
One Transaction │ ├── create order │ ├── Savepoint │ ├── create payment │ └── Final Commit
بنابراین موفقیت atomic() داخلی به معنی Commit نهایی نیست.
اگر Transaction خارجی rollback شود، تغییرات داخلی نیز میتوانند rollback شوند.
IntegrityError باشیدیکی دیگر از اشتباهات رایج، گرفتن IntegrityError داخل یک atomic block و ادامه دادن کار در همان Transaction است:
with transaction.atomic(): try: create_payment() except IntegrityError: pass create_order()
بعد از خطای دیتابیس، Transaction ممکن است در وضعیت rollback قرار گرفته باشد و ادامه دادن Queryها در همان Transaction میتواند باعث TransactionManagementError شود.
معمولاً بهتر است مرز atomic() را اطراف عملیاتی قرار دهیم که ممکن است fail شود:
try: with transaction.atomic(): create_payment() except IntegrityError: handle_payment_error()
ساختار try/except و atomic() در مدیریت Transaction اهمیت زیادی دارد.
atomic()نمیتواند API خارجی را Rollback کنداین شاید مهمترین محدودیت باشد.
مثلاً:
with transaction.atomic(): order = create_order() charge_credit_card()
فرض کنید پرداخت با موفقیت انجام شود اما بعد از آن یک عملیات دیتابیس fail شود.
Django میتواند تغییرات دیتابیس را rollback کند.
اما نمیتواند درخواست موفقی که به Payment Provider ارسال شده را rollback کند.
چون Payment Provider بخشی از Transaction دیتابیس شما نیست.
همین موضوع درباره موارد زیر نیز صدق میکند:
HTTP API
SMS
Payment Gateway
File Storage
سرویسهای خارجی
سایر Microserviceها
برای چنین سیستمهایی باید به مفاهیمی مثل Idempotency، Retry، State Machine و Reconciliation فکر کنیم.
transaction.on_commit()برای Side Effectهایی که باید فقط بعد از Commit موفق اجرا شوند، transaction.on_commit() بسیار مفید است.مثلاً به جای:
with transaction.atomic(): order = create_order() send_notification(order)
میتوانیم بنویسیم:
with transaction.atomic(): order = create_order() transaction.on_commit( lambda: send_notification(order) )
حالا Notification بعد از Commit موفق اجرا میشود.
این موضوع برای Celery نیز بسیار مهم است:
with transaction.atomic(): order = create_order() transaction.on_commit( lambda: process_order.delay(order.pk) )
در غیر این صورت ممکن است Worker قبل از Commit شدن Transaction شروع به پردازش Order کند.
این الگو معمولاً انتخاب خوبی نیست:
with transaction.atomic(): order = create_order() response = requests.post( payment_url, json={"amount": order.total}, ) create_payment_record(response)
چون Transaction دیتابیس در تمام مدت انتظار برای Network Request باز میماند.
ممکن است API در چند میلیثانیه پاسخ دهد یا چند ثانیه طول بکشد.
هرچه Transaction طولانیتر باشد، احتمال افزایش contention و مصرف منابع دیتابیس بیشتر میشود.
در بسیاری از سیستمها بهتر است معماری به این شکل باشد:
Short Database Transaction ↓ Commit ↓ External Operation ↓ Retry / Reconcile / Update State
به جای اینکه فکر کنیم:
atomic() = همهچیز امن است
بهتر است این مدل ذهنی را داشته باشیم:
atomic() ↓ Transaction Boundary ↓ Commit Together OR Rollback Together
و سپس بر اساس نیاز، ابزارهای دیگر را اضافه کنیم:
atomic() ↓ Transaction Boundary select_for_update() ↓ Concurrency Control F() ↓ Database-level Atomic Update Database Constraints ↓ Invariant Enforcement on_commit() ↓ Post-Commit Side Effects
transaction.atomic() یک ابزار بسیار قدرتمند است، اما Magic Safety Switch نیست.
کار اصلی آن این است که چند عملیات دیتابیس را در یک Transaction قرار دهد:
Database Changes ↓ All succeed OR All rollback
اما بهتنهایی:
جلوی Race Condition را نمیگیرد.
همه Rowها را Lock نمیکند.
API خارجی را Rollback نمیکند.
جایگزین Database Constraint نیست.
تضمین نمیکند Celery بعد از Commit اجرا شود.
Network Operation را Transactional نمیکند.
یک Transaction طولانی را به Transaction خوبی تبدیل نمیکند.
در یک سیستم Production معمولاً باید چند ابزار را کنار هم ببینیم:
atomic() + select_for_update() / F() + Database Constraints + on_commit() + Idempotency / Retry
هدف این نیست که atomic() را همهجا استفاده کنیم.
هدف این است که Transaction Boundary درست را تعریف کنیم، State مشترک را در برابر Concurrency محافظت کنیم، قوانین مهم را به دیتابیس بسپاریم و Side Effectهای خارجی را تا حد امکان بعد از Commit انجام دهیم.
در نهایت، transaction.atomic() را باید بهعنوان ابزاری برای تعریف مرز Transaction و حفظ Atomicity تغییرات دیتابیس ببینیم، نه راهحلی برای تمام مشکلات همزمانی و سازگاری دادهها.
وقتی atomic() را در کنار select_for_update()، F() expressions، Database Constraints و on_commit() درست استفاده کنیم، میتوانیم رفتار قابلاعتمادتری در سیستمهای Production داشته باشیم.
نسخه انگلیسی و کاملتر این مقاله در CyberHuginn Com منتشر شده است.