paint-brush
Python: O privire mai profundă asupra modului în care funcționează tranzacțiile Djangode@mta
134 lecturi Noua istorie

Python: O privire mai profundă asupra modului în care funcționează tranzacțiile Django

de Michael T. Andemeskel9m2025/03/02
Read on Terminal Reader

Prea lung; A citi

Transaction.atomic() de la Django poate fi folosit pentru a crea tranzacții pentru a realiza operațiuni atomice - operațiuni DB care sunt garantate că toate se execută și vor fi salvate împreună sau eșuează. Django creează tranzacții folosind mecanismul de tranzacție al DB-ului de bază, execută codul din tranzacție și, dacă nu apar erori, trimite tranzacția către DB.
featured image - Python: O privire mai profundă asupra modului în care funcționează tranzacțiile Django
Michael T. Andemeskel HackerNoon profile picture
0-item


Conţinut

  • TLDR — Rezumatul logicii tranzacțiilor Django
  • Diagrama logică
  • Tranzacții și operațiuni ATOMIC
  • Tranzacții în Django
  • Django auto-commit mode — Dezactivarea tranzacțiilor automate
  • Surse

TLDR — Rezumatul logicii tranzacțiilor Django

Aceasta este pentru aplicațiile care rulează Django cu PSQL sau alte baze de date (DB-uri) bazate pe SQL (SQLite, MySQL etc.); este posibil ca regulile care stau la baza tranzacțiilor să nu se aplice altor DB. Sub capotă, codul de gestionare a tranzacțiilor Django face următoarele (acesta este copiat din documentul lui Django -defilați în jos- cu propriile mele puncte pentru clarificare):


  • Deschide o tranzacție la intrarea în blocul atomic cel mai exterior;
    • Obține sau creează o conexiune la DB și o setează ca tranzacție.


  • Solicită și dobândește încuietori atunci când este întâlnită o operație DB;
    • Toate operațiunile DB necesită blocări — unele blocaje sunt libere și vor permite altor conexiuni să execute operațiuni pe aceeași resursă, dar alte blocări sunt mai stricte; sunt exclusive și vor bloca atât operațiile de citire, cât și de scriere. Crearea, actualizarea și ștergerea înregistrărilor, precum și modificarea tabelelor și coloanelor, vor obține blocări mai stricte care vor bloca alte conexiuni de la executarea operațiunilor. Aceste blocări sunt păstrate până la sfârșitul tranzacției.


  • Creează un punct de salvare la intrarea într-un bloc atomic interior;
    • Acest lucru se face automat atunci când, de exemplu, se inserează înregistrări într-un tabel - unele operații de model se vor încheia automat în blocuri atomice.

    • Acest lucru se poate face manual apelând o funcție care este înfășurată de atomic() sau folosind atomic() într-o instrucțiune with (ca manager de context).

    • Punctele de salvare vor folosi aceeași conexiune ca și tranzacția exterioară - dacă vă uitați la codul Django pentru punctele de salvare, este doar un apel recursiv pentru a crea o altă tranzacție.

    • Punctele de salvare pot fi imbricate în punctele de salvare — o stivă ține evidența punctelor de salvare.


  • Eliberează sau revine la punctul de salvare la ieșirea dintr-un bloc interior;
    • După inserare, punctul de salvare este eliminat fie prin comiterea modificărilor și punerea lor la dispoziție pentru restul tranzacției (dar NU alte conexiuni), fie prin anularea modificărilor.


  • Când se întâlnește cod care nu atinge DB, codul este executat ca de obicei;
    • Blocările de pe DB sunt încă ținute în timp ce logica non-DB este executată, de exemplu, dacă trimitem o cerere HTTP în corpul unei tranzacții, blocările vor fi reținute până când acea cerere este trimisă și răspunsul este returnat. Django nu eliberează blocările atunci când oprim executarea operațiunilor DB. Acesta deține încuietori PÂNĂ când tranzacția exterioară este încheiată.

    • Blocările dobândite în tranzacțiile imbricate sunt, de asemenea, păstrate până când tranzacția exterioară este încheiată sau tranzacția imbricată este anulată.


  • Când apare o eroare RuntimeError sau apare o eroare în afara DB (nu este legată de operațiunile DB);
    • Tranzacția este oprită, derulată înapoi și blocările sunt eliberate.

    • Dacă eroarea apare într-o tranzacție imbricată, tranzacția exterioară nu este afectată. Are loc rollback-ul, dar tranzacția externă poate continua neîntreruptă (atâta timp cât eroarea generată este prinsă și gestionată).

    • Tranzacțiile nu detectează erori, dar depind de acestea pentru a ști când să anuleze modificările. Prin urmare, nu împachetați operațiunile DB într-un try/except, în schimb, împachetați-le într-o tranzacție și apoi împachetați tranzacția într-un try/except. Procedând astfel, operațiunea DB eșuează și va fi anulată automat, fără a afecta tranzacția externă.


  • Commite sau derulează înapoi tranzacția la ieșirea din blocul exterior.
    • În cele din urmă, întreaga tranzacție este comisă, făcând modificările disponibile tuturor utilizatorilor/conexiunilor DB. Sau este derulat înapoi, lăsând DB în aceeași stare în care era atunci când a fost întâlnită tranzacția.


