Registrering av ordre.
SQL-setningene for registrering av ordren blir som vist under (i forhold til tabelldefinisjonene i Vedlegg B hopper vi over noen av kolonnene).
Kolonnen OrdreNr ville egnet seg for autonummerering, men her bare velger vi 5005 som et nytt ordrenummer (og antar at dette er første ledige nummer).
-- I noen systemer (f.eks. MySQL) startes transaksjoner eksplisitt, slik:
START TRANSACTION;
INSERT INTO Ordre(OrdreNr, OrdreDato, KNr)
VALUES (5005, '2019-08-10', '5002');
INSERT INTO Ordrelinje(OrdreNr, VNr, PrisPrEnhet, Antall)
VALUES (5005, '12055', 40, 25);
INSERT INTO Ordrelinje(OrdreNr, VNr, PrisPrEnhet, Antall)
VALUES (5005, '33045', 50, 10);
INSERT INTO Ordrelinje(OrdreNr, VNr, PrisPrEnhet, Antall)
VALUES (5005, '25079', 60, 1);
UPDATE Vare
SET Antall = Antall-25
WHERE VNr = 12055;
UPDATE Vare
SET Antall = Antall-10
WHERE VNr = 33045;
UPDATE Vare
SET Antall = Antall-1
WHERE VNr = 25079;
COMMIT;
En mer praktisk måte å gjøre dette på vil være at kundebehandleren taster inn informasjonen i et skjema ("skjermbilde"). I skjemaet kan man tenke seg en "Lagre"-knapp som gjør at SQL-setningene over blir utført.
ROLLBACK
Erstatt COMMIT med ROLLBACK i løsningsforslaget til oppgave 1. (Hvis du allerede har kjørt eksempelkoden over med ordrenummer 5005, så bør du nå bytte til f.eks. 5006, slik at du ikke får feilmelding for brudd på primærnøkkelregler.) Kjør deretter utvalgsspørringer for å sjekke at tabellinnholdet er uendret.
Eksempler på samtidighetsproblemer. Vi tenker oss et system uten skikkelig samtidighetskontroll, og også at skriveoperasjoner blir utført direkte til disk (uten bruk av transaksjonslogg).
Tapt oppdatering
T1 og T2 skal begge oppdatere antall enheter på lager for vare 12055. La X stå for adressen der denne verdien er lagret. Vi antar at det er 100 enheter på lager av denne varen. Problemet starter når T2 avleser X etter at T1 er i gang med oppdatering, men før T2 har skrevet resultatet tilbake. Oppdateringen til T1 går tapt når T2 overskriver.
Tidspunkt | Lokal T1-kopi | T1 | Databasen | T2 | Lokal T2-kopi |
1 | 100 | ||||
2 | 100 | Les inn X | 100 | ||
3 | 75 | Tell ned med 25 | 100 | Les inn X | 100 |
4 | 75 | Skriv til disk | 75 | Tell ned med 5 | 95 |
5 | 75 | 95 | Skriv til disk | 95 |
Angret oppdatering
Bruker igjen T1 og T2, og X står for antall enheter på lager av vare 12055 som over. T1 gjennomfører hele operasjonen inkludert oppdatering av databasen. T2 avleser ny verdi, før T1 angrer (rollback). T2 har dermed observert "sluttresultatet" til T1 som jo aldri vil bli registrert.
ROLLBACK skal altså angre operasjonen som T1 har utført, men det må i så fall bety at også T2 må angres. Oppgaven forutsetter at vi bruker et databasesystem uten skikkelig flerbrukerhåndtering, så vi nøyer oss med å antyde at lokal kopi i T2 ikke er gyldig.
Tidspunkt | Lokal T1-kopi | T1 | Databasen | T2 | Lokal T2-kopi |
1 | 100 | ||||
2 | 100 | Les inn | 100 | ||
3 | 75 | Tell ned med 25 | 100 | ||
4 | 75 | Skriv til disk | 75 | ||
5 | 75 | 75 | Les inn | 75 | |
6 | 75 | 75 | Tell ned med 5 | 70 | |
7 | ROLLBACK | 100 | Ugyldig lokal verdi | 70 |
Inkonsistent analyse
T2 legger til 2 rader i Ordrelinje. T3 summerer salg pr. kunde og vil gi forskjellige rersultat avhengig av om den blir utført før eller etter T2. La oss si at salgsverdiene av de to ordrelinjene øker salgssummen for kunde 5091 med henholdsvis 50 og 100 kroner, og at total salgssum for kunde 5091 forut for T2 var 1200 kroner. Hvis T3 blir utført før T2 får vi svaret 1200. Hvis T3 blir utført etter T2 får vi svaret 1350. Følgende forløp gir salgssum lik 1250 kroner - som er feil:
Tidspunkt | T2 | T3 | Salgsverdi |
1200 | |||
1 | Skriv ordrelinje 1 | 1250 | |
2 | Les tabeller og beregn salgssum | 1250 | |
3 | Skriv ordrelinje 2 | 1350 |
Ingen av disse problemene kan oppstå dersom ingen transaksjoner blir utført samtidig. Problemer kan heller ikke oppstå for to transaksjoner som opererer på ulike dataområder (selv om de blir utført samtidig).
Forløp med låser
Forløpet under viser hvordan problemet med tapt oppdatering kan unngås ved hjelp av låser (T2 må vente). Begge transaksjoner må bruke skrivelåser ettersom de skal oppdatere X. Uansett om transaksjonene låser en enkelt rad, hele tabeller, eller hele databasen får vi i dette eksemplet venting, fordi transaksjonene skal oppdatere samme rad.
Tidspunkt | T1 | T2 | X |
1 | Skrivelås X | 100 | |
2 | Les inn X | Skrivelås X | 100 |
3 | Tell ned X med 25 | Vent | 100 |
4 | Skriv X til disk | Vent | 75 |
5 | Lås opp X | Vent | 75 |
6 | Les inn X | 75 | |
7 | Tell ned X med 5 | 75 | |
8 | Skriv X til disk | 70 | |
9 | Lås opp X | 70 |
Når T1 låser X må T2 vente, og det er dette som gjør at problemet unngås. Tilsvarende venting inntreffer for de to andre situasjonene (angret oppdatering og inkonsistent analyse) når vi bruker låser. Låser er altså en måte å forhindre "farlig" samtidighet.
Serialiserbart forløp
Et forløp er serialiserbart hvis det har samme effekt som et sekvensielt forløp (som er et forløp der transaksjoner blir utført etter hverandre - en av gangen). Vi ønsker så mye samtidighet som mulig, men samtidig insisterer vi på serialiserbare forløp.
Ikke-serialiserbart forløp
Denne deloppgaven gikk ut på å lage et forløp som ikke er serialiserbart, og altså gir galt svar. Anta både X og Y har startverdi 10. Hvis man utfører T4 før T5 vil både X og Y få verdien 40. Hvis man utfører T5 før T4 vil både X og Y få verdiene 30. Dette er altså de to korrekte forløpene. Ved å la T4 operere først på X og la T5 operere først på Y får X verdien 40 og Y verdien 30, som altså er galt. Merk at både T4 og T5 bryter med to-faselåsing, ettersom de låser data-elementer etter at de har låst opp et annet.
Tidspunkt | T4 | T5 | X | Y |
10 | 10 | |||
1 | Skrivelås X | Skrivelås Y | 10 | 10 |
2 | Les inn X | Les inn Y | 10 | 10 |
3 | Øk X med 10 | Gang Y med 2 | 10 | 10 |
4 | Skriv X til disk | Skriv Y til disk | 20 | 20 |
5 | Lås opp X | Lås opp Y | 20 | 20 |
6 | Skrivelås Y | Skrivelås X | 20 | 20 |
7 | Les inn Y | Les inn X | 20 | 20 |
8 | Øk Y med 10 | Gang X med 2 | 20 | 20 |
9 | Skriv Y til disk | Skriv Y til disk | 40 | 30 |
10 | Lås opp Y | Lås opp X | 40 | 30 |
Tofaselåsing
Med tofaselåsing vil enhver transaksjon følge tre faser - låsing, behandling og deretter opplåsing. Man tillater altså ikke transaksjoner som låser, behandler, låser opp, og deretter låser nye data. Hvis man har to transaksjoner som følger tofaselåsing, og som jobber med de samme dataene, blir forløpet serialiserbart. En enkel strategi for å gjøre forløpet i punkt 4 serialiserbart er å flytte WRITELOCK-operasjonene først og UNLOCK-operasjonene til slutt.
Tidspunkt | T4 | T5 | X | Y |
10 | 10 | |||
1 | Skrivelås X | 10 | 10 | |
2 | Skrivelås Y | 10 | 10 | |
3 | Les inn X | Skrivelås Y | 10 | 10 |
4 | Øk X med 10 | Vent | 10 | 10 |
5 | Skriv X til disk | Vent | 20 | 10 |
6 | Lås opp X | Vent | 20 | 10 |
7 | Les inn Y | Vent | 20 | 10 |
8 | Øk Y med 10 | Vent | 20 | 10 |
9 | Skriv Y til disk | Vent | 20 | 20 |
10 | Lås opp Y | Vent | 20 | 20 |
11 | Vent | 20 | 20 | |
12 | Skrivelås X | 20 | 20 | |
13 | Les inn Y | 20 | 20 | |
14 | Gang Y med 2 | 20 | 20 | |
15 | Skriv Y til disk | 20 | 40 | |
16 | Lås opp Y | 20 | 40 | |
17 | Les inn X | 20 | 40 | |
18 | Gang X med 2 | 20 | 40 | |
19 | Skriv X til disk | 40 | 40 | |
20 | Lås opp X | 40 | 40 |
Vranglås
Hvis vi bruker samme forløp som i punkt 5, men lar T5 starte litt før oppstår det vranglås. Viser her kun operasjonene ned til tidspunktet da gjensidig venting oppstår:
Tidspunkt | T4 | T5 |
1 | Skrivelås X | Skrivelås Y |
2 | Skrivelås Y | Skrivelås X |
3 | Vent | Vent |
... | ... | ... |
Transaksjonslogg
Når strømmen er tilbake startes databasesystemet opp igjen. Problemet er at det kan ha vært transaksjoner halvveis utført på tidspunktet da strømbruddet inntraff, og uten transaksjonslogg er det ikke mulig å vite hvilke transaksjoner dette gjelder. På loggen er det skrevet før- og etter-verdier for alle deloperasjoner som er utført, og alle oppdateringer blir skrevet til loggen før selve databasen blir oppdatert. Ved gjenoppretting av databasen kan loggen brukes for å gjenskape databasen til tilstanden like før strømbruddet.
Eksperiment: Oppdatering fra to transaksjoner
Hvis en transaksjon T1 allerede har startet oppdatering av en bestemt rad, vil transaksjon T2 bli satt "på vent" ved forsøk på oppdatering av samme rad (hvis systemet er satt opp med vanlig pessimistisk låsing). Vi kan prøve å "lage" en vranglås ved å starte en transaksjon T1 som først oppdaterer tabell A og en transaksjon T2 som først oppdaterer tabell B. Deretter lar vi T1 forsøke å oppdatere tabell B, og T2 forsøke å oppdatere tabell A. DBHS vil avbryte en av transaksjonene, enten umiddelbart eller etter en kort stund.
Eksperiment: Isolasjonsnivåer
Vi definerer to transaksjoner: T1 oppdaterer Vare-tabellen, mens T2 bare avleser (gjentatte ganger). Her er rekkefølgen som kommandoene blir utført:
T1: SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
T2: SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
T1: SELECT * FROM Vare;
T2: SELECT * FROM Vare;
T1: UPDATE Vare SET Pris=Pris+10 WHERE VNr=12055;
T1: SELECT * FROM Vare;
T2: SELECT * FROM Vare;
T1: COMMIT;
T2: SELECT * FROM Vare;
T2: COMMIT;
T1 observerer sin egen oppdatering umiddelbart. For T2 vil derimot Vare-tabellen se lik ut gjennom hele forløpet. Oppdateringen som T2 gjør blir ikke synlig for T1, selv ikke etter at T1 utfører COMMIT. Endre så de to første kommandoene til:
T1: SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
T2: SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
Nå vil T2 observere prisendringen på vare 12055 etter at T1 har utført COMMIT.