paint-brush
Python: Tarkempi katsaus Django-transaktioiden toimintaankirjoittaja@mta
134 lukemat Uusi historia

Python: Tarkempi katsaus Django-transaktioiden toimintaan

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

Liian pitkä; Lukea

Djangon transaktio.atomic()-funktiota voidaan käyttää tapahtumien luomiseen atomioperaatioiden suorittamiseksi - DB-operaatiot, jotka taataan kaikkien suoriutuvan ja tallentuvat yhdessä tai epäonnistuvat. Django luo tapahtumia käyttämällä taustalla olevan DB:n tapahtumamekanismia, se suorittaa tapahtumassa olevan koodin ja jos virheitä ei tapahdu, se sitoo tapahtuman tietokantaan.
featured image - Python: Tarkempi katsaus Django-transaktioiden toimintaan
Michael T. Andemeskel HackerNoon profile picture
0-item


Sisältö

  • TLDR — Django Transaction Logic -yhteenveto
  • Logiikkakaavio
  • Tapahtumat ja ATOMIC-toiminnot
  • Liiketoimet Djangossa
  • Djangon automaattinen vahvistustila — Automaattisten tapahtumien poistaminen käytöstä
  • Lähteet

TLDR — Django Transaction Logic -yhteenveto

Tämä on tarkoitettu sovelluksille, jotka käyttävät Djangoa PSQL- tai muiden vastaavien SQL-pohjaisten tietokantojen (DBs) kanssa (SQLite, MySQL jne.); tapahtumia koskevat taustalla olevat säännöt eivät välttämättä koske muita tietokantoja. Konepellin alla Djangon tapahtumanhallintakoodi tekee seuraavaa (tämä on kopioitu Djangon dokumentista - vieritä alas - omilla luettelomerkeilläni selvyyden vuoksi):


  • Avaa tapahtuman tullessaan uloimpaan atomilohkoon;
    • Hakee tai luo yhteyden tietokantaan ja asettaa sen tapahtumaksi.


  • Pyydä ja hankkii lukot, kun DB-toiminto havaitaan;
    • Kaikki tietokantatoiminnot vaativat lukot – jotkut lukot ovat löysällä ja sallivat muiden yhteyksien suorittaa toimintoja samalla resurssilla, mutta toiset lukot ovat tiukempia; ne ovat eksklusiivisia ja estävät sekä luku- että kirjoitustoiminnot. Tietueiden luominen, päivittäminen ja poistaminen sekä taulukoiden ja sarakkeiden muuttaminen saa tiukemmat lukitukset, jotka estävät muita yhteyksiä suorittamasta toimintoja. Nämä lukot ovat voimassa tapahtuman LOPPUUN asti.


  • Luo tallennuspisteen, kun syötetään sisempi atomilohko;
    • Tämä tapahtuu automaattisesti, kun esimerkiksi lisätään tietueita taulukkoon – jotkin mallioperaatiot kietoutuvat automaattisesti atomilohkoihin.

    • Tämä voidaan tehdä manuaalisesti kutsumalla funktio, joka on kääritty atomic():lla, tai käyttämällä atomic():ta with-lauseessa (kontekstinhallinnana).

    • Tallennuspisteet käyttävät samaa yhteyttä kuin ulkoinen tapahtuma - jos katsot Django-koodia tallennuspisteille, se on vain rekursiivinen kutsu toisen tapahtuman luomiseksi.

    • Tallennuspisteet voidaan upottaa tallennuspisteisiin – pino pitää kirjaa tallennuspisteistä.


  • Vapauttaa tai vierii takaisin tallennuspisteeseen poistuessaan sisemmästä lohkosta;
    • Lisäyksen jälkeen tallennuspiste poistetaan joko tekemällä muutokset ja asettamalla ne saataville muulle tapahtumalle (mutta EI muille yhteyksille) tai peruuttamalla muutokset.


  • Kun kohdataan koodi, joka ei kosketa tietokantaa, koodi suoritetaan tavalliseen tapaan;
    • DB:n lukot ovat edelleen voimassa, kun ei-DB-logiikkaa suoritetaan, esim. jos lähetämme HTTP-pyynnön tapahtuman rungossa, lukot pysyvät voimassa, kunnes pyyntö lähetetään ja vastaus palautetaan. Django ei vapauta lukkoja, kun lopetamme DB-toimintojen suorittamisen. Se pitää lukot, KUNnes ulkoinen tapahtuma on valmis.

    • Sisäkkäisissä tapahtumissa hankitut lukot säilytetään myös, kunnes ulompi tapahtuma on valmis tai sisäkkäinen tapahtuma peruutetaan.


  • Kun RuntimeError tapahtuu tai virhe syntyy tietokannan ulkopuolella (ei liity tietokantatoimintoihin);
    • Tapahtuma pysäytetään, peruutetaan ja lukot vapautetaan.

    • Jos virhe ilmenee sisäkkäisessä tapahtumassa, se ei vaikuta ulkoiseen tapahtumaan. Peruutus tapahtuu, mutta ulompi tapahtuma voi jatkua keskeytyksettä (niin kauan kuin esiin tullut virhe havaitaan ja sitä käsitellään).

    • Tapahtumat eivät havaitse virheitä, mutta ne riippuvat siitä, että he tietävät, milloin muutokset peruutetaan. Siksi älä kääri DB-operaatioita try/expert-muotoon, vaan kääri ne tapahtumaan ja kääri tapahtuma sitten try/expert-muotoon. Näin DB-toiminto epäonnistuu ja palautuu automaattisesti ilman, että se vaikuttaa ulkoiseen tapahtumaan.


  • Sitouttaa tai peruuttaa tapahtuman poistuessaan uloimmasta lohkosta.
    • Lopuksi koko tapahtuma sitoutuu, jolloin muutokset ovat kaikkien DB-käyttäjien/yhteyksien saatavilla. Tai se palautetaan, jolloin tietokanta jää samaan tilaan kuin se oli tapahtuman yhteydessä.