În următorul blog, voi scrie despre ce ar trebui sau nu ar trebui să puneți într-o tranzacție pentru a evita întreruperile cauzate de înfometarea lacătelor.

Diagrama logică

Diagrama logică a tranzacțiilor Django — Complet

Tranzacții și operațiuni ATOMIC

Tranzacțiile sunt mecanisme care permit DB-urilor ACID (PSQL, SQL, cel mai recent MongoDB etc.) să fie ATOMIC (A în ACID). Aceasta înseamnă că toate scrierile în DB sunt efectuate ca o singură operațiune – indiferent cât de complexă. Dacă scrierea reușește, toate modificările din DB persistă și sunt disponibile pentru toate conexiunile simultan (fără scrieri parțiale). Dacă scrierea eșuează, toate modificările sunt anulate - din nou, nu există modificări parțiale.


Tranzacțiile garantează următoarele reguli:

  • Datele din DB nu se vor schimba în mijlocul unei tranzacții.
  • Nicio altă conexiune nu va putea vedea modificările efectuate de tranzacție până la finalizarea acesteia.
  • DB va avea fie toate datele noi, fie nici una.


Aceste reguli permit utilizatorului DB să grupeze operațiuni complexe și să le execute ca o singură operație. De exemplu, executarea unui transfer dintr-un cont bancar în altul; dacă nu am avut tranzacții, atunci în mijlocul acestor două operațiuni de scriere ar putea exista o retragere sau chiar o operațiune de închidere a contului care face transferul invalid. Tranzacțiile permit utilizatorului o anumită formă de control al traficului. Putem bloca toate celelalte operațiuni conflictuale în timp ce tranzacția este în curs.


Operațiunile sunt blocate printr-o serie de încuietori pe tabele și rânduri. În PSQL și alte variante SQL, tranzacțiile sunt create cu BEGIN; atunci blocările sunt achiziționate atunci când se execută o operațiune precum select/insert/delete/alter și tranzacțiile se termină cu COMMIT sau ROLLBACK. Blocările sunt eliberate când se execută COMMIT sau ROLLBACK. Din fericire, Django ne permite să creăm tranzacții fără a fi nevoie să folosim aceste trei declarații (dar trebuie să ne îngrijorăm totuși cu privire la blocaje; mai multe despre asta în postarea următoare).


 -- Start a new transaction BEGIN; SELECT … INSERT INTO … UPDATE … DELETE FROM … -- Save the changes COMMIT;


  • Selectarea dobândește blocări care blochează actualizările pe rândurile citite.
  • Inserarea dobândește încuietori care blochează actualizările pe rândurile care sunt create.
  • Actualizarea dobândește blocări care blochează actualizările pe rândurile care sunt actualizate.
  • Ștergerea dobândește încuietori care blochează actualizările pe rândurile care sunt șterse.
  • Nu trebuie să verificăm în mod explicit erorile și să apelăm la ROLLBACK - tranzacția face acest lucru pentru noi.
  • Dacă ar exista o operațiune de modificare a tabelului acolo, ar bloca toate citirile și scrierile de pe masă (mai multe despre asta în următorul blog).

Tranzacții în Django

 from django.db import transaction from app.core.models import Accounts, Fee, NotificationJob def do_migration(): overdrawn_accounts = Accounts.objects.filter(type='overdrawn') for acount in overdrawn_accounts: create_notification(acount) @transaction.atomic def create_notification(acount: Accounts): # $5 fee for overdraft - 500 because we never store money as float!!! recall = Fee.objects.create(acount=acount, description='Fee for overdraft', amount=500) NotificationJob.objects.create(recall=recall, notification_type='all') acount.status = 'awaiting_payment' acount.save() def do_migration2(): overdawn_account = Accounts.objects.filter(type='overdrawn') for account in overdawn_account: with transaction.atomic(): recall = Fee.objects.create(acount=account, description='Fee for overdraft', amount=500) NotificationJob.objects.create(recall=recall, notification_type='all') account.status = 'awaiting_payment' account.save()


Django împachetează automat fiecare operațiune DB într-o tranzacție pentru noi atunci când rulează în modul autocommit (mai multe despre asta mai jos). Tranzacțiile explicite pot fi create folosind decoratorul atomic() sau managerul de context (cu atomic()) — atunci când este utilizat atomic(), operațiunile DB individuale NU sunt incluse într-o tranzacție sau trimise imediat în DB (sunt executate, dar modificările în DB nu sunt vizibile pentru alți utilizatori). În schimb, întreaga funcție este înfășurată într-un bloc de tranzacție și comisă la sfârșit (dacă nu sunt generate erori) — COMMIT este executat.


