Die Hintergrundsynchronisierung hat neue Datensätze bereits per Commit gespeichert, doch nach dem Aktualisieren zeigt die Liste weiterhin alte Werte. Nach einem Neuaufbau des Caches verschwindet das Problem vorübergehend, tritt aber beim nächsten gebündelten Lesezugriff erneut auf. Prüfen Sie in diesem Fall zuerst, ob die Lesezugriffe noch innerhalb derselben SQLite-Transaktion stattfinden. Mit zwei unabhängigen Verbindungen auf einem Cloud-Mac lässt sich leichter zwischen Datenbanksemantik und einem UI-Cache-Problem unterscheiden als durch wiederholte Aktionen auf dem Remote-Desktop.
Zuerst den erforderlichen Lesevertrag festlegen
Dieser Artikel behandelt ausschließlich den WAL-Modus einer lokalen, dateibasierten Datenbank. Die Ergebnisse lassen sich nicht auf In-Memory-Datenbanken oder Konfigurationen mit gemeinsamem Cache übertragen. Sie benötigen Python 3 mit einem funktionsfähigen SQLite-Modul. Führen Sie zunächst python3 -c 'import sqlite3; print(sqlite3.sqlite_version)' aus und speichern Sie die Ausgabe zusammen mit dem Testprotokoll.
Als Abnahmekriterium gilt: Nach der ersten Abfrage der Leseverbindung kann die Schreibverbindung einen Commit ausführen. Die bestehende Lesetransaktion sieht weiterhin den alten Wert. Erst nachdem diese Transaktion beendet wurde, liefert eine erneute Abfrage den neuen Wert. „Alt“ bedeutet hier Transaktionskonsistenz, nicht Datenverlust.
Beenden Sie eine Transaktion nicht nach Belieben per Commit, nur damit eine Liste „aktueller“ wirkt, wenn mehrere Tabellen innerhalb einer konsistenten Sicht gelesen werden müssen. Klären Sie zuerst, ob die Anwendung denselben Snapshot benötigt oder ob jede Abfrage den jeweils neuesten bestätigten Stand lesen soll.
Der Test muss eine echte temporäre Datei und zwei Verbindungen verwenden. Zwei gewöhnliche :memory:-Verbindungen haben jeweils ihre eigene Datenbank und können dieses Zusammenspiel nicht überprüfen.
Ein Minimalexperiment mit zwei Verbindungen aufbauen
Speichern Sie den folgenden Inhalt als snapshot_check.py und führen Sie python3 snapshot_check.py aus. Das Skript greift nicht auf die Anwendungsdatenbank zu; das temporäre Verzeichnis wird beim Verlassen automatisch gelöscht. Alle Cursor werden sofort vollständig ausgelesen und geschlossen, damit eine noch nicht abgeschlossene Anweisung nicht fälschlich als Problem einer expliziten Transaktion interpretiert wird.
import sqlite3
import tempfile
from pathlib import Path
def one(db, sql):
cursor = db.execute(sql)
try:
return cursor.fetchone()
finally:
cursor.close()
with tempfile.TemporaryDirectory() as root:
path = Path(root) / "snapshot.sqlite"
writer = sqlite3.connect(path, isolation_level=None)
reader = sqlite3.connect(path, isolation_level=None)
try:
assert one(writer, "PRAGMA journal_mode=WAL")[0] == "wal"
writer.execute("PRAGMA wal_autocheckpoint=0")
writer.execute("CREATE TABLE item(id INTEGER PRIMARY KEY, value TEXT)")
writer.execute("INSERT INTO item VALUES(1, 'old')")
one(writer, "PRAGMA wal_checkpoint(TRUNCATE)")
reader.execute("BEGIN")
assert one(reader, "SELECT value FROM item WHERE id=1")[0] == "old"
writer.execute("BEGIN IMMEDIATE")
writer.execute("UPDATE item SET value='new' WHERE id=1")
writer.execute("COMMIT")
assert one(reader, "SELECT value FROM item WHERE id=1")[0] == "old"
held = one(writer, "PRAGMA wal_checkpoint(PASSIVE)")
assert held[1] > held[2], held
reader.execute("COMMIT")
assert one(reader, "SELECT value FROM item WHERE id=1")[0] == "new"
released = one(writer, "PRAGMA wal_checkpoint(PASSIVE)")
assert released[1] == released[2], released
print({"held": held, "released": released, "result": "passed"})
finally:
if reader.in_transaction:
reader.rollback()
if writer.in_transaction:
writer.rollback()
reader.close()
writer.close()
Der Code deaktiviert automatische Checkpoints ausdrücklich, damit sich der Ablauf beobachten lässt. Das ist keine Empfehlung für den Produktivbetrieb. Auch das Kürzen der WAL-Datei nach der Initialisierung gilt nur für diese exklusiv verwendete Test-Fixture. Übernehmen Sie diesen Schritt nicht für eine Datenbank, die gerade laufende Aufgaben verarbeitet.
BEGIN und den ersten Lesezugriff auseinanderhalten
Ein gewöhnliches BEGIN startet eine verzögerte Transaktion. Der Ausführungszeitpunkt dieser Anweisung ist daher nicht automatisch der Zeitpunkt des Snapshots. Im obigen Beispiel wird die Lesesicht erst durch die erste Abfrage festgelegt.
Probieren Sie eine separate Variante aus: Verschieben Sie den Schreibvorgang samt Commit vor die erste Abfrage der Leseverbindung. Dann müssen sowohl die erste als auch alle weiteren Abfragen innerhalb derselben Transaktion new liefern. Passen Sie die Assertions entsprechend an.
Damit lässt sich ein häufiger Fehlschluss aufdecken: Im Protokoll steht „Transaktion gestartet“, und daraus wird abgeleitet, dass sich alle folgenden Lesezugriffe auf genau diesen Zeitpunkt beziehen. Protokollieren Sie zur Diagnose mindestens die Verbindungs-ID, die Transaktions-ID sowie die Stellen der ersten Abfrage und des Transaktionsendes — nicht nur den Eintritt in eine Methode.
Müssen mehrere Abfragen eine konsistente Sicht liefern?
Wenn Sie beispielsweise zunächst die Anzahl der Bestellungen und anschließend deren Details abfragen und beides zum selben fachlichen Snapshot gehören soll, müssen beide Abfragen in einer kurzen Lesetransaktion liegen. Soll dagegen jeder Nachladevorgang beim Scrollen die neuesten Daten liefern, darf nicht die gesamte Lebensdauer der Seite von derselben Transaktion umschlossen werden. Die Abnahmekriterien ergeben sich aus den fachlichen Grenzen, nicht daraus, welche Variante scheinbar schneller aktuelle Werte anzeigt.
Den Checkpoint-Fortschritt prüfen, nicht nur die Dateigröße
wal_checkpoint(PASSIVE) gibt drei Ganzzahlen zurück: den Busy-Status, die Anzahl der Frames im Log und die Anzahl der bereits per Checkpoint übertragenen Frames. Die alte Lesetransaktion in der Test-Fixture benötigt weiterhin die frühere Sicht. Deshalb lassen sich einige neue Frames noch nicht per Checkpoint übertragen. Nach dem Beenden der Lesetransaktion kann ein erneuter Aufruf weiter vorankommen.
Schreiben Sie keine feste Assertion dafür, wie viele Frames ein bestimmter Vorgang erzeugt. Seitengröße, Datenbanklayout und Version der Laufzeitbibliothek beeinflussen diese Zahl. Der Test sollte relative Beziehungen prüfen.
Auch wenn die Anzahl der per Checkpoint übertragenen Frames die Anzahl der Log-Frames erreicht, muss die WAL-Datei nicht sofort kleiner werden. Ihr Speicherplatz kann für spätere Schreibvorgänge erhalten bleiben. Erfassen Sie im Produktivbetrieb die Dauer von Lesetransaktionen, Checkpoint-Ergebnisse und die Entwicklung der Schreiblast gemeinsam. Löschen Sie das Log nicht allein deshalb, weil im Verzeichnis eine große Datei liegt.
Falls die zweite Assertion fehlschlägt, suchen Sie zunächst nach noch offenen Lesetransaktionen, Cursorn oder weiteren Verbindungen, bevor Sie die Checkpoint-Strategie untersuchen. Erzwingen Sie nicht als Erstes das Kürzen der WAL-Datei in einer Schleife: Damit würden Sie die eigentliche Ursache, eine langlebige Lesetransaktion, nur verdecken.
Die Test-Fixture in die Tests der iOS-Datenschicht übernehmen
Das Experiment auf dem Host prüft die SQLite-Version, mit der das dortige Python verknüpft ist. Nach der Übernahme in iOS müssen Sie den Test auf dem Zielsimulator oder Zielgerät über die tatsächlich eingesetzte Datenzugriffsschicht erneut ausführen und für diese Umgebung sqlite3_libversion() protokollieren. Das Ergebnis auf dem Host ist kein Nachweis für sämtliche Laufzeitumgebungen.
Behalten Sie zwei tatsächlich unabhängige zugrunde liegende Verbindungen bei. Steuern Sie die drei Phasen „ersten Lesezugriff abgeschlossen“, „Schreibvorgang per Commit bestätigt“ und „erneut lesen“ mit expliziten Synchronisationsbarrieren im Test, nicht mit Wartezeiten. Wenn der Verbindungspool beide Vorgänge derselben Verbindung zuweist, verliert das Experiment seine Aussagekraft.
Prüfen Sie abschließend vier Punkte: Die alte Transaktion liest den alten Wert, eine neue Transaktion liest den neuen Wert, auch im Fehlerfall werden Transaktionen beendet, und nach dem Freigeben langlebiger Lesetransaktionen können Checkpoints weiter vorankommen. Bei asynchronen Abfragen muss außerdem sichergestellt sein, dass Cursor und Transaktionen auch nach einem Abbruch ordnungsgemäß geschlossen beziehungsweise beendet sind. Belassen Sie diese Assertions in den Regressionstests. Wenn eine Liste das nächste Mal alte Daten anzeigt, können Sie so zuerst klären, ob das ein korrekter Snapshot ist oder ob die Transaktion ihre vorgesehene Lebensdauer überschritten hat.
Häufig gestellte Fragen
Legt BEGIN den Lesesnapshot bereits fest?
Bei einer verzögerten Transaktion noch nicht. Im gezeigten WAL-Test entsteht der Snapshot erst beim ersten tatsächlichen Lesezugriff.
Muss ein bestehender Leser neue Commits sofort sehen?
Nein. Er liest weiter seinen bisherigen Snapshot. Erst nach dem Transaktionsende sieht ein neuer Lesezugriff die bestätigten Änderungen.
Wird die WAL-Datei nach einem Checkpoint immer kleiner?
Nein. Das Zurückschreiben von Seiten und das Kürzen der Datei sind getrennte Vorgänge. Die Dateigröße allein reicht zur Bewertung nicht aus.
Aufgaben auf einem exklusiven Mac in der Cloud erledigen
Mieten Sie einen exklusiven physischen Mac mini in der Cloud und wählen Sie Standort und Laufzeit passend zu Ihrem Arbeitsalltag. Nach der Bereitstellung finden Sie die Verbindungsdaten in der Konsole. Bei Fragen können Sie dort ein Supportticket erstellen.
Cloud-Mac-Tarife ansehen