Seuraavassa blogissa kirjoitan siitä, mitä sinun pitäisi tai ei pitäisi tehdä kaupassa, jotta vältytään lukkojen nälkään aiheuttavilta katkoksilta.

Logiikkakaavio

Django-tapahtumien logiikkakaavio — Täysi

Tapahtumat ja ATOMIC-toiminnot

Tapahtumat ovat mekanismeja, jotka sallivat ACID- tietokannan (PSQL, SQL, uusin MongoDB jne.) olla ATOMIC (A ACID:ssä). Tämä tarkoittaa, että kaikki kirjoitukset tietokantaan tehdään yhtenä toimenpiteenä – riippumatta siitä, kuinka monimutkaista. Jos kirjoitus onnistuu, kaikki tietokantaan tehdyt muutokset säilyvät ja ovat kaikkien yhteyksien käytettävissä samanaikaisesti (ei osittaisia kirjoituksia). Jos kirjoitus epäonnistuu, kaikki muutokset peruutetaan – taaskaan ei ole osittaisia muutoksia.


Tapahtumat takaavat seuraavat säännöt:

  • DB:n tiedot eivät muutu tapahtuman aikana.
  • Mikään muu yhteys ei näe tapahtuman tekemiä muutoksia ennen kuin se on valmis.
  • Tietokannassa on joko kaikki uudet tiedot tai ei yhtään niistä.


Nämä säännöt sallivat tietokannan käyttäjän niputtaa monimutkaiset toiminnot yhteen ja suorittaa ne yhtenä toimintona. Esimerkiksi siirron suorittaminen pankkitililtä toiselle; jos meillä ei olisi tapahtumia, niin näiden kahden kirjoitusoperaation keskellä voi tapahtua nosto tai jopa tilin sulkemistoiminto, joka tekee siirron pätemättömäksi. Tapahtumat mahdollistavat käyttäjälle jonkinlaisen liikenteen ohjauksen. Voimme estää kaikki muut ristiriitaiset toiminnot tapahtuman aikana.