Dacă există erori, modificările DB sunt anulate — ROLLBACK este executat. Acesta este motivul pentru care tranzacțiile păstrează încuietori până la sfârșit; nu vrem să eliberăm o blocare pe o masă, să facem ca o altă conexiune să schimbe tabelul sau să-l citim și apoi să fim forțați să derulăm înapoi ceea ce tocmai a fost schimbat sau citit.


 ANGRY_CUSTOMER_THRESHOLD = 3 @transaction.atomic def create_notification(acount: Accounts): recall = Fee.objects.create(acount=acount, description='Fee for overdraft', amount=500) NotificationJob.objects.create(recall=recall, notification_type='all') try: # when this completes successfully, the changes will be available to the outer transaction with transaction.atomic(): owner = acount.owner fees = Fee.objects.filter(owner=owner).count() if fees >= ANGRY_CUSTOMER_THRESHOLD: for fee in fees: fee.amount = 0 fee.save() owner.status = 'angry' owner.save() # as long as we catch the error, the outer transaction will not be rolled back except Exception as e: logger.error(f'Error while removings fees for account {acount.id}: {e}') acount.status = 'awaiting_payment' acount.save()


De asemenea, putem imbrica tranzacții apelând o altă funcție care este încapsulată de atomic() sau folosind managerul de context în cadrul unei tranzacții. Acest lucru ne permite să creăm puncte de salvare în care putem încerca operațiuni riscante fără a afecta restul tranzacției - atunci când este detectată o tranzacție internă, este creat un punct de salvare, tranzacția internă este executată și, dacă tranzacția internă eșuează, datele sunt returnate la punctul de salvare și tranzacția externă continuă. Dacă tranzacția externă eșuează, toate tranzacțiile interioare sunt anulate împreună cu tranzacția externă. Imbricarea tranzacțiilor poate fi prevenită prin setarea durable=True în atomic() - acest lucru va face ca tranzacția să genereze o RuntimeError dacă sunt detectate tranzacții interne.


Ce trebuie să rețineți despre tranzacțiile imbricate:

  • Tranzacțiile imbricate NU eliberează blocaje decât dacă tranzacțiile imbricate sunt anulate sau tranzacția exterioară este confirmată sau anulată.
  • Tranzacțiile imbricate NU surprind erori DB; pur și simplu derulează înapoi operațiunile din tranzacția care a cauzat eroarea. Încă trebuie să detectăm eroarea DB pentru ca tranzacția externă să continue.
  • Modificările efectuate în tranzacțiile imbricate sunt disponibile pentru tranzacția externă și tranzacțiile imbricate ulterioare.

Django Autocommit Mode — Dezactivarea tranzacțiilor automate

 DATABASES = { 'default': { # True by default 'AUTOCOMMIT': False, # ...rest of configs } }

În mod implicit, Django rulează în modul autocommit , ceea ce înseamnă că fiecare operație DB este încapsulată într-o tranzacție și rulează imediat (committed, deci autocommit), cu excepția cazului în care operația este în mod explicit inclusă într-o tranzacție de către utilizator. Acest lucru obstrucționează logica tranzacției a DB-ului de bază - unele DB, cum ar fi PSQL, înglobează automat toate operațiunile în tranzacții, în timp ce altele nu. Dacă îl avem pe Django care face asta pentru noi, nu trebuie să ne facem griji cu privire la logica DB de bază.


Putem dezactiva modul autocommit, dar asta ne-ar face să ne bazăm pe DB pentru a gestiona operațiunile și a ne asigura că sunt ATOMIC, ceea ce este riscant și incomod pentru dezvoltatori. Nu ar fi distractiv dacă, în timpul creării unui API, ar trebui să ne dăm seama care operațiuni de scriere sunt în tranzacții și, prin urmare, vor fi scrise în totalitate și nu parțial.


Cu toate acestea, este posibil să dorim să dezactivăm modul autocommit dacă știm numărul de conexiuni pe DB și secvența operațiunilor pe care le execută. Dacă ne dăm seama cum să deconflictăm aceste operațiuni, atunci putem dezactiva modul autocommit și obținem câteva eficiențe:

  • Eliminam doua comenzi din fiecare operatie: BEGIN si COMMIT/ROLLBACK. Dar majoritatea DB-urilor împachetează automat fiecare operațiune dintr-o tranzacție, astfel încât acest lucru poate fi inutil.
  • Blocările nu vor fi ținute prea mult timp - de îndată ce o operațiune se încheie, blocările pe care le deține sunt eliberate, în comparație cu așteptarea încheierii întregii tranzacții.
  • Blocajele sunt mai puțin probabile - blocajele apar atunci când două tranzacții au nevoie de o blocare pe care o deține cealaltă tranzacție. Acest lucru lasă ambele tranzacții în așteptare ca cealaltă să se termine, dar niciuna nu se poate termina. Totuși, pot apărea blocaje. Unele operațiuni DB vor face scrieri multiple, de exemplu, actualizarea unei coloane care are un index va actualiza coloana și indexul (cel puțin două scrieri). Acest lucru poate duce la blocaje dacă o altă conexiune încearcă să opereze pe rândurile sau coloanele afectate.


Dezavantajele sunt riscurile mai mari de corupere a datelor. Nu merită, dar poate că există un caz de utilizare la care nu mă pot gândi.

Surse