Sisältö
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):
Hakee tai luo yhteyden tietokantaan ja asettaa sen tapahtumaksi.
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.
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ä.
Lisäyksen jälkeen tallennuspiste poistetaan joko tekemällä muutokset ja asettamalla ne saataville muulle tapahtumalle (mutta EI muille yhteyksille) tai peruuttamalla muutokset.
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.
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.
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.
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:
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;
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:
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:
Haittapuolena ovat korkeammat tietojen korruption riskit. Se ei ole sen arvoista, mutta ehkä siinä on käyttötapa, jota en voi ajatella.