Өмнөх нийтлэлээс бид transaction.atomic
-ээр чимэглэсэн функц дуудагдах үед юу болдог, мөн with transaction.atomic()
юу болдог талаар олж мэдсэн. Дүгнэж хэлэхэд:
BEGIN;
DB руу илгээгдэнэ (хэрэв DB нь PSQL эсвэл өөр SQL хувилбар бол).
Одоо бид гүйлгээнд юу хийх, юу хийхээс зайлсхийх талаар ярилцах болно. Дараахь гүйлгээний зан үйлийн улмаас гүйлгээний блокт байршуулах үед тодорхой үйлдлүүд аюултай.
Өөр хувилбарууд болон кодын жишээнүүдийг үргэлжлүүлэн уншаарай.
Анхдагч эрсдэл нь хүснэгт болон мөрүүд дээр зөрчилдөх үйлдлээс урьдчилан сэргийлэхийн тулд гүйлгээг хийх хүртэл түгжээтэй байж, гүйлгээг буцаах боломжтой байдаг - энэ нь гүйлгээний DB-ийн үйл ажиллагааг атом хэлбэртэй болгоход зайлшгүй шаардлагатай. Энэ нь олон хүснэгт эсвэл цөөн хэдэн чухал хүснэгтүүд дээр ажилладаг урт хугацааны гүйлгээ нь түгжээг хааж, тэдгээр хүснэгт/мөрүүдийг унших/бичихээс сэргийлж тасалдал үүсгэж болзошгүй гэсэн үг юм.
Нэг ёсондоо, хэрэв бид гүйлгээний блокт буруу код оруулсан бол бид DB-тэй холбогдох бусад бүх холболтыг түүн дээр үйлдлээр нь хааснаар DB-г үр дүнтэй устгаж чадна.
Хоёрдогч эрсдэл нь гүйлгээг буцаах шаардлагатай бөгөөд буцаах боломжтой байх ёстой. Гүйлгээнд алдаа гарвал DB автоматаар үйлдэл бүрийг буцаана. Тиймээс бидний гүйлгээнд оруулсан DB-ийн үйлдлүүд буцаах боломжтой байх ёстой - ихэнх тохиолдолд бид PSQL-ийн хувьд энэ талаар санаа зовох шаардлагагүй болно. Гэхдээ бусад кодуудын талаар юу хэлэх вэ?
Ихэнхдээ бид өгөгдлөө өөрчлөхдөө үйл явдлуудыг халах, үйлчилгээг шинэчлэх, түлхэх мэдэгдэл илгээх гэх мэт даалгавруудыг хийх шаардлагатай болдог. Эдгээр ажлыг буцаах боломжгүй - бид үйл явдал, хүсэлт, мэдэгдлийг буцаах боломжгүй. Хэрэв алдаа гарвал өгөгдлийн өөрчлөлтийг буцаах боловч бид "Таны тайланг үүсгэсэн; энд дарж үзэх" гэсэн мэдэгдлийг аль хэдийн илгээсэн. Хэрэглэгч эсвэл бусад үйлчилгээнүүд энэ худал мэдээлэл дээр ажиллахад юу болох вэ? Бүтэлгүйтлийн цуваа байх болно. Тиймээс буцаах боломжгүй аливаа код нь гүйлгээнд байх ёсгүй, эс тэгвээс гүйлгээнд алдаа гарвал бид системээ муу байдалд оруулах эрсдэлтэй.
Эдгээр нь хэр их өгөгдөл боловсруулж байгаа болон DB урсгалаас хамааран түгжээг хэт удаан барьснаас болж тасалдал үүсгэж болзошгүй зүйлүүд юм. Эдгээр бүх зүйл хэтэрхий удаан үргэлжлэхгүй бол зүгээр.
Удаан асуулга — Эдгээр гурван хувилбараас бусад тохиолдолд өгөгдөл татах нь ихэвчлэн хурдан байдаг. Эдгээр асуулга нь гүйлгээг удаашруулж, түгжигдэх хугацааг уртасгах бөгөөд энэ нь бусад хэрэглэгчдэд сөрөг нөлөө үзүүлэх болно.
@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
Олон хүснэгт дээрх үйлдлүүд — олон хүснэгтүүд дээрх үйлдлүүдтэй гүйлгээ нь хүснэгт бүрийг дуусгах хүртэл түгжих боломжтой. Энэ нь ялангуяа Жангогийн шилжилт хөдөлгөөнд түгээмэл байдаг нь шилжилт хөдөлгөөнийг бага байлгах бас нэг шалтгаан бөгөөд нэг эсвэл хэд хэдэн хүснэгтэд төвлөрдөг.
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)), ]
Өгөгдлийн шилжилт - Гүйлгээ нь асуулга хийгдсэнээс эхлэн гүйлгээ бүтэлгүйтэх эсвэл дуусах хүртэл түгжээтэй байдаг тул хүснэгтийн мөр бүр дээр ажилладаг шилжүүлэг нь мөр бүр дээр унших, бичихээс сэргийлж хүснэгтийг бүхэлд нь түгжих болно.
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()
Тэднийг дараа ажиллуул. Эдгээр асуулга зайлшгүй шаардлагатай, ГЭХДЭЭ бид үүнийг ажлын цагаар ажиллуулах шаардлагагүй. Энд байгаа хамгийн сайн бодлого бол хүснэгт нь хэр чухал болохыг тодорхойлж, шилжилт хөдөлгөөн хэр удаан үргэлжлэхийг тооцоолж, DB хамгийн бага ачаалалтай үед шилжүүлгийг хийж, буцаах төлөвлөгөөг боловсруулах замаар тасалдал үүсэх эрсдэлийг бууруулах явдал юм.
Гүйлгээнд шаардагдах хугацааг багасга. Үүнийг хүснэгтийг хувааж, бие даасан хуваалтууд дээр шилжүүлэх замаар хийж болно. PSQL хуваалтууд ба Django
*Django нь зөвхөн PSQL болон SQLite-д зориулсан шилжүүлгийн эргэн тойронд гүйлгээг хийдэг.
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"), ]
Буцааж болшгүй үйлдлүүд — Гүйлгээ буцаагдсан тохиолдолд гүйлгээний бүх зүйл буцаагдах ёстой; Хэрэв бид гүйлгээнд API дуудлага хийвэл үүнийг буцаах боломжгүй.
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!"))
Дуудлага хаах — Гүйлгээ нь өөрчлөгдөж буй хүснэгт/мөрүүд дээр бусад бүх асуулгад ажиллахаас сэргийлдэг тул гүйлгээний үргэлжлэх хугацааг уртасгах аливаа код нь DB-г түгжихэд хүргэж, DB-ээс хамааралтай аппликешнүүдэд хугацаа хэтрүүлж, хариу өгөхгүй болно.
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)
Дараагийн нийтлэлд бид PSQL-д шумбаж, дараахь зүйлийг олж мэдэх болно.