Datakorrekthed ved flere brugere
Undgå overskrevne ændringer og dubletter, når flere bruger appen
En app kan virke fejlfrit for én person og stadig miste en kollegas note, oprette to bookinger eller sende samme handling flere gange. Problemet opstår, når brugere, baggrundsjob eller retries arbejder på de samme data næsten samtidig. Løsningen er at gøre forretningsreglerne atomiske, opdage konflikter og teste det endelige dataresultat.
Udgivet 17. august 2026 · Ca. 13 minutters læsetid
Tre fejl kan ligne hinanden, men kræver forskellig beskyttelse
Start med den konkrete forretningsfejl. “Der er concurrency” er ikke præcist nok til at vælge en sikker løsning.
| Situation | Hvad går galt? | Typisk kontrol |
|---|---|---|
| To redigerer samme sag | Den sidst gemte version overskriver den første uden varsel | Versionsfelt, ETag eller anden betinget opdatering |
| To booker sidste plads | Begge når at se “ledig” og opretter hver sin booking | Transaktion og en regel, databasen håndhæver atomisk |
| Et request gentages | Dobbeltklik, timeout, webhook eller retry udfører samme hensigt igen | Idempotency key eller stabilt hændelses-id |
Belastningstest undersøger, om systemet holder til trafik. Denne guide handler om, hvorvidt data stadig er korrekte, når handlinger overlapper — også ved lav trafik.
Beskriv reglen før teknologien
Skriv den tilstand, som aldrig må opstå, og den konflikt brugeren skal opleve. Det gør det muligt at vælge kontrol efter risiko i stedet for efter databasebrand.
- Booking: En tidsblok må højst have én aktiv booking.
- Ordre: Det samme betalings- eller webhook-event må højst oprette én ordre.
- Sagsnote: En medarbejder må ikke overskrive en nyere version uden at få konflikten vist.
- Lager eller saldo: Kontrol og ændring skal ske som én samlet handling mod aktuelle data.
Brug constraints til tilstande, der aldrig må eksistere
Hvis en regel kan udtrykkes direkte i databasen, er en constraint ofte den stærkeste sidste barriere. En unik constraint kan eksempelvis afvise to rækker med samme virksomheds-id og eksterne hændelses-id, selv når requests rammer samtidig.
- Unikhed: Eksternt event-id, ordrenummer eller en anden forretningsnøgle må ikke forekomme to gange i det relevante scope.
- Relationer: En booking, betaling eller note skal pege på en eksisterende ejer eller sag.
- Gyldige værdier: Status, mængde og datoer skal afvises, hvis de skaber en umulig post.
Vis stadig en forståelig fejl til brugeren. Databasefejlen er kontrollen; den er ikke den tekst, som skal stå i brugerfladen. Dokumentdatabaser har andre mekanismer, så reglen kan kræve en transaktion, en entydig dokumentnøgle eller betroet backendkode.
Saml kontrol og ændring i en transaktion
Mønstret “læs om tiden er ledig, og gem derefter bookingen” er usikkert, hvis læsning og skrivning er to uafhængige operationer. En transaktion lader databasen afgøre, om hele ændringen kan gennemføres mod en konsistent tilstand.
Firestore genkører en transaktionsfunktion, hvis et dokument, den har læst, ændres samtidig. Derfor må funktionen ikke sende mail, trække betaling eller ændre lokal skærmtilstand. Registrér i stedet den ønskede følgehandling som data, og lad en separat, idempotent proces udføre den efter et vellykket commit.
await runTransaction(db, async (tx) => {
const slot = await tx.get(slotRef);
if (slot.data().booked) {
throw new Error("Tiden er allerede booket");
}
tx.update(slotRef, { booked: true, bookingId });
tx.set(outboxRef, { type: "booking-confirmed", bookingId });
});
// Send ikke mail inde i transaktionsfunktionen.Firestore-transaktioner kræver læsninger før skrivninger, kan køre flere gange og fejler offline. Håndtér derfor både konflikt, forbindelsesfejl og et endeligt afslag i brugerfladen. Andre databaser har andre transaktions- og låsemekanismer.
Opdag en nyere version før I overskriver den
Ved redigering af noter, tilbud eller sager er det ofte bedre at afvise en forældet gemning end at låse posten, mens en bruger tænker. Gem en version med posten, send den med i opdateringen, og gennemfør kun ændringen, hvis versionen stadig matcher.
UPDATE cases
SET note = $1, version = version + 1
WHERE id = $2 AND version = $3
RETURNING id, version;
-- 0 rows: en anden har gemt siden version $3Hvis ingen række opdateres, henter appen den nye version og viser konflikten. Et HTTP-API kan bruge samme princip med en stærk ETag og If-Match. RFC 9110 beskriver netop betingede requests som beskyttelse mod tabte opdateringer.
- Ikke-kritisk felt: Tilbyd genindlæsning og eventuelt manuel sammenfletning.
- Kritisk status: Afvis ændringen og kræv, at brugeren tager stilling til den aktuelle tilstand.
- Automatisk gemning: Vis “gemmer”, “gemt” og “nyere version fundet”; skjul ikke konflikten bag et grønt flueben.
Gør gentagne requests sikre med idempotens
En timeout fortæller kun, at klienten ikke modtog svaret. Serveren kan allerede have oprettet ordren. Et blindt retry kan derfor gentage handlingen. Generér en stabil nøgle for brugerens ene hensigt, og genbrug den ved retry.
- 1. Opret nøglen før requestet. Et nyt klik efter en reel ændring i brugerens valg skal have en ny nøgle; et teknisk retry skal beholde den gamle.
- 2. Bind nøglen til scope og input. Samme nøgle må ikke kunne genbruges af en anden virksomhed eller til en ændret handling.
- 3. Gem nøgle og resultat atomisk. Brug en unik regel, så to samtidige requests ikke begge bliver “den første”.
- 4. Returnér det kendte resultat. Et retry skal få den eksisterende ordre eller status, ikke udføre handlingen igen.
- 5. Aftal levetid og oprydning. Perioden skal passe til hvor længe leverandøren, køen eller klienten realistisk kan gentage hændelsen.
HTTP definerer blandt andet PUT, DELETE og sikre metoder som idempotente, men et POST bliver ikke automatisk sikkert at gentage. Appens forretningshandling skal være designet til det. En tilfældig timestamp eller deaktiveret knap er ikke tilstrækkelig.
Retry kun det, der kan prøves igen sikkert
Databaser kan afvise en transaktion på grund af en reel konflikt. PostgreSQLs højere isolationsniveauer kan kræve, at hele transaktionen køres igen efter en serialiseringsfejl, og Firestore-klienter genprøver bestemte transaktioner automatisk. Det gør retry til en del af designet — ikke en generel løsning på alle fejl.
- Genprøv: Dokumenterede, midlertidige konflikter med et begrænset antal forsøg og ventetid.
- Afvis: Ugyldigt input, manglende rettighed, udsolgt tid eller anden permanent forretningskonflikt.
- Stop og undersøg: Ukendt commitstatus, gentagne konflikter eller en operation med ekstern sideeffekt uden idempotens.
Hold transaktioner korte, og udfør ikke langsomme tredjepartskald, mens databaselåse eller en retrybar transaktion er aktiv. Høj konfliktfrekvens kan være et tegn på, at datamodellen samler for meget aktivitet i den samme post.
Test overlappet — ikke kun handlingerne hver for sig
To tests, der køres efter hinanden, beviser ikke samtidig brug. Lav kontrollerede scenarier, hvor requests eller browsere rammer den samme forretningsregel før den første handling er færdig.
- Åbn samme sag i to browsere, gem forskellige ændringer, og kontrollér at én ikke forsvinder lydløst.
- Forsøg at booke den sidste plads samtidig fra to konti; der må kun findes én gyldig booking.
- Send samme request to gange med samme idempotency key; forretningsresultatet må kun opstå én gang.
- Simulér at svaret mistes efter commit, og gentag requestet uden at skabe en ny handling.
- Lever samme webhook eller jobhændelse flere gange og i en anden rækkefølge, hvis leverandøren kan gøre det.
- Afprøv offline-genforbindelse, autosave og en lang åben formular mod data, som en anden ændrer.
Mål konflikter uden at skjule dem
En korrekt afvist konflikt er ikke nødvendigvis en driftsfejl, men mange konflikter kan gøre appen ubrugelig. Registrér type, endpoint eller funktion, berørt objekttype, resultat og korrelations-id — uden at logge hele noter, tokens eller følsomme payloads.
- Følg antallet af versionskonflikter og hvor ofte brugeren opgiver efter dem.
- Alarmér på dubletbrud, udtømte transaktionsretries og fastlåste følgehandlinger.
- Skeln mellem en forventet forretningskonflikt og en ukendt teknisk fejl.
- Bevar et revisionsspor, hvis virksomheden skal kunne se, hvem der ændrede en kritisk post.
Typiske fejl
- Sidste gemning vinder altid: En ældre formular overskriver nyere data uden varsel.
- Tjek og gem er to kald: Begge brugere ser den samme ledige plads, før nogen af dem skriver.
- Knappen deaktiveres: Dobbeltklik begrænses i én browser, men retries, andre klienter og webhooks er stadig ubeskyttede.
- Mail sendes inde i transaktionen: Funktionen genkøres og udsender samme besked mere end én gang.
- Alle fejl prøves igen: Permanent ugyldigt input eller en reel konflikt skaber en retry-storm.
- Kun successvaret testes: Dubletten eller den tabte ændring opdages ikke i data.
- Versionskontrol bruger klientens ur: Uens ure og afrundede timestamps bliver en svag konfliktkontrol.
Tjekliste før flere arbejder i appen
- ☐ Kritiske regler for booking, ordre, status, lager og redigering er skrevet i almindeligt sprog.
- ☐ Hver regel har en navngiven konsekvens, hvis to handlinger overlapper.
- ☐ Browserkontrol suppleres af håndhævelse i backend eller database.
- ☐ Umulige tilstande er beskyttet af relevante constraints, nøgler eller atomiske regler.
- ☐ Kontrol og ændring sker i én transaktion, når de skal lykkes eller fejle samlet.
- ☐ Lange formularer og autosave opdager en nyere version før overskrivning.
- ☐ Gentagne requests, webhooks og jobs bruger stabil idempotens inden for det rigtige scope.
- ☐ Sideeffekter udføres ikke inde i en transaktionsfunktion, som kan genkøres.
- ☐ Retry er begrænset til dokumenterede midlertidige fejl og har stopkriterier.
- ☐ Konflikter vises forståeligt, så brugeren kan genindlæse, sammenligne eller prøve igen.
- ☐ Tests overlapper handlinger og kontrollerer det endelige dataresultat.
- ☐ Logs og alarmer kan skelne konflikter, dubletter, udtømte retries og ukendte fejl.
Relaterede guides
Officielle kilder
Den konkrete løsning afhænger af databasens garantier og appens arkitektur. Rådene er kontrolleret mod de aktuelle officielle beskrivelser af PostgreSQL-isolation og constraints, Firestore-transaktioner og HTTP-standardens idempotens og betingede requests.