Oldingi postda biz transaction.atomic
bilan bezatilgan funksiya chaqirilganda nima sodir bo'lishini va with transaction.atomic()
nima sodir bo'lishini bilib oldik. Qisqa bayoni; yakunida:
BEGIN;
ma'lumotlar bazasiga yuboriladi (agar JB PSQL yoki boshqa SQL varianti bo'lsa).
Endi biz tranzaktsiyada nima qilish kerakligini va nimadan qochish kerakligini muhokama qilamiz. Quyidagi tranzaksiya xatti-harakatlari tufayli ma'lum operatsiyalar tranzaksiya blokiga joylashtirilganda xavflidir.
Muqobil variantlar va kod misollari uchun o'qishni davom eting.
Birlamchi xavf shundan iboratki, tranzaktsiyalar jadvallar va qatorlarda qarama-qarshi operatsiyalarni oldini olish va tranzaksiyani qaytarib bo'lishiga imkon berish uchun bajarilgunga qadar qulfni ushlab turadi - bu tranzaksiyadagi JB operatsiyalarini atomik qilish uchun zarurdir. Bu shuni anglatadiki, bir nechta jadvallarda yoki bir nechta muhim jadvallarda ishlaydigan uzoq muddatli tranzaksiya blokirovkalash va ushbu jadvallar/satrlarni o'qish/yozishni oldini olish orqali uzilishlarga olib kelishi mumkin.
Aslini olganda, agar biz tranzaksiya blokiga noto'g'ri kod qo'ysak, biz JB bilan boshqa barcha ulanishlarni unda operatsiyalarni amalga oshirishni bloklash orqali JBni samarali ravishda olib tashlashimiz mumkin.
Ikkilamchi tavakkalchilik shundan iboratki, tranzaktsiyalar qaytarilishi kerak va qaytarilishi kutiladi. Agar tranzaktsiyada xatolik yuzaga kelsa, JB avtomatik ravishda har bir operatsiyani o'zgartiradi. Shuning uchun biz tranzaksiyaga kiritgan ma'lumotlar bazasi operatsiyalari qaytarilishi kerak - aksariyat hollarda PSQL bilan bu haqda tashvishlanishga hojat yo'q. Ammo boshqa kodlar haqida nima deyish mumkin?
Ko'pincha ma'lumotlarimizni o'zgartirganimizda, biz voqealarni yoqish, xizmatlarni yangilash, push bildirishnomalarini yuborish va hokazolarni bajarishimiz kerak. Bu vazifalarni qaytarib bo'lmaydi - biz voqea, so'rov yoki bildirishnomani qaytarib bo'lmaydi. Agar xatolik yuz bersa, maʼlumotlar oʻzgarishlari orqaga qaytariladi, lekin biz “Sizning hisobotingiz yaratildi; uni koʻrish uchun shu yerni bosing” degan push-bildirishnomani allaqachon yuborganmiz. Agar foydalanuvchi yoki boshqa xizmatlar ushbu noto'g'ri ma'lumotlar bo'yicha harakat qilsa nima bo'ladi? Muvaffaqiyatsizliklar kaskadi bo'ladi. Shuning uchun, bekor qilib bo'lmaydigan har qanday kod tranzaksiyada bo'lmasligi kerak yoki tranzaksiyada xatolik yuz berganda tizimimizni yomon holatda qoldirish xavfi bor.
Bular, qancha ma'lumotlar qayta ishlanayotganiga va JB trafigiga qarab, qulflarni juda uzoq vaqt ushlab turish tufayli uzilishlarga olib kelishi mumkin bo'lgan narsalardir. Agar ular uzoq vaqt talab qilmasa, bularning barchasi yaxshi.
Sekin so'rovlar - Ushbu uchta stsenariydan tashqari ma'lumotlarni olish odatda tezdir. Ushbu so'rovlar tranzaksiyani sekinlashtiradi va uning qulflangan vaqtini uzaytiradi, bu esa boshqa foydalanuvchilarga salbiy ta'sir ko'rsatadi.
@transaction.atomic def process_large_order_report(start_date, end_date, min_order_value=1000): # Complex query with multiple joins and aggregations large_orders = Order.objects.filter( created_at__range=(start_date, end_date), total_amount__gte=min_order_value, status='completed' ).select_related( 'customer', 'shipping_address', 'billing_address' ).prefetch_related( 'items__product__category', 'items__product__supplier' ).annotate( item_count=Count('items'), total_weight=Sum('items__product__weight'), discount_percentage=F('discount_amount') * 100 / F('total_amount') ).filter( # Additional complex filtering Q(customer__user__is_active=True) & (Q(items__product__category__name='Electronics') | Q(items__product__category__name='Furniture')) & ~Q(shipping_address__country='US') ).order_by('-total_amount') # do the transactional work with the large_orders queryset
# fixed def process_large_order_report(start_date, end_date, min_order_value=1000): # Complex query with multiple joins and aggregations large_orders = Order.objects.filter( created_at__range=(start_date, end_date), total_amount__gte=min_order_value, status='completed' ).select_related( 'customer', 'shipping_address', 'billing_address' ).prefetch_related( 'items__product__category', 'items__product__supplier' ).annotate( item_count=Count('items'), total_weight=Sum('items__product__weight'), discount_percentage=F('discount_amount') * 100 / F('total_amount') ).filter( # Additional complex filtering Q(customer__user__is_active=True) & (Q(items__product__category__name='Electronics') | Q(items__product__category__name='Furniture')) & ~Q(shipping_address__country='US') ).order_by('-total_amount') # Start the transaction block with transaction.atomic(): # do the transactional work with the large_orders queryset
Bir nechta jadvallar ustida operatsiyalar - bir nechta jadvallar ustida operatsiyalar bilan tranzaksiya har bir jadvalni bajarilgunga qadar bloklashi mumkin. Bu, ayniqsa, Django migratsiyalarida keng tarqalgan - migratsiyani kichik va bir vaqtning o'zida bir yoki bir nechta jadvallarga yo'naltirishning yana bir sababi.
class Migration(migrations.Migration): dependencies = [("migrations", "0001_initial")] # too many operations operations = [ migrations.RemoveField("Author", "age"), migrations.AddField("Author", "rating", models.IntegerField(default=0)), migrations.AlterField("Book", "price", models.DecimalField(max_digits=5, decimal_places=2)), ]
# fixed # 1st migration class Migration(migrations.Migration): dependencies = [("migrations", "0001_initial")] operations = [ migrations.RemoveField("Author", "age"), ] # 2nd migration class Migration(migrations.Migration): dependencies = [("migrations", "0002_initial")] operations = [ migrations.AddField("Author", "rating", models.IntegerField(default=0)), ] # 3rd migration class Migration(migrations.Migration): dependencies = [("migrations", "0003_initial")] operations = [ migrations.AlterField("Book", "price", models.DecimalField(max_digits=5, decimal_places=2)), ]
Ma'lumotlarni ko'chirish - tranzaktsiyalar so'rov bajarilgandan so'ng tranzaksiya bajarilmagan yoki tugaguniga qadar blokirovkada ushlab turilganligi sababli, jadvaldagi har bir satrda ishlaydigan migratsiya har bir satrda o'qish yoki yozishni oldini olish orqali butun jadvalni blokirovka qiladi.
def migrate_user_profiles(): # Get all users with legacy profiles users_with_profiles = User.objects.filter( legacy_profile__isnull=False ).select_related('legacy_profile') # Process all users in a single transaction with transaction.atomic(): # Track progress total = users_with_profiles.count() print(f"Migrating {total} user profiles...") # Process each user for i, user in enumerate(users_with_profiles): if i % 100 == 0: print(f"Processed {i}/{total} profiles") legacy = user.legacy_profile legacy.update_new_user_profile()
# fixed def migrate_user_profiles(): # Get all users with legacy profiles users_with_profiles = User.objects.filter( legacy_profile__isnull=False ).select_related('legacy_profile') # Process all users in a single transaction # Track progress total = users_with_profiles.count() print(f"Migrating {total} user profiles...") # Process each user for i, user in enumerate(users_with_profiles): if i % 100 == 0: print(f"Processed {i}/{total} profiles") with transaction.atomic(): legacy = user.legacy_profile legacy.update_new_user_profile()
Ularni keyinroq ishga tushiring. Bu soʻrovlar zarur, lekin biz ularni ish vaqtida bajarishimiz shart emas. Bu erda eng yaxshi siyosat jadvalning qanchalik muhimligini aniqlash, migratsiya qancha davom etishini taxmin qilish, JB eng kam trafikga ega bo'lganda migratsiyani amalga oshirish va orqaga qaytish rejasini tayyorlash orqali uzilish xavfini kamaytirishdir.
Tranzaktsiyaga ketadigan vaqtni qisqartiring. Buni jadvalni qismlarga bo'lish va alohida bo'limlarda migratsiyani amalga oshirish orqali amalga oshirish mumkin. PSQL bo'limlari va Django
*Django faqat PSQL va SQLite uchun migratsiya atrofidagi tranzaktsiyalarni o'rab oladi.
class Migration(migrations.Migration): dependencies = [("migrations", "0001_initial")] # this migration, if on a large table, can slow down and block other operations # do it later operations = [ migrations.RemoveField("Users", "middle_name"), ]
Qaytarib bo'lmaydigan operatsiyalar - tranzaksiya orqaga qaytarilsa, tranzaksiyadagi hamma narsa qaytarilishi kerak; agar biz tranzaktsiyaga API chaqiruvini qo'ysak, uni qaytarib bo'lmaydi.
def transaction(user_data, user_files): with transaction.atomic(): user = User.objects.create(**user_data) async_notification_service.send_email(user.email, "You can login now!") Account.objects.create(user=user, balance=0) # rest of user creation proccess
def transaction(user_data, user_files): with transaction.atomic(): user = User.objects.create(**user_data) Account.objects.create(user=user, balance=0) # rest of user creation proccess # the transaction is still in progress, so it can still be rolled back, it is not # committed until the transaction block is exited, so putting the notification here # is not a good idea - especially if the job starts immediately tries to read the data # this creates a race condition async_notification_service.send_email(user.email, "You can login now!") def transaction(user_data, user_files): with transaction.atomic(): user = User.objects.create(**user_data) Account.objects.create(user=user, balance=0) # rest of user creation proccess transaction.on_commit(partial(async_notification_service.send_email, user.email, "You can login now!"))
Qo'ng'iroqlarni bloklash — tranzaksiyalar o'zgaruvchan jadvallar/satrlar bo'yicha boshqa barcha so'rovlarning ishlashiga to'sqinlik qilganligi sababli, tranzaksiya davomiyligini oshiradigan har qanday kod JB bloklanishiga olib keladi, bu esa JBga bog'liq bo'lgan ilovalarda kutish vaqti va javob bermasligiga olib keladi.
def transaction(user_data, user_files): with transaction.atomic(): user = User.objects.create(**user_data) for file_data in user_files: # transaction waits for this upload and so do all other connections that need access to table/rows the transaction # uses url = Cloudinary.upload_file(file_data['data']) Files.objects.create(**file_data['meta_data'], user=user, url=url) Account.objects.create(user=user, balance=0) # rest of user creation proccess
# not bad def transaction(user_data, user_files): user = None with transaction.atomic(): user = User.objects.create(**user_data) Account.objects.create(user=user, balance=0) # rest of user creation proccess for file_data in user_files: url = Cloudinary.upload_file(file_data['data']) Files.objects.create(**file_data['meta_data'], user=user, url=url) # best fix from functools import partial def transaction(user_data, user_files): user = None with transaction.atomic(): user = User.objects.create(**user_data) Account.objects.create(user=user, balance=0) # rest of user creation proccess # partials create a callable with the function and arguments # so that the function is called with the arguments when the transaction is committed # TODO: diff between partial and lambda here??? transaction.on_commit(partial(create_user_files, user_files, user)) def create_user_files(user_files, user): for file_data in user_files: url = Cloudinary.upload_file(file_data['data']) Files.objects.create(**file_data['meta_data'], user=user, url=url)
Keyingi postda biz PSQL-ga sho'ng'iymiz va quyidagilarni bilib olamiz: