Home Up PDF Prof. Dr. Ingo Claßen
Verteilte Transaktionen - ADBKT

Verteilte Transaktionen

Transaktionen, die mehr als einen Ressource-Manager umfassen

Typische Ressource-Manager sind Datenbank- oder Messaging-Systeme

Anforderungen

ACID muss wie bei nicht verteilten Transaktionen gewahrt werden

Alle RM müssen

  • die Transaktion erfolgreich beenden oder
  • alle ihre Änderungen rückgängig machen

Zwei-Phasen-Commit-Protokoll (2PC)

  • Umgang mit mit Systemfehlern,
  • z.B. Kommunikationsfehler (Netzwerkunterbrechung)
  • TM stürzt ab, RM stürzt ab
  • Sicherstellung der Atomarität

Spezifikationen

Distributed-Transaction-Processing-Modell (DTP)

TX-Spezifikation

  • Schnittstelle zwischen AP und TM
  • Das AP steuert die Transaktion: bot, commit, rollback

XA-Spezifikation

  • Schnittstelle zwischen TM und RM
  • Assoziation von Operationenen im RM mit einer Transaktion
  • Funktionen zur Abbildung des Zwei-Phasen-Commit-Protokolls

Überweisung Giro-/Sparkonto

Verschiedene Unternehmenseinheiten für Giro- und Sparkonten

Jeweils eigene IT-Infrastruktur

2PC - erfolgreicher Abschluss

2PC - Fehlerabbruch

Zustandsübergänge in RM

executing

  • RM führt Operationen durch, z. B. Änderungen an Daten
  • Bei Fehler kann sofort in den aborting/aborted-Zustand gewechselt werden
  • Nach der prepare-Aufforderung durch den TM wird bei positivem Ergebnis in den prepared-Zustand gewechselt
  • Der RM muss auf die Entscheidung durch den TM warten

prepared

  • Abhängig von der Entscheidung des TM wird die Transaktion erfolgreich beendet oder es findet ein Rückgängigmachen aller Operationen statt

aborting/aborted

  • Transaktion wurde abgebrochen

commiting/committed

  • Transaktion wurde erfolgreich beendet

Zustandsübergänge in TM

executing

  • TM startet commit-Bearbeitung
  • Sendet prepare-Meldung an alle RM
  • Kann aber Transaktion auch abbrechen

prepared

  • TM wartet auf Ergebnisse von den RM
  • Antworten alle RM positiv geht er in den Zustand committing über
  • Antwortet nur ein RM negativ geht er in den aborting-Zustand über

aborting, commiting

  • TM warted auf Bestätigungen

aborted/committed

  • Information zur Transaktion wird in den Programmstrukturen des TM gelöscht, da sie komplett abgeschlossen ist

Heuristische Entscheidungen

RM hat bei prepare mit yes geantwortet

TM stürzt ab

  • Alle Sperren müssen im RM gehalten werden
  • Andere Transaktionen auf diesem RM werden möglicherweise blockiert

Pragmatische Lösung für diesen Fall: RM können von sich aus, d. h. heuristisch, eine Entscheidung zum Ausgang der Transaktion treffen, entweder commit oder abort

  • Dadurch wird die Atomarität der Transaktion durchbrochen, da der TM möglicherweise eine andere Entscheidung zum Ausgang getroffen hat
  • In diesem Fall muss ggf. manuell wieder ein konsistenter Gesamtzustand des Systems hergestellt werden

2PC in Postgres

Überweisungs-Szenarien, in dem zwei Postgres-Datenbanken in einer Transaktion zusammenspielen.

Szenario 1

  • Abbuchen 100 Euro von Konto 1001 auf Server "pg"
  • Zubuchen 100 Euro auf Konto 1002 auf Server "pg2"
  • Erfolgreicher Abschluss

Szenario 2

  • Abbuchen 100 Euro von Konto 1001 auf Server "pg"
  • Zubuchen 100 Euro auf Konto 1002 auf Server "pg2"
  • Abbruch

Informationen dazu in der psycopg-Dokumentation.

  • Two-Phase Commit protocol support (link)
  • Two-Phase Commit support methods (link)

Zweiter Postgres-Server

Wichtig, um Konflikte mit dem ersten Server zu vermeiden:

  • Neuer Service- und Container-Name: pg2
  • Port 5432 wird als 5433 nach außen gereicht
networks:
  adbkt:
    external: true
    
services:
  pg2:
    container_name: pg2
    image: postgres:latest
    ports:
      - 5433:5432
    environment:
      POSTGRES_PASSWORD: htw-bln-pg
    networks:
      - adbkt

Technische Vorbereitungen

