Zum Inhalt
wannacode

Verbindungspools, Backups und Berechtigungen

Kurz gesagt: Eine Datenbank hinter einer laufenden Website braucht einen Pool, der offene Verbindungen wiederverwendet, Backups, aus denen man wirklich wiederherstellen kann, und einen Benutzer mit nur den nötigen Rechten.

Problem

Freitagabend

Der Shop macht eine Aktion, und innerhalb von fünf Minuten kommen 400 Leute. Die Seite bleibt stehen, und im Log steht:

FATAL: sorry, too many clients already

Das Programm öffnet für jede Seitenansicht eine neue Verbindung zur Datenbank und schließt sie am Ende. Das Öffnen ist nicht umsonst: ein Weg übers Netzwerk, eine Anmeldung mit Passwort und in PostgreSQL ein neuer Prozess auf dem Server. Und der Server nimmt nur eine begrenzte Zahl von Verbindungen an (max_connections, standardmäßig 100). Vierhundert Seitenansichten gleichzeitig sind vierhundert Verbindungen.

Die Idee

Ein Verbindungspool

Ein Pool hält ein paar Verbindungen offen. Eine Seitenansicht nimmt eine freie, benutzt sie und gibt sie zurück, und die nächste nimmt dieselbe. Das Öffnen wird einmal bezahlt, und die Datenbank sieht nie mehr Verbindungen als die Größe des Pools.

Hier ist ein kleiner Pool mit drei Verbindungen, von Hand geschrieben, damit du siehst, was er tut. borrow gibt eine freie Verbindung, und wenn keine da ist, öffnet es eine neue, aber nie mehr als drei. giveBack gibt sie zurück:

static final int MAX = 3;
static final Deque<Connection> free = new ArrayDeque<>();
static int opened = 0;
static Connection borrow() throws SQLException {
if (!free.isEmpty()) return free.pop();
if (opened == MAX) throw new SQLException("der Pool ist leer");
opened++;
Connection conn = DriverManager.getConnection("jdbc:sqlite::memory:");
setup(conn);
return conn;
}
static void giveBack(Connection conn) {
free.push(conn);
}
static String productName(int id) throws SQLException {
Connection conn = borrow();
try (PreparedStatement ps = conn.prepareStatement("SELECT name FROM products WHERE id = ?")) {
ps.setInt(1, id);
try (ResultSet rs = ps.executeQuery()) {
if (!rs.next()) return "kein solches Produkt"; // die Verbindung wird nie zurückgegeben
String name = rs.getString("name");
giveBack(conn);
return name;
}
}
}
for (int id : new int[] { 1, 99, 5, 98, 97, 7 }) {
try {
System.out.println("Produkt " + id + ": " + productName(id));
} catch (SQLException e) {
System.out.println("Produkt " + id + ": Fehler: " + e.getMessage());
}
}
System.out.println("offene Verbindungen: " + opened + ", frei: " + free.size());

Sieh es dir an

Eine verlorene Verbindung

Die Funktion für den Namen eines Produkts hat zwei Ausgänge. Wenn das Produkt existiert, gibt sie die Verbindung zurück. Wenn nicht, kehrt sie vorzeitig zurück, und die Verbindung bleibt belegt:

static String productName(int id) throws SQLException {
Connection conn = borrow();
try (PreparedStatement ps = conn.prepareStatement("SELECT name FROM products WHERE id = ?")) {
ps.setInt(1, id);
try (ResultSet rs = ps.executeQuery()) {
if (!rs.next()) return "kein solches Produkt"; // die Verbindung wird nie zurückgegeben
String name = rs.getString("name");
giveBack(conn);
return name;
}
}
}
Ausgabe
Produkt 1: Weißbrot
Produkt 99: kein solches Produkt
Produkt 5: Frischmilch
Produkt 98: kein solches Produkt
Produkt 97: kein solches Produkt
Produkt 7: Fehler: der Pool ist leer
offene Verbindungen: 3, frei: 0

Drei Abfragen nach einem Produkt, das es nicht gibt, und der Pool ist leer. Die sechste Anfrage ist völlig normal, bekommt aber keine Verbindung. Ein echter Pool scheitert nicht sofort: Er wartet auf eine freie Verbindung (bis zu 30 Sekunden in HikariCP), und die Seite hängt. Deshalb zeigt sich ein Leck erst als „die Seite ist langsam“ und dann als „die Seite antwortet nicht“.

Die Idee

Das Zurückgeben gehört in finally

Die Lösung: Die Verbindung wird zurückgegeben, egal wie die Funktion verlassen wird:

static String productName(int id) throws SQLException {
Connection conn = borrow();
try (PreparedStatement ps = conn.prepareStatement("SELECT name FROM products WHERE id = ?")) {
ps.setInt(1, id);
try (ResultSet rs = ps.executeQuery()) {
return rs.next() ? rs.getString("name") : "kein solches Produkt";
}
} finally {
giveBack(conn);
}
}
Ausgabe
Produkt 1: Weißbrot
Produkt 99: kein solches Produkt
Produkt 5: Frischmilch
Produkt 98: kein solches Produkt
Produkt 97: kein solches Produkt
Produkt 7: Weißkäse
offene Verbindungen: 1, frei: 1