Toiminnot estetään lukuisten taulukoiden ja rivien lukkojen avulla. PSQL:ssä ja muissa SQL-muunnelmissa tapahtumat luodaan BEGIN; sitten lukot hankitaan, kun suoritetaan toiminto, kuten select/insert/delete/alter, ja tapahtumat päättyvät COMMIT- tai ROLLBACK-toimintoon. Lukot vapautetaan, kun COMMIT tai ROLLBACK suoritetaan. Onneksi Django antaa meille mahdollisuuden luoda tapahtumia ilman, että tarvitsemme käyttää näitä kolmea lausetta (mutta meidän on silti huolehdittava lukoista; siitä lisää seuraavassa viestissä).


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


  • Select hankkii lukot, jotka estävät luettavien rivien päivitykset.
  • Lisäys hankkii lukot, jotka estävät luotavien rivien päivitykset.
  • Päivitys hankkii lukot, jotka estävät päivitykset päivitettävillä riveillä.
  • Poistaminen hankkii lukot, jotka estävät päivitykset poistetuille riveille.
  • Meidän ei tarvitse erikseen tarkistaa virheiden varalta ja soittaa ROLLBACKiin – tapahtuma tekee tämän puolestamme.
  • Jos siellä olisi alter table -toiminto, se estäisi kaiken lukemisen ja kirjoittamisen taulukossa (lisää tästä seuraavassa blogissa).

Liiketoimet Djangossa

 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 kääri automaattisesti jokaisen DB-operaation tapahtumaksi puolestamme, kun se suoritetaan automaattisessa toimitustilassa (lisätietoja alla). Eksplisiittisiä tapahtumia voidaan luoda käyttämällä atomic()-dekoraattoria tai kontekstinhallintaa (atomic()-sovelluksella) — kun atomic()-toimintoa käytetään, yksittäisiä tietokantatoimintoja EI kääri tapahtumaan tai sitoudu välittömästi tietokantaan (ne suoritetaan, mutta tietokantaan tehdyt muutokset eivät näy muille käyttäjille). Sen sijaan koko toiminto kääritään tapahtumalohkoon ja sitoutuu lopussa (jos virheitä ei esiinny) - COMMIT suoritetaan.


Jos virheitä ilmenee, tietokannan muutokset peruutetaan — ROLLBACK suoritetaan. Tästä syystä tapahtumat pitävät lukoissa aivan loppuun asti; emme halua vapauttaa pöydän lukkoa, saada toista yhteyttä vaihtamaan taulukkoa tai lukemaan sitä, ja sitten meidän on pakko peruuttaa juuri muutettua tai luettua.


 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()


Voimme myös sisäkkäisiä tapahtumia kutsumalla toista funktiota, joka on kääritty atomic():lla, tai käyttämällä tapahtuman yhteydessä kontekstinhallintaa. Näin voimme luoda tallennuspisteitä, joissa voimme yrittää riskialttiita toimintoja vaikuttamatta tapahtuman loppuosaan – kun sisäinen tapahtuma havaitaan, tallennuspiste luodaan, sisäinen tapahtuma suoritetaan, ja jos sisäinen tapahtuma epäonnistuu, tiedot palautetaan tallennuspisteeseen ja ulompi tapahtuma jatkuu. Jos ulompi tapahtuma epäonnistuu, kaikki sisäiset tapahtumat peruutetaan ulkoisen tapahtuman rinnalla. Tapahtuman sisäkkäisyyden voi estää asettamalla durable=True in atomic() - tämä aiheuttaa tapahtumalle RuntimeError-ilmoituksen, jos sisäisiä tapahtumia havaitaan.


