Samtidighet og låser
Notater kap. 16–17. Hvordan databasen faktisk gjennomtvinger isolation når flere transaksjoner går samtidig — uten å bli for treg.
Klassiske anomalier
Før vi snakker om låser, må vi se hva som går galt uten dem. Disse fire mønstrene er årsaken til at concurrency control finnes.
Dirty read
T2 leser en verdi T1 har skrevet, men ikke committed. Hvis T1 aborter, har T2 jobbet på en fantasi-verdi.
Lost update
T1 og T2 leser samme verdi, modifiserer hver sin kopi, og skriver tilbake. Den som skriver sist «vinner» — den andres oppdatering forsvinner.
Non-repeatable read
T1 leser X to ganger. Mellom de to leseoperasjonene har T2 oppdatert X og committed. T1 ser to ulike verdier i samme transaksjon.
Phantom read
T1 utfører en spørring to ganger. Mellom dem har T2 satt inn en ny rad som matcher predikatet. Den «dukker opp» i andre kjøring.
Eksempel — Lost update
Begge transaksjonene leser samme initial verdi X = 100 og legger til 10:
SELECT COUNT(*) FROM ordrer WHERE status='ny' to ganger. Mellom de to kjøringene har T2 satt inn en ny ordre med status='ny'. Hvilken anomali er dette?SELECT-er. Forskjellen er hva slags låsing som trengs: phantoms krever predikat-/range-låser, ikke bare radlåser.
Shared og exclusive
Den enkleste mekanismen mot anomaliene: tving transaksjonene til å ta låser før de leser/skriver, og slipp dem etterpå.
- Shared (S, read lock) — flere transaksjoner kan holde S-lås på samme element samtidig.
- Exclusive (X, write lock) — bare én transaksjon kan holde X-lås, og ingen andre kan ha S-lås samtidig.
Forholdet mellom dem oppsummeres i kompatibilitetsmatrisen:
| Holder lås på X | Vil sette lås | ||
|---|---|---|---|
| S | X | ||
| Eksisterende | S | ✓ | ✗ |
| X | ✗ | ✗ | |
Tommelfingerregel: writers lockes ut alle. Readers tolererer hverandre.
Låser administreres av en låstabell i RAM. Hver aktiv transaksjon har en oppføring som peker til de låsene den allerede holder («has locks») og de den venter på («wants locks»). Operasjoner som må vente blir blokkert til lås frigjøres.
Two-phase locking
Bare det å bruke S/X-låser er ikke nok. Det er hvordan vi tar og slipper låsene som gir korrekthet:
2PL-regelen: Alle låser tas i en vokse-fase; så snart første lås er sluppet, går transaksjonen over til en krympe-fase hvor den bare kan slippe.
Resultatet ligner et fjell — låstellet vokser, peaker, og krymper. Aldri «tilbake opp» etter første unlock.
Hovedteorem: Hvis alle transaksjoner følger 2PL, er enhver schedule de produserer conflict-serializable. Det er grunnen til at 2PL er den de-facto standarden.
Hvorfor virker det?
Tenk: hvis en kant Ti→Tj finnes i presedensgrafen (Ti hadde en konfliktende op før Tj), må Ti ha sluppet sin lås før Tj tok sin. Hvis det også var en kant Tj→Ti, måtte Ti ha sluppet, deretter tatt en ny lås — brudd på 2PL. Altså: 2PL ⇒ ingen sykler ⇒ conflict-serializable.
Fire smaker av 2PL
| Variant | Når slippes låser? | Egenskaper |
|---|---|---|
| Basic 2PL | Når som helst etter peak — så lenge regelen holder. | Conflict-serializable. Ikke nødvendigvis cascadeless. |
| Conservative 2PL | Alle låser tas på forhånd, før første operasjon. | Deadlock-fri (alle Tx venter eller starter), men dårlig samtidighet. |
| Strict 2PL | X-låser holdes til commit/abort. S-låser kan slippes tidligere. | Conflict-serializable + strict schedule. Standard i praksis. |
| Rigorous 2PL | Alle låser holdes til commit/abort. | Enklest å implementere. Litt mindre samtidighet enn strict. |
I praksis bruker de fleste databaser strict 2PL eller rigorous 2PL — fordi de gjør recovery enkelt (undo via before-image fungerer). Basic 2PL er teoretisk korrekt, men gir cascading aborts.
Når begge venter på hverandre
Prisen for låser: noen ganger venter to transaksjoner på hverandre samtidig. Klassisk eksempel:
Wait-for-graf
Tegn en kant Ti → Tj hvis Ti venter på en lås Tj holder. Sykel ⇔ deadlock.
Tre strategier
- Forebygging — conservative 2PL (alle låser opp foran), eller rangér ressurser globalt så alle tar låser i samme rekkefølge.
- Deteksjon + offer — periodisk sjekk wait-for-grafen; finn sykel; abort en transaksjon (ofte den yngste eller den som har gjort minst).
- Timeout — om en transaksjon ikke fullfører innen X sekunder, abort. Enkelt, men vanskelig å sette gode timeouts.
De fleste kommersielle systemer (PostgreSQL, MySQL/InnoDB) bruker en kombinasjon av deteksjon og timeout.
Hva låser vi egentlig?
Et dataelement i 2PL-teori. I virkeligheten har vi et hierarki av muligheter:
Trade-off:
- Coarse (tabell-lås) — lite metadata å holde, men én skribent stenger ute alle.
- Fine (record-lås) — høy samtidighet, men låsetabellen blir stor og dyr.
Intent locks (IS, IX, SIX) brukes for å varsle at en transaksjon vil ha finer-granular låser i underliggende noder, slik at konflikter kan oppdages tidlig på høyere nivå uten å sjekke alle barn.
Multi-version concurrency control
Problem med rene låser: en read blokkerer en write og omvendt. Det betyr at en lang rapporterings-spørring kan stenge ute hele OLTP-traffikken.
MVCC (også kalt snapshot isolation) løser dette ved å la hver write lage en ny versjon i stedet for å overskrive. Reads ser et konsistent øyeblikksbilde fra et bestemt tidspunkt — typisk transaksjonens starttid.
Reads låser ikke writes, og writes låser ikke reads. Bare write/write-konflikter krever låsing eller (i optimistic varianter) abort av en transaksjon ved commit hvis det er konflikt.
PostgreSQL, Oracle, SQL Server (RCSI), MySQL/InnoDB — alle bruker MVCC. Det er de-facto i moderne SQL.
Snapshot isolation hindrer dirty/non-repeatable/phantom reads, men har sin egen anomali — write skew: to transaksjoner leser overlappende sett, hver skriver inn i sitt eget delsett basert på det den leste, og resultatet er inkonsistent. PostgreSQL «Serializable Snapshot Isolation» (SSI) løser det med ekstra konflikt-deteksjon.
Fire isolation levels
SQL-standarden definerer fire nivåer i henhold til hvilke anomalier som kan forekomme:
| Level | Dirty read | Non-repeatable | Phantom | Typisk implementasjon |
|---|---|---|---|---|
| READ UNCOMMITTED | Ja | Ja | Ja | Ingen leselåser |
| READ COMMITTED | Nei | Ja | Ja | X-låser, korte S-låser, eller snapshot |
| REPEATABLE READ | Nei | Nei | Avhenger | Strict 2PL på leste rader, eller snapshot |
| SERIALIZABLE | Nei | Nei | Nei | Strict 2PL m. range-låser, eller SSI |
«Nivå» = strengere lenger ned. Mer korrekthet, mindre samtidighet.
Standard vs. virkelighet
Standarden er stort sett ikke fulgt i detaljer av leverandørene:
- Oracle har ingen «REPEATABLE READ» — SERIALIZABLE er egentlig snapshot isolation.
- PostgreSQL sin REPEATABLE READ er snapshot isolation; SERIALIZABLE er SSI.
- MySQL InnoDB sin REPEATABLE READ tillater phantoms i noen tilfeller.
Tommelfingerregel: les dokumentasjonen til din database før du stoler på navnet.
Kort oppsummert
- Fire klassiske anomalier: dirty read, lost update, non-repeatable read, phantom.
- S/X-låser + kompatibilitetsmatrise styrer tilgang.
- 2PL (vokse-fase, krympe-fase) garanterer conflict-serializability.
- Strict 2PL = standard i praksis: X-låser holdes til commit, gir cascadeless.
- Deadlock = sykel i wait-for-graf. Løsninger: forebygging, deteksjon, timeout.
- Granularitet: tabell ↔ side ↔ record. Trade-off mellom kontensjon og overhead.
- MVCC / snapshot isolation: writers låser ikke readers og omvendt. Standard i moderne SQL.
- SQL isolation levels: READ UNCOMMITTED → READ COMMITTED → REPEATABLE READ → SERIALIZABLE. Sjekk hva din DB faktisk leverer.
Klar for siste etappe? 8C · Recovery →