Sechs Anfragen mit einer Verbindung: Jede gibt sie zurück, und die nächste nimmt sie. Mit den echten Bibliotheken schreibst du keinen Pool selbst. Du nimmst eine Verbindung aus dem Pool und schließt sie wie in der ersten Lektion des Moduls, und das Schließen gibt sie an den Pool zurück, statt sie wirklich zu schließen:

HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://localhost:5432/shop");
config.setUsername("shop_app");
config.setPassword(System.getenv("SHOP_DB_PASSWORD"));
config.setMaximumPoolSize(10);
HikariDataSource pool = new HikariDataSource(config); // einmal, beim Start
try (Connection conn = pool.getConnection()) { // close() gibt sie an den Pool zurück
// die Abfragen
}
Genauer, für Neugierige

Diese Beispiele werden von der Prüfung der Seite nicht ausgeführt, weil sie PostgreSQL und die Pool-Pakete brauchen. Die Standardgrößen: HikariCP 10, Npgsql 100, SQLAlchemy 5 (und bei Bedarf 10 mehr), pg 10.

Ein größerer Pool bedeutet keine schnellere Seite. Die Datenbank hat so viele Kerne, wie sie hat, und 200 Abfragen gleichzeitig warten nur aufeinander. Der Leitfaden von HikariCP rät, mit etwa der doppelten Kernzahl des Datenbankservers anzufangen. Läuft die Seite auf fünf Servern mit je 20 Verbindungen, sieht die Datenbank 100. Wenn es zu viele Verbindungen sind, kommt ein eigener Pool wie PgBouncer vor PostgreSQL.

Die Idee

Ein Backup

Eines Tages wird die Datenbank beschädigt: eine Festplatte, eine falsche Abfrage, eine Migration mit einem Fehler. Eine Kopie ist der einzige Weg zurück. In SQLite schreibt VACUUM INTO die ganze Datenbank im laufenden Betrieb in eine neue Datei, und ATTACH öffnet diese Datei als zweite Datenbank auf derselben Verbindung:

SQLDatenbank: Shop

Hier siehst du, wozu die Kopie gut ist. Ein Skript löscht aus Versehen die Bewertungen des Kaffees, und die Kopie von vor einer Minute bringt sie zurück:

SQLDatenbank: Shop

Führ das Feld ein zweites Mal aus, und VACUUM INTO bleibt stehen, weil die Datei schon existiert. Im echten Leben ist es genauso: Jede Kopie ist eine neue Datei mit Datum und Uhrzeit im Namen.

Genauer, für Neugierige

In PostgreSQL macht pg_dump eine Kopie einer Datenbank, und pg_basebackup mit einem WAL-Archiv erlaubt die Wiederherstellung auf einen genauen Zeitpunkt („auf 14:14, vor dem Skript“). In MySQL ist es mysqldump. Eine verbreitete Regel ist 3-2-1: drei Kopien, auf zwei Arten von Medien, eine davon an einem anderen Ort.

Probier es aus

Prüfen nach der Wiederherstellung

Eine Kopie, aus der du nie wiederhergestellt hast, ist nur eine Hoffnung. Nach jeder Wiederherstellung (zumindest in eine Testdatenbank) prüfst du, dass die Datenbank vollständig ist:

SQLDatenbank: Shop

und wie viele Zeilen jede Tabelle hat, um sie mit den Zahlen vor der Kopie zu vergleichen.

Zeig für jede der sechs Tabellen des Shops ihren Namen (Spalte table_name) und ihre Zeilenzahl (Spalte row_count).

SQLDatenbank: Shop

Übung

Hol zurück, was gelöscht wurde, ohne zu verlieren, was neu ist

Die Kopie von gestern Nacht ist in die Tabelle reviews_2026_10_24 wiederhergestellt. Heute Morgen hat ein Skript Bewertungen aus reviews gelöscht, und danach hat ein Kunde eine neue Bewertung geschrieben. Hol die gelöschten Bewertungen aus der Kopie zurück, ohne die neue zu verlieren.

SQLDatenbank: Shop

Die Idee

Nur so viele Rechte, wie nötig

SQLite hat keine Benutzer: Wer die Datei öffnen kann, kann alles. Server-Datenbanken haben sie, und die Seite meldet sich mit einem eigenen Benutzer an, der nur kann, was er braucht:

CREATE ROLE shop_app LOGIN PASSWORD '…';
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO shop_app;

Kein DROP, kein ALTER und kein Zugriff auf die anderen Datenbanken auf dem Server. Wenn jemand in die Seite einbricht, kann er so viel wie shop_app. Die Migrationen laufen mit einem anderen Benutzer, dem die Tabellen gehören, und beide Passwörter bleiben aus dem Code, wie in der ersten Lektion des Moduls.

Test

Teste dich

1. Die Seite öffnet für jede Seitenansicht eine neue PostgreSQL-Verbindung. Was passiert beim Ansturm?
2. Der Pool hat 10 Verbindungen. Eine Funktion kehrt vorzeitig zurück, bevor sie ihre Verbindung zurückgibt. Was passiert?
3. Woran erkennst du, dass ein Backup funktioniert?
4. Welche Rechte braucht der Benutzer, mit dem sich die Seite anmeldet?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·