Wenn zwei App-Versionen eine Datenbank teilen: Der Dual-Write- und Dual-Read-Übergang

Stellen Sie sich vor: Ihr Team hat gerade eine neue Spalte zu einer Produktionsdatenbanktabelle hinzugefügt. Das Schema-Update verlief reibungslos. Jetzt müssen Sie die neue Anwendungsversion bereitstellen, die in diese Spalte schreibt. Aber die alte Anwendungsversion läuft noch, bearbeitet Anfragen und weiß nichts von dieser neuen Spalte.

Wenn die neue App nur in die neue Spalte schreibt, sieht die alte App diese Daten nicht. Benutzer, deren Anfragen bei der alten App landen, erhalten unvollständige oder inkonsistente Ergebnisse. Sie können die alte App nicht sofort stoppen. Sie können nicht alles auf einmal auf die neue Version umstellen. Sie benötigen eine Übergangsphase, in der beide Versionen koexistieren und dieselbe Datenbank nutzen.

Hier kommen Dual-Write- und Dual-Read-Muster ins Spiel. Sie sind nicht elegant. Sie sind nicht einfach. Aber sie sind der praktische Weg, um Datenstrukturen in einem Live-System ohne Ausfallzeiten zu migrieren.

Das Kernproblem: Zwei Versionen, eine Datenbank

Wenn Sie ein Schema durch Hinzufügen einer neuen Spalte oder Tabelle erweitern, kann die Datenbank sowohl alte als auch neue Strukturen aufnehmen. Aber die Anwendungen, die diese Daten lesen und schreiben, sind nicht so flexibel. Die alte Anwendungsversion versteht nur die alte Struktur. Die neue Anwendungsversion versteht beide, muss aber die alte Version funktionsfähig halten, bis alle Instanzen aktualisiert wurden.

Der naive Ansatz wäre, die neue App nur in die neue Spalte schreiben zu lassen. Das würde die alte App sofort lahmlegen. Die alte App liest aus der alten Spalte, findet nichts und schlägt fehl. Der richtige Ansatz ist, die neue App in beide Stellen schreiben zu lassen, bis die alte App verschwunden ist.

Das folgende Sequenzdiagramm veranschaulicht den Ablauf von Schreib- und Lesevorgängen während der Übergangsphase:

sequenceDiagram participant OldApp as Alte App participant NewApp as Neue App participant DB as Datenbank Note over OldApp,DB: Dual-Write-Phase NewApp->>DB: Schreiben in alte Spalte NewApp->>DB: Schreiben in neue Spalte OldApp->>DB: Lesen aus alter Spalte DB-->>OldApp: Daten aus alter Spalte NewApp->>DB: Lesen aus alter Spalte (bevorzugt alt) DB-->>NewApp: Daten aus alter Spalte Note over OldApp,DB: Umschaltpunkt NewApp->>DB: Lesen aus neuer Spalte (Umschaltung) DB-->>NewApp: Daten aus neuer Spalte Note over OldApp,DB: Nach Umschaltung NewApp->>DB: Nur in neue Spalte schreiben NewApp->>DB: Aus neuer Spalte lesen

Dual-Write: Gleichzeitig an zwei Stellen schreiben

Dual-Write bedeutet, dass die neue Anwendungsversion bei jedem Schreibvorgang Daten sowohl in die alte als auch in die neue Struktur schreibt. Wenn ein Benutzer einen Datensatz erstellt, füllt die neue App die alte Spalte wie gewohnt aus und schreibt dann dieselben Daten auch in die neue Spalte.

Das klingt einfach, aber zwei Details sind sehr wichtig.

Hier ist ein JavaScript-Beispiel, das Dual-Write und Dual-Read für ein Benutzerprofil-Update implementiert:

async function updateUserProfile(userId, name, email) {
  // Dual-Write: zuerst in alte Spalte schreiben, dann in neue
  await db.query(
    'UPDATE users SET name = ?, email = ? WHERE id = ?',
    [name, email, userId]
  );
  await db.query(
    'UPDATE users SET profile_data = ? WHERE id = ?',
    [JSON.stringify({ name, email }), userId]
  );
}

async function getUserProfile(userId) {
  // Dual-Read: neue Spalte bevorzugen, auf alte zurückfallen
  const row = await db.query(
    'SELECT profile_data, name, email FROM users WHERE id = ?',
    [userId]
  );
  if (row.profile_data) {
    return JSON.parse(row.profile_data);
  }
  return { name: row.name, email: row.email };
}

Erstens muss die Schreibreihenfolge konsistent sein. Schreiben Sie zuerst in die alte Spalte, dann in die neue. Wenn der Prozess nach dem Schreiben in die alte, aber vor dem Schreiben in die neue Spalte fehlschlägt, kann die alte App die Daten noch lesen. Die neue Spalte wird diesen Datensatz vermissen, aber das können Sie später mit einem Backfill beheben. Wenn Sie zuerst in die neue Spalte geschrieben hätten und der Prozess fehlgeschlagen wäre, würde die alte App sofort unvollständige Daten sehen. Das wäre ein Produktionsvorfall, der nur darauf wartet, passieren.

Zweitens müssen die Werte identisch sein. Die in die alte und die neue Spalte geschriebenen Daten müssen dieselbe Information repräsentieren. Wenn es eine Transformation oder logische Abweichung zwischen den beiden Schreibvorgängen gibt, entsteht eine Dateninkonsistenz, die später nur sehr schwer zu debuggen ist. Halten Sie die Schreiblogik identisch. Die neue Spalte speichert die Daten möglicherweise in einem anderen Format oder einer anderen Struktur, aber die Bedeutung muss dieselbe sein.

