Transaktion
en grupp databasändringar som genomförs helt eller inte alls

Foto: Efraimstochter / Pixabay 
Foto: JancickaL / Pixabay
Använd begreppet i ett beslut
Använd ”Vad är en transaktion?” i ett konkret beslut. Vi kan knyta begreppet till systemet du bygger eller köper. En struktur som speglar hur verksamheten faktiskt fungerar, så att de frågor du behöver svar på senare fortfarande går att ställa.
Granska antagandena bakom ordet
Använd förklaringen och det länkade arbetet för att formulera frågor till projektet. Vi kan undersöka antagandena tillsammans innan ett verktyg väljs. Utformade och drev 6 ETL/ELT‑pipelines — 6 ETL‑pipelines över tre databasmotorer
Vad det är
En transaktion är en grupp ändringar som en databas genomför som en enhet: antingen får alla verkan eller ingen av dem. När den behöver hållas isär från en betalning, som också kallas transaktion, skriver man databastransaktion. PostgreSQL-handledningen beskriver idén så här: flera steg samlas i en enda operation där allt eller inget sker, och ingen annan ser något av det som ligger däremellan.
I PostgreSQL öppnas en transaktion med BEGIN och avslutas antingen med
COMMIT, som gör ändringarna beständiga och synliga för andra sessioner, eller
med ROLLBACK, som kastar dem. En sats som skickas ensam körs också i en
transaktion: PostgreSQL lägger ett implicit BEGIN runt den och, om den lyckas,
ett implicit COMMIT. En lång transaktion kan sätta en sparpunkt med
SAVEPOINT och senare gå tillbaka till den med ROLLBACK TO, så att arbetet
efter punkten kastas medan allt före den behålls.
Garantierna en transaktion ger sammanfattas som ACID. Atomicitet betyder att transaktionen genomförs som en enhet eller inte alls och att inget halvfärdigt resultat överlever en krasch. Konsistens betyder att integritetsvillkoren som definierats för datan gäller när den committas, och att en transaktion som skulle bryta mot något av dem rullas tillbaka i stället. Isolering betyder att dess ändringar är osynliga för samtidiga transaktioner tills den har committats. Hållbarhet betyder att en committad ändring överlever att servern kraschar.
Isolering finns i nivåer, och SQL-standarden definierar fyra: Read Uncommitted,
Read Committed, Repeatable Read och Serializable. PostgreSQL startar varje
transaktion på Read Committed om inget annat anges, och dess Read Uncommitted
beter sig exakt som Read Committed. På Read Committed ser varje sats de data som
committats innan satsen började, så två frågor i samma transaktion kan se olika
siffror. På Repeatable Read arbetar hela transaktionen utifrån en
ögonblicksbild, tagen när dess första sats efter BEGIN körs. Serializable går
längre och ser till att samtidiga transaktioner ger samma resultat som om de
hade körts en i taget.
Varför det spelar roll
Nästan varje handling som är värd att registrera rör mer än en rad. En order är en rad i en tabell, en nedräkning av lagret i en annan och en post i en huvudbok. Utan transaktion kan en krasch, en timeout eller ett brutet integritetsvillkor mellan andra och tredje satsen lämna kvar en order vars lager aldrig drogs, eller lager som dragits för en order som inte finns. Sedan hittar någon det i en rapport och rättar det för hand. Med en transaktion finns det halva tillståndet aldrig för någon att se, och det är databasen, inte applikationen, som garanterar det.
Det andra skälet är samtidighet. Två anrop som kommer samtidigt körs sida vid
sida, och isoleringsnivån avgör vad vart och ett ser medan lås avgör vilket som
får vänta. Det är här team oftast blir överraskade: att slå in kod i BEGIN och
COMMIT hindrar inte i sig två anrop från att läsa samma saldo och båda
spendera det. På Read Committed läser varje sats det som var committat när den
startade, så en kontroll i en sats och en skrivning i nästa kan skiljas åt av en
commit från en annan session. Kontrollen måste antingen låsa raden först eller
bli en del av skrivningen.
Transaktioner har också ett pris, och det betalas i tid. Radlås som tas av
UPDATE, DELETE och SELECT ... FOR UPDATE hålls tills transaktionen
avslutas, och en session som vill ändra eller låsa samma rad väntar så länge det
tar. En öppen transaktion hindrar också VACUUM från att ta bort gamla
radversioner som den fortfarande kan behöva se, så en transaktion som står öppen
i timmar får tabellerna att svälla. Designfrågan är därför aldrig bara om en
transaktion ska användas, utan var dess gränser går och hur lite som händer
mellan dem.
Slutligen omfattar en transaktion en databas och inget annat. Ett anrop till en betalleverantör, ett mejl eller ett meddelande på en kö sker utanför den, och en rollback kan inte ta tillbaka något av dem. Att bestämma vad som hamnar innanför gränsen, och hur omvärlden får veta om en commit, är det mesta av arbetet med att utforma en transaktion.
Så ser det ut i praktiken
En medlem i ett lojalitetsprogram löser in 250 poäng mot en biobiljett. Tre saker måste ske tillsammans: plånboken förlorar poängen, huvudboken registrerar varför och inlösen skapas. Skrivet som en transaktion ser det ut så här:
BEGIN;
UPDATE wallet
SET points = points - 250
WHERE member_id = 42
AND points >= 250
RETURNING points;
-- ingen rad tillbaka: för få poäng, så ROLLBACK och stopp här
INSERT INTO points_entry (member_id, delta, reason)
VALUES (42, -250, 'inlöst: biobiljett');
INSERT INTO redemption (member_id, reward_id)
VALUES (42, 7);
COMMIT;UPDATE tar ett radlås på plånboken för medlem 42. Fram till COMMIT ser varje
annan session som läser plånboken fortfarande det gamla saldot, eftersom radlås
inte blockerar läsare och ändringar som inte har committats är osynliga. En
andra inlösen för samma medlem, skickad från en annan flik ett ögonblick senare,
når sin egen UPDATE och väntar. När den första transaktionen committas
utvärderar PostgreSQL på Read Committed den andras WHERE-villkor på nytt mot
raden som den ser ut nu. Om bara 100 poäng finns kvar är villkoret inte
uppfyllt, ingen rad kommer tillbaka och det andra anropet rullas tillbaka utan
att ha skrivit något. Om i stället den sista INSERT misslyckas, till exempel
för att belöning 7 har dragits tillbaka och en främmande nyckel avvisar den,
rullar klienten tillbaka och poängen är exakt där de var.
Det här är formen på lojalitetsplånboken med ligor. Varje skrivoperation där tar ett radlås på plånboken innan den läses, så att det andra anropet väntar på att det första ska bli klart i stället för att kapplöpa med det, och eftersom plånboken ligger i samma databas som allt annat committas saldot och det som köptes för det tillsammans eller inte alls. Sidan säger öppet vad det kostar: radlåsning är det långsammare svaret, och vid trängsel får transaktionerna vänta på sin tur, men alternativet är en medlem som spenderar samma poäng två gånger.
Anteckningen om
att sälja den sista biljetten exakt en gång
prövar samma verktyg under en blixtrea. Lösningen där gör kontrollen och
nedräkningen till en enda villkorad UPDATE, eftersom ett lås som hålls medan
applikationen tänker mellan en SELECT och en UPDATE får nästa köpare att
vänta varenda millisekund av det. Först på listan över resten av arbetet står
att hålla allt externt utanför transaktionen: ett betalningsanrop, en
HTTP-begäran eller ett publicerat meddelande hör hemma före den eller efter den.
Var det går fel
En session står overksam i en transaktion och allt bakom den får vänta. En
applikation öppnar en transaktion, uppdaterar en rad och väntar sedan på ett
långsamt API, på användarens nästa klick eller på en bakgrundsprocess som
kraschade utan att någonsin skicka COMMIT. Låsen ligger kvar, och PostgreSQL
låter en session som vill ha ett konflikterande lås vänta på obestämd tid om den
inte upptäcker ett deadlock. Dokumentationen kallar det en dålig idé att hålla
en transaktion öppen medan man väntar på indata från användaren.
pg_stat_activity visar sådana sessioner med tillståndet idle in transaction,
och xact_start anger när transaktionen började:
SELECT pid, now() - xact_start AS open_for, state, query
FROM pg_stat_activity
WHERE state LIKE 'idle in transaction%'
ORDER BY xact_start;Flytta ut det långsamma arbetet ur transaktionen, och ge servern en gräns så att
en bortglömd transaktion inte kan hålla lås i timmar.
idle_in_transaction_session_timeout avslutar varje session som har stått
overksam i en öppen transaktion längre än värdet. Standardvärdet är 0, vilket
stänger av den, så den måste sättas, till exempel för applikationens roll med
ALTER ROLE app SET idle_in_transaction_session_timeout = '30s';.
Två anrop spenderar samma saldo. Koden läser ett värde i en sats, fattar
beslut i applikationen och skriver resultatet i en annan. Två sessioner läser
samma tal, båda klarar kontrollen och båda skriver, så den ena ändringen skriver
tyst över den andra. Ett omgivande BEGIN förhindrar det inte på Read
Committed. Gör kontrollen till en del av skrivningen med ett villkor i UPDATE,
som i exemplet ovan, eller lås raden innan den läses med
SELECT ... FOR UPDATE, så att den andra sessionen väntar på den första.
Fel dyker upp under last på Repeatable Read eller Serializable. På de här
nivåerna avvisar PostgreSQL en transaktion som skulle bryta nivåns löfte och
rapporterar ett serialiseringsfel, SQLSTATE 40001, med meddelanden som
could not serialize access due to concurrent update. Det är så det är tänkt
att fungera, och dokumentationen säger uttryckligen att applikationer som
använder de här nivåerna måste vara beredda att försöka igen. Fånga 40001 och
kör hela transaktionen igen från BEGIN, läsningarna inräknade, eftersom
beslutet den fattade byggde på data som sedan har ändrats. Att bara köra om den
sats som misslyckades upprepar misstaget.
Två transaktioner hamnar i deadlock (baklås). Den ena låser order 1 och sedan order 2, den andra låser order 2 och sedan order 1, och var och en väntar på den andra. PostgreSQL upptäcker cirkeln och avbryter en av transaktionerna så att den andra kan bli klar, och vilken den väljer är svårt att förutse. Försvaret som dokumentationen anger är att alla kodvägar tar lås i samma ordning, till exempel genom att uppdatera rader i stigande ordning efter primärnyckel, och att en transaktion som avbröts på grund av deadlock körs igen.
Ett mejl går ut för en order som rullades tillbaka. Applikationen skickar en bekräftelse eller publicerar en händelse inifrån transaktionen, och sedan misslyckas en senare sats. Rollbacken tar bort ordern, men inget kan kalla tillbaka mejlet. Skriv meddelandet i en outbox-tabell i samma transaktion som ändringen det beskriver, och låt en separat process skicka det när commiten har skett. Då finns meddelandet exakt när ordern finns.
Läs mer
- Transactions — kapitlet i PostgreSQL-handledningen: allt-eller-inget-beteende, implicita transaktioner och sparpunkter, visade med en banköverföring.
- Transaction Isolation — de fyra standardnivåerna, vad var och en ser i PostgreSQL och serialiseringsfelen som kräver ett nytt försök.
- Explicit Locking — rad- och tabellås, hur länge de hålls och hur deadlocks upptäcks och undviks.
- Client Connection Defaults
—
default_transaction_isolation,idle_in_transaction_session_timeout,lock_timeoutoch de andra gränserna för en session. - Routine Vacuuming — varför gamla radversioner ligger kvar så länge någon transaktion fortfarande kan se dem, och vad långa öppna transaktioner gör med städningen.

