Nahtlose Datenbankevolutionen: Schemaänderungen ohne Ausfallzeiten realisieren
Takashi Yamamoto
Infrastructure Engineer · Leapcell

Einleitung
In der schnelllebigen Welt der Softwareentwicklung sind Continuous Integration- und Continuous Delivery (CI/CD)-Pipelines zur Norm geworden. Anwendungen entwickeln sich ständig weiter, und mit ihnen ihre zugrunde liegenden Datenmodelle. Änderungen am Datenbankschema, wie z.B. das Hinzufügen einer neuen Spalte, das Ändern eines Datentyps oder sogar das Umbenennen einer Tabelle, erforderten traditionell die Offline-Schaltung der Anwendung, was zu gefürchteten Ausfallzeiten führte. Für Unternehmen, die rund um die Uhr (24/7) tätig sind, können solche Störungen erhebliche finanzielle Verluste und Reputationsschäden bedeuten. Das Streben nach "Zero-Downtime"-Datenbankschemaänderungen ist kein Luxus mehr, sondern eine grundlegende Voraussetzung für die Aufrechterhaltung hoher Verfügbarkeit und eine reibungslose Benutzererfahrung. Dieser Artikel untersucht die Kernkonzepte und praktischen Strategien, um dieses kritische Ziel zu erreichen und Ihren Anwendungen zu ermöglichen, sich ohne Verzögerung weiterzuentwickeln.
Kernkonzepte für unterbrechungsfreie Schemaevolution
Bevor wir uns mit den Details befassen, ist es wichtig, einige Kernkonzepte zu verstehen, die Zero-Downtime-Schemaänderungen zugrunde liegen.
Abwärtskompatibilität: Dies ist der Eckpfeiler. Jede Änderung am Datenbankschema darf bestehende Anwendungen, die noch auf der alten Schemaversion laufen, nicht beeinträchtigen. Dies bedeutet in der Regel, dass Anwendungen, die das alte Schema erwarten, Daten weiterhin korrekt lesen und schreiben können müssen, auch wenn sich das Schema teilweise weiterentwickelt hat.
Vorwärtskompatibilität: Dieses Konzept stellt sicher, dass Anwendungen, die auf der neuen Schemaversion laufen, immer noch mit Daten interagieren (lesen/schreiben) können, die möglicherweise von Anwendungen geschrieben wurden, die noch dem alten Schema entsprechen. Dies ist während Übergangsphasen entscheidend, in denen sowohl alte als auch neue Anwendungsversionen aktiv sein können.
Atomare Operationen: Obwohl für komplexe Schemaänderungen nicht immer erreichbar, ermutigt das Prinzip dazu, große Änderungen in kleinere, unabhängige und umkehrbare atomare Operationen zu zerlegen. Dies minimiert das Risikoprofil jedes Schritts.
Migrationstools: Spezialisierte Tools wie Flyway, Liquibase oder auch benutzerdefinierte Skripte sind unverzichtbar für die Verwaltung und Anwendung von Schemaänderungen in einer kontrollierten und versionierten Weise. Sie stellen sicher, dass Änderungen in allen Umgebungen konsistent angewendet werden.
Dual-Write und Read-Replikation: Diese Muster sind bei komplexen Datenmigrationen oder strukturellen Änderungen von entscheidender Bedeutung. Dual-Write beinhaltet das gleichzeitige Schreiben von Daten sowohl an alte als auch an neue Schemapositionen. Read-Replikation kann das Lesen von alten und neuen Positionen basierend auf der Datenverfügbarkeit oder der Anwendungsversion beinhalten.
Strategien und Schritte für Zero-Downtime-Schemaänderungen
Das Erzielen von Zero-Downtime-Schemaänderungen beinhaltet typischerweise einen mehrstufigen Prozess, der sorgfältig orchestriert wird, um während des gesamten Übergangs sowohl die Abwärts- als auch die Vorwärtskompatibilität zu gewährleisten. Lassen Sie uns gängige Strategien mit praktischen Beispielen untersuchen.
1. Hinzufügen einer neuen Spalte (Nicht-Null mit Standardwertempfindlichkeit)
Dies ist eine relativ einfache Änderung, die jedoch die Prinzipien verdeutlicht.
Strategie:
- Fügen Sie zunächst die neue Spalte als nullable hinzu.
- Stellen Sie Anwendungen bereit, die in die neue Spalte schreiben.
- Migrieren Sie bei Bedarf vorhandene Daten.
- Aktualisieren Sie die Spalte auf Nicht-Null.
Beispielszenario: Hinzufügen einer Spalte shipping_address zur bestehenden Tabelle orders.
-- Schritt 1: Fügen Sie die neue Spalte als nullable hinzu. ALTER TABLE orders ADD COLUMN shipping_address VARCHAR(255) NULL;
- Erklärung: Zu diesem Zeitpunkt funktionieren bestehende Anwendungen weiterhin normal. Neue Instanzen der Anwendung (wenn bereitgestellt) können mit dem Schreiben in
shipping_addressbeginnen. Wichtig ist, dass eine alte Anwendung, dieshipping_addressnicht kennt, sie einfach ignoriert und somit die Abwärtskompatibilität gewahrt bleibt.
// Beispiel Anwendungs-Code (neue Version) public void createOrder(Order order) { // ... andere Felder preparedStatement.setString(4, order.getShippingAddress()); // Schreibt in die neue Spalte // ... } // Beispiel Anwendungs-Code (alte Version) public void createOrder(Order order) { // ... andere Felder // Keine Änderung, alte App interagiert nicht mit shipping_address // ... }
-- Schritt 2: Stellen Sie die neue Anwendungsversion bereit, die in shipping_address schreiben kann. -- (Dies wird von Ihrer CI/CD-Pipeline gehandhabt, nicht von SQL)
-- Schritt 3 (Optional, aber üblich): Vorhandene Daten nachträglich füllen. -- Dies kann einen Batch-Job oder ein einmaliges Skript für bestehende Bestellungen beinhalten UPDATE orders SET shipping_address = (SELECT address FROM users WHERE users.id = orders.user_id) WHERE shipping_address IS NULL;
- Erklärung: Dieser Schritt stellt sicher, dass auch ältere Bestellungen eine
shipping_addresshaben. Dies sollte sorgfältig geschehen, möglicherweise in Batches, um die Datenbank nicht zu überlasten.
-- Schritt 4: Machen Sie die Spalte nicht-nullable. ALTER TABLE orders ALTER COLUMN shipping_address VARCHAR(255) NOT NULL;
- Erklärung: Jetzt, da alle alten Anwendungen aktualisiert wurden und vorhandene Daten nachträglich gefüllt wurden (oder neue Daten immer geschrieben werden), können wir die Nicht-Null-Beschränkung erzwingen. Jeder Versuch, einen Nullwert in diese Spalte einzufügen, wird nun fehlschlagen und zukünftige Schreibvorgänge auf Datenintegrität gewährleisten.
2. Umbenennen einer Spalte oder Tabelle
Das Umbenennen ist komplexer, da es direkt beeinflusst, wie Anwendungen auf Daten verweisen.
Strategie (Blue/Green-Deployment-Ansatz):
- Erstellen Sie eine neue Spalte/Tabelle mit dem gewünschten Namen.
- Implementieren Sie Dual-Writes in der Anwendung: Schreiben Sie sowohl an alte als auch an neue Positionen.
- Füllen Sie Daten von der alten zur neuen Spalte/Tabelle nachträglich.
- Aktualisieren Sie die Anwendung, um von der neuen Spalte/Tabelle zu lesen.
- Stoppen Sie die Dual-Writes.
- Löschen Sie die alte Spalte/Tabelle.
Beispielszenario: Umbenennen von product_code in sku in der Tabelle products.
-- Schritt 1: Fügen Sie die neue Spalte 'sku' als nullable hinzu. ALTER TABLE products ADD COLUMN sku VARCHAR(50) NULL;
// Schritt 2: Stellen Sie die Anwendung mit Dual-Write-Logik bereit (neue Version) // Alte App schreibt weiterhin in 'product_code' public void updateProduct(Product product) { // ... andere Felder // Schreibe in alte Spalte (product_code) preparedStatement.setString(2, product.getProductCode()); // Schreibe in neue Spalte (sku) preparedStatement.setString(3, product.getSku()); // Angenommen, getSku() gibt zunächst den aktuellen product_code zurück // ... }
- Erklärung: Die Anwendung schreibt nun dieselben Daten in beide Spalten. Dies stellt sicher, dass sowohl die alte als auch die neue Schemaversion über aktuelle Daten verfügen.
-- Schritt 3: Füllen Sie Daten von 'product_code' zu 'sku' nachträglich. UPDATE products SET sku = product_code WHERE sku IS NULL;
- Erklärung: Dies migriert alle vorhandenen Daten. Dies kann eine umfangreiche Operation sein, daher sollten Sie das Batching in Betracht ziehen.
// Schritt 4: Stellen Sie die Anwendung zum Lesen aus 'sku' bereit (neue Version) // Lesepriorität: Versuche 'sku', falle auf 'product_code' zurück, um die Sicherheit während des Übergangs zu gewährleisten public Product getProductById(long id) { // ... Abfrage String sku = resultSet.getString("sku"); if (sku == null) { sku = resultSet.getString("product_code"); // Fallback für alte Daten während des Übergangs } // ... product.setSku(sku); // Anwendung erwartet nun 'sku' // ... return product; }
- Erklärung: Die Anwendung bevorzugt nun das Lesen von
sku. Der Fallback stellt sicher, dass während des Übergangs, falls eine ältere Anwendung irgendwie direkt inproduct_codegeschrieben hat (z.B. ein noch nicht aktualisierter Batch-Job), die neue Anwendung ihn immer noch lesen kann.
-- Schritt 5: Stoppen Sie Dual-Writes (Stellen Sie die Anwendung bereit, die nur in 'sku' schreibt). -- (Entfernen Sie die 'product_code'-Schreiboperation aus dem Anwendungscode)
-- Schritt 6: Löschen Sie die alte Spalte 'product_code'. ALTER TABLE products DROP COLUMN product_code;
- Erklärung: Sobald die Zuversicht hoch ist, dass alle Anwendungen
skuverwenden und die Daten konsistent sind, kann die alte Spalte entfernt werden.
3. Aufteilen einer Tabelle oder Denormalisierung
Komplexe Operationen wie das Aufteilen einer Tabelle oder die Denormalisierung von Daten folgen einem ähnlichen Prinzip, jedoch mit komplexerer Datenmigration.
Strategie:
- Erstellen Sie die neue(n) Tabelle(n).
- Implementieren Sie Dual-Writes in der Anwendung, um die neue(n) Tabelle(n) zu befüllen.
- Füllen Sie vorhandene Daten in die neue(n) Tabelle(n) nachträglich.
- Aktualisieren Sie die Leseoperationen der Anwendung, um die neue(n) Tabelle(n) zu verwenden.
- Stoppen Sie Dual-Writes und entfernen Sie Leseoperationen aus den alten Tabellen.
- Löschen Sie die alten Tabellen oder entfernen Sie alte Spalten bei Denormalisierung.
Diese Strategie betont einen schrittweisen Übergang, der es der Anwendungsschicht ermöglicht, sich an Änderungen im Datenbankschema anzupassen, ohne jemals offline genommen zu werden. Jeder Schritt gewährleistet entweder die Abwärts- oder Vorwärtskompatibilität und minimiert somit das Risiko.
Schlussfolgerung
Das Erreichen von Zero-Downtime-Datenbankschemaänderungen ist eine anspruchsvolle, aber unverzichtbare Praxis für moderne, hochverfügbare Systeme. Durch die Übernahme der Prinzipien der Abwärts- und Vorwärtskompatibilität, die Zerlegung von Änderungen in atomare Schritte und die Nutzung von Mustern wie Dual-Writes und sorgfältigen Deployment-Strategien können Unternehmen ihre Datenmodelle nahtlos weiterentwickeln. Dieser iterative Ansatz zur Schemaevolution gewährleistet eine kontinuierliche Servicebereitstellung und verwandelt potenzielle Ausfallzeiten in einen unsichtbaren, aber entscheidenden technischen Triumph.