Dual-Read: Von zwei Stellen lesen, die alte bevorzugen

Sobald Dual-Write läuft, kann die neue App Daten schreiben, die beide Versionen lesen können. Aber wie sieht es mit dem Lesen aus? Die neue App könnte sofort aus der neuen Spalte lesen, aber das schafft ein Problem. Die alte App schreibt weiterhin nur in die alte Spalte. Wenn die neue App nur aus der neuen Spalte liest, übersieht sie Daten, die von der alten App geschrieben wurden.

Die Lösung ist Dual-Read. Die neue App liest von beiden Stellen, priorisiert aber während des Übergangs die alte Spalte. Dadurch wird sichergestellt, dass von der alten App geschriebene Daten für die neue App immer sichtbar sind. Im Laufe der Zeit, wenn Sie verifizieren, dass die Daten korrekt in die neue Spalte fließen, können Sie die Lesevorgänge schrittweise auf die neue Spalte umstellen.

Diese schrittweise Umstellung ist der Punkt, an dem Feature Flags nützlich werden. Sie können die neue App so konfigurieren, dass sie für einen kleinen Prozentsatz der Anfragen aus der neuen Spalte liest. Wenn nichts kaputt geht, erhöhen Sie den Prozentsatz. Wenn ein Fehler auftritt, schalten Sie das Flag zurück und alle Lesevorgänge gehen wieder zur alten Spalte. Keine erneute Bereitstellung erforderlich.

Was ist mit Daten, die von der alten App geschrieben wurden?

Während dieses Übergangs läuft die alte App noch und schreibt nur in die alte Spalte. Die neue App muss damit umgehen können. Wenn die neue App einen Datensatz liest, der von der alten App geschrieben wurde, findet sie Daten nur in der alten Spalte. Die neue App sollte in der Lage sein, diese Daten aus der alten Spalte zu lesen, zu verwenden und optional im Rahmen eines Hintergrundprozesses in die neue Spalte zu kopieren.

Dies ist nicht dasselbe wie Dual-Write. Dies ist ein Backfill-Prozess, der separat läuft und die neue Spalte mit Daten füllt, die geschrieben wurden, bevor die neue App begann, in beide Stellen zu schreiben. Backfill ist ein Batch-Vorgang, der ausgeführt wird, nachdem die Dual-Write- und Dual-Read-Muster stabil sind.

Die eigentliche Komplexität: Koordination des Übergangs

Der schwierigste Teil dieser Phase ist nicht der Code. Es ist die Koordination. Sie müssen wissen, welche Instanzen welche Version ausführen. Sie müssen wissen, wann die letzte alte Instanz außer Betrieb genommen wurde. Sie müssen auf Dateninkonsistenzen zwischen der alten und der neuen Spalte überwachen.

Während Dual-Write wird jeder Schreibvorgang zu zwei Schreibvorgängen. Das bedeutet mehr Datenbanklast, mehr Transaktionszeit und mehr potenzielle Fehlerquellen. Überwachen Sie Ihre Datenbankmetriken während dieser Phase. Wenn die Schreiblatenz steigt, müssen Sie die Schreibvorgänge möglicherweise bündeln oder asynchrone Replikation verwenden.

Während Dual-Read muss jeder Lesevorgang möglicherweise zwei Speicherorte überprüfen. Das erhöht die Komplexität Ihrer Abfragelogik und kann Lesepfade verlangsamen. Verwenden Sie Caching mit Bedacht. Cachen Sie keine Daten aus der alten Spalte, wenn die neue Spalte zur Quelle der Wahrheit werden soll.

Praktische Checkliste für den Übergang

  • Stellen Sie sicher, dass die Schemaerweiterung (neue Spalte oder Tabelle) bereitgestellt und verifiziert ist.
  • Stellen Sie die neue App-Version mit Dual-Write-Logik bereit: zuerst in alte Spalte schreiben, dann in neue.
  • Verifizieren Sie, dass von der neuen App geschriebene Daten für die alte App sichtbar sind.
  • Aktivieren Sie Dual-Read in der neuen App: standardmäßig aus alter Spalte lesen, aber auf Umschaltung vorbereitet sein.
  • Verwenden Sie ein Feature Flag, um Lesevorgänge schrittweise von der alten auf die neue Spalte umzustellen.
  • Überwachen Sie auf Dateninkonsistenzen zwischen alter und neuer Spalte.
  • Starten Sie den Backfill-Prozess, um alte Daten in die neue Spalte zu kopieren.
  • Entfernen Sie nach Abschluss des Backfills und wenn alle Lesevorgänge die neue Spalte verwenden, die Dual-Write-Logik.
  • Entfernen Sie die alte Spalte oder Tabelle, nachdem bestätigt wurde, dass keine Anwendung mehr daraus liest.

Das Fazit

Dual-Write und Dual-Read sind keine dauerhaften Muster. Sie sind temporäre Brücken, die es Ihnen ermöglichen, Datenstrukturen zu migrieren, während das System für alle Benutzer in Betrieb bleibt. Das Ziel ist es, einen Zustand zu erreichen, in dem nur die neue Struktur relevant ist und die alte Struktur entfernt werden kann. Bis dahin geht jeder Schreibvorgang an zwei Stellen, jeder Lesevorgang prüft zwei Quellen, und Ihr Team bleibt wachsam für Inkonsistenzen. Diese Phase ist unangenehm, aber sie ist der einzige Weg, ein Live-Datenbankschema zu ändern, ohne die Welt anzuhalten.