Mitä sinun tulee muistaa sisäkkäisistä tapahtumista:

  • Sisäkkäiset tapahtumat EIVÄT vapauta lukkoja, ellei sisäkkäisiä tapahtumia peruuteta tai ulompaa tapahtumaa ei ole sitova tai peruutettu.
  • Sisäkkäiset tapahtumat EIVÄT TAPA DB-virheitä; he yksinkertaisesti peruuttavat virheen aiheuttaneen tapahtuman toiminnot. Meidän on vielä löydettävä DB-virhe, jotta ulkoinen tapahtuma voi jatkua.
  • Sisäkkäisiin tapahtumiin tehdyt muutokset ovat käytettävissä ulkoisessa tapahtumassa ja sitä seuraavissa sisäkkäisissä tapahtumissa.

Django Autocommit Mode - Automaattisten tapahtumien poistaminen käytöstä

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

Oletusarvoisesti Django toimii automaattisessa toimitustilassa, mikä tarkoittaa, että jokainen tietokantatoiminto kääritään tapahtumaan ja suoritetaan välittömästi (sitoutunut, eli automaattinen toimitus), paitsi jos käyttäjä on nimenomaisesti käärinyt toiminnon tapahtumaan. Tämä hämärtää taustalla olevan tietokannan tapahtumalogiikan – jotkin tietokannat, kuten PSQL, käärivät automaattisesti kaikki toiminnot tapahtumiin, kun taas toiset eivät. Jos Django tekee tämän puolestamme, meidän ei tarvitse huolehtia taustalla olevasta DB-logiikasta.


Voimme kytkeä automaattisen toimitustilan pois päältä, mutta se jättäisi meidän luottamaan tietokantaan toimintojen hallinnassa ja niiden ATOMIC-tason varmistamisessa, mikä on riskialtista ja hankalaa kehittäjille. Ei olisi hauskaa, jos API:a luotaessa meidän täytyisi selvittää, mitkä kirjoitusoperaatiot ovat tapahtumissa ja siksi ne kirjoitetaan kokonaisuudessaan eikä osittain.


Saatamme kuitenkin haluta kytkeä automaattisen vahvistuksen tilan pois päältä, jos tiedämme tietokannassa olevien yhteyksien määrän ja niiden suorittamien toimintojen järjestyksen. Jos selvitämme kuinka purkaa nämä toiminnot, voimme sammuttaa automaattisen sitoutumisen tilan ja saada joitain tehokkuuksia:

  • Poistamme jokaisesta toiminnasta kaksi komentoa: BEGIN ja COMMIT/ROLLBACK. Mutta useimmat DB:t käärivät automaattisesti kaikki toiminnot tapahtumaan, joten tämä voi olla turhaa.
  • Lukot eivät säily liian kauan – heti kun toiminto päättyy, sen hallussa olevat lukot vapautetaan verrattuna siihen, että odotettaisiin koko tapahtuman päättymistä.
  • Lukitukset ovat vähemmän todennäköisiä – lukkiutumista tapahtuu, kun kaksi tapahtumaa tarvitsevat toisen tapahtuman lukon. Tämä jättää molemmat tapahtumat odottamaan toisen valmistumista, mutta kumpikaan ei voi suorittaa loppuun. Umpikujia voi kuitenkin vielä esiintyä. Jotkin tietokantatoiminnot tekevät useita kirjoituksia, esim. hakemiston sisältävän sarakkeen päivittäminen päivittää sarakkeen ja indeksin (vähintään kaksi kirjoitusta). Tämä voi johtaa lukkiutumiseen, jos toinen yhteys yrittää toimia kyseisillä riveillä tai sarakkeilla.


Haittapuolena ovat korkeammat tietojen korruption riskit. Se ei ole sen arvoista, mutta ehkä siinä on käyttötapa, jota en voi ajatella.

Lähteet