Datenbankserver konfigurieren

In pg und pg2 durchführen

su postgres
psql postgres

ALTER SYSTEM SET max_prepared_transactions = 100;

Container stoppen und starten (restart)

SHOW max_prepared_transactions;

Befehl zum Anzeigen der gesetzten Sperren

select relation::regclass, mode from pg_locks;

Hilfsfunktionen

def create_kto(conninfo):
    sql1 = "drop table if exists kto"
    sql2 = """ 
    create table kto (
      kid integer not null,
      val integer not null
    )
    """
    with psycopg.connect(conninfo) as conn:
        conn.execute(sql1) 
        conn.execute(sql2) 

def reset_kto(conninfo, kid, val):
    sql1 = "delete from kto"
    sql2 = f"insert into kto values ({kid}, {val})"
    with psycopg.connect(conninfo) as conn:
        conn.execute(sql1)
        conn.execute(sql2)

def show_kto(conninfo):
    sql = "select * from kto"
    with psycopg.connect(conninfo) as conn:
        rs = conn.execute(sql).fetchall()
        return rs

def print_all():
    print(f"pg1: {show_kto(conninfo1)}")
    print(f"pg2: {show_kto(conninfo2)}")

def prepare(db, conninfo, xid, kid, mode, updval):
    sql = f"update kto set val = val {mode} {updval} where kid={kid}"
    print(f"{db}: {sql}")
    conn = psycopg.connect(conninfo)
    conn.tpc_begin(xid)
    conn.execute(sql)
    conn.tpc_prepare()
    conn.close()

def commit(conninfo, db, xid):
    print(f"{db}: commit")
    with psycopg.connect(conninfo) as conn:
        conn.tpc_commit(xid) 

def rollback(conninfo, db, xid):
    print(f"{db}: rollback")
    with psycopg.connect(conninfo) as conn:
        conn.tpc_rollback(xid)

Initialisierung

import psycopg

conninfo1 = " ".join([
"user='postgres'",
"password='htw-bln-pg'",
"host='pg'",
"port=5432",
"dbname='postgres'"])
print(conninfo1)

conninfo2 = " ".join([
"user='postgres'",
"password='htw-bln-pg'",
"host='pg2'",
"port=5432",
"dbname='postgres'"])
print(conninfo2)

xid = "txn1"

create_kto(conninfo1)
create_kto(conninfo2)

reset_kto(conninfo1, 1001, 200)
reset_kto(conninfo2, 1002, 500)

print_all()
user='postgres' password='htw-bln-pg' host='pg' port=5432 dbname='postgres'
user='postgres' password='htw-bln-pg' host='pg2' port=5432 dbname='postgres'
pg1: [(1001, 200)]
pg2: [(1002, 500)]

Szenario 1

Daten verändern aber noch nicht betätigen

reset_kto(conninfo1, 1001, 200)
reset_kto(conninfo2, 1002, 500)

prepare("pg1", conninfo1, xid, 1001, "-", 100)
prepare("pg2", conninfo2, xid, 1002, "+", 100)
print_all()
pg1: update kto set val = val - 100 where kid=1001
pg2: update kto set val = val + 100 where kid=1002
pg1: [(1001, 200)]
pg2: [(1002, 500)]

Durch das "prepare" sind beide Datenbanken bereit für "commit" oder "rollback". Die Daten sind aber noch nicht verändert, da die Transaktion noch nicht betätigt wurde.

Das print_all() öffnet eine neue Verbindung und sieht daher die unveränderten Daten (Snapshot).

Erfolgreicher Abschluss

commit(conninfo1, "pg1", xid)
commit(conninfo2, "pg2", xid)
print_all()
pg1: commit
pg2: commit
pg1: [(1001, 100)]
pg2: [(1002, 600)]

Das "commit" kann auch dann noch erfolgreich durchgeführt werden, wenn der Server zwischenzeitlich abstürzt.

Szenario 2

Daten verändern aber noch nicht betätigen

reset_kto(conninfo1, 1001, 200)
reset_kto(conninfo2, 1002, 500)

prepare("pg1", conninfo1, xid, 1001, "-", 100)
prepare("pg2", conninfo2, xid, 1002, "+", 100)
print_all()
pg1: update kto set val = val - 100 where kid=1001
pg2: update kto set val = val + 100 where kid=1002
pg1: [(1001, 200)]
pg2: [(1002, 500)]

Abbruch

rollback(conninfo1, "pg1", xid)
rollback(conninfo2, "pg2", xid)
print_all()
pg1: rollback
pg2: rollback
pg1: [(1001, 200)]
pg2: [(1002, 500)]