Wie funktioniert es?
Ihre Anwendung verwendet eine geänderte Version der SQLite-Bibliothek, die den LiteSync-Code enthält, um auf Ihre Datenbank zuzugreifen.
Die Änderungen an der SQLite-Bibliothek sind intern und die Schnittstelle ist dieselbe.
Die LiteSync-Bibliotheken kommunizieren miteinander und tauschen Transaktionsdaten aus.
Replikation
Wenn die App zum ersten Mal geöffnet wird, stellt sie eine Verbindung zu den anderen Knoten her und lädt eine neue Kopie der Datenbank herunter.
In einer zentralisierten Topologie sendet der Primärknoten die Datenbankkopie an die Sekundärknoten.
Nach dem Herunterladen startet der Knoten die Synchronisation.
Synchronisation
Sobald die Knoten dieselbe Basis-Datenbank haben, tauschen sie Transaktionen aus, die ausgeführt wurden, als sie offline waren.
Danach wechseln sie in den Online-Modus und sobald eine neue Transaktion in einem Knoten ausgeführt wurde, wird sie zur Ausführung in den verbundenen Knoten übertragen.
Wenn der Knoten offline ist, wird die Transaktion in einem lokalen Protokoll gespeichert, um später ausgetauscht zu werden.
MUSS ICH MEINEN APP-CODE ÄNDERN?
Es gibt ein paar Schritte grundsätzlich müssen wir jedoch die URI-Zeichenfolge in der sich öffnenden Datenbank ändern:
“file:/path/to/app.db”
zu so etwas:
“file:/path/to/app.db?node=secondary&connect=tcp://server.ip:1234”
Die gute Nachricht ist, dass LiteSync die native SQLite3-Schnittstelle verwendet. Dies bedeutet, dass wir keine andere API verwenden müssen.
Verbindung
Jeder Knoten hat 2 Optionen:
binden an eine Adresse
verbinden an die Peer-Adresse
Sie können also auswählen, welche Seite mit der anderen verbunden werden soll. Dies ist nützlich, wenn sich eine Seite hinter einem Router oder einer Firewall befindet.
Unterstützte Topologien
Zentralisierte Sterntopologie
In dieser Topologie haben wir einen Knoten, mit dem alle anderen Knoten verbunden sind. Daher muss er online sein, damit die Synchronisation stattfinden kann.
Hier einige Beispielkonfigurationen:
Der Primärknoten kann an eine Adresse gebunden werden und die Sekundärknoten stellen eine Verbindung zu dieser Adresse her.
Primärknoten:
"file:/home/user/app.db?node=primary&bind=tcp://0.0.0.0:1234"
Sekundärknoten: (in einem anderen Gerät)
"file:/home/user/app.db?node=secondary&connect=tcp://server:1234"
Der Primärknoten kann auch eine Verbindung zu Sekundärknoten herstellen.
Primärknoten:
"file:/home/user/app.db?node=primary&connect=tcp://address1:port1,tcp://address2:port2"
Sekundärknoten: (jeweils auf einem separaten Gerät)
"file:/home/user/app.db?node=secondary&bind=tcp://0.0.0.0:1234"
Wir können sogar eine Mischung dieser beiden Optionen verwenden.
Primärknoten:
"file:/home/user/app.db?node=primary&bind=tcp://0.0.0.0:1234&connect=tcp://address1:port1"
Sekundärknoten 1:
"file:/home/user/app.db?node=secondary&connect=tcp://server:1234"
Sekundärknoten 2:
"file:/home/user/app.db?node=secondary&bind=tcp://0.0.0.0:1234"
Peer-to-Peer-Topologie
Das vollständig verbundene Peer-to-Peer-Netzwerk wird zwischen Primärknoten hergestellt.
Wir müssen die Gesamtzahl der Knoten im Netzwerk manuell auf jedem Knoten (vorerst) mitteilen.
Die Richtung der Verbindungen muss ebenfalls mitgeteilt werden (welche Knoten werden mit welchen verbunden)
Hier ist ein Beispiel für ein Netzwerk mit 3 Knoten:
Knoten 1:
"file:db1.db?node=primary&total_primary_nodes=3&bind=tcp://0.0.0.0:1201"
Knoten 2:
"file:db2.db?node=primary&total_primary_nodes=3&bind=tcp://0.0.0.0:1202& connect=tcp://127.0.0.1:1201"
Knoten 3:
"file:db3.db?node=primary&total_primary_nodes=3&bind=tcp://0.0.0.0:1203& connect=tcp://127.0.0.1:1201,tcp://127.0.0.1:1202"
Gemischte Topologie
In dieser Topologie sind mehr als ein Primärknoten als Peers und viele Sekundärknoten mit ihnen verbunden.
Die Konfiguration für die Primärknoten ist dieselbe wie oben in der Peer-to-Peer-Topologie.
Jeder sekundäre Knoten wird zu einem bestimmten Zeitpunkt mit einem einzelnen primären Knoten verbunden. Wir können die Adresse vieler Primärknoten mitteilen, damit sie zufällig einen auswählen. Wenn die Verbindung zu einem Primärknoten unterbrochen wird, wird eine Verbindung zu einem anderen Knoten hergestellt.
Hier ist ein Beispiel-URI für einen sekundären Knoten:
"file:db4.db?node=secondary&connect=tcp://127.0.0.1:1201,tcp://127.0.0.1:1202,tcp://127.0.0.1:1203"
Synchronisationsstatus
Wir können den Synchronisationsstatus mit diesem Befehl überprüfen:
PRAGMA sync_status
Es wird eine JSON-Zeichenfolge zurückgegeben.
Synchronisierungsbenachrichtigung
Ihre Anwendung kann benachrichtigt werden, wenn die lokale Datenbank aufgrund der Synchronisierung mit Remote-Knoten aktualisiert wird. Die Benachrichtigung erfolgt über eine benutzerdefinierte Funktion.
Wähle eine Sprache -->
static void on_db_update(sqlite3_context *context, int argc, sqlite3_value **argv){ char* changes = sqlite3_value_text(argv[0]); printf("Update erhalten: %s\n", changes); } sqlite3_create_function(db, "update_notification", 1, SQLITE_UTF8, NULL, &on_db_update, NULL, NULL);
def on_db_update(changes): print("Update erhalten:", changes) con.create_function("update_notification", 1, on_db_update)
// using better-sqlite3: db.on('sync', function(changes) { console.log('Update erhalten: ' + changes); });
Function.create(conn, "update_notification", new Function() { protected void xFunc(changes) { System.out.println("Update erhalten: " + changes); } });
// using SQLite.NET: db.OnSync((changes) => { // the db received an update. update the screen with new data UpdateScreen(db); }); // using Microsoft.Data.SQLite: db.CreateFunction("update_notification", (changes) => { // notification received on the worker thread // do not access the db connection here // transfer the notification to the main thread Console.WriteLine("Update erhalten: " + changes); return 0; });
' Using SQLite.NET: db.OnSync(Function(changes) As Integer ' the db received an update. update the screen with new data UpdateScreen(db) End Function) ' Using Microsoft.Data.SQLite: db.CreateFunction("update_notification", Function(changes) As Integer ' notification received on the worker thread ' do not access the db connection here ' transfer the notification to the main thread Console.WriteLine("Update erhalten: " & changes) End Function)
function on_db_update($changes) { echo 'Update erhalten: $changes'; } // with sqlite3: $db->createFunction('update_notification', 'on_db_update'); // with pdo_sqlite: $db->sqliteCreateFunction('update_notification', 'on_db_update', 1);
$dbh->sqlite_create_function( 'update_notification', 1, sub { my $changes = shift; # retrieve the argument passed to the function print "Update erhalten: $changes"; });
db.create_function "update_notification", 1 do |func, changes| puts "Update erhalten: #{changes}" func.result = null end
db.create(function: "update_notification", argc: 1) { args in let changes = args.first as! String println("Update erhalten: \(changes)") return nil }
db:create_function('update_notification',1,function(ctx,changes) print('Update erhalten: ' .. changes) ctx:result_null() end)
func on_db_update(changes string) int64 { print('Update erhalten: ' + changes) return null } sql.Register("sqlite3_custom", &sqlite.SQLiteDriver{ ConnectHook: func(conn *sqlite.SQLiteConn) error { if err := conn.RegisterFunc("update_notification", on_db_update, true); err != nil { return err } return nil }, }) db, err := sql.Open("sqlite3_custom", "file:data.db?node=...")
Wichtig: Die Benachrichtigungsfunktion wird vom Arbeitsthread aufgerufen. Die Anwendung sollte die Datenbankverbindung NICHT innerhalb der Benachrichtigungsfunktion verwenden und muss so schnell wie möglich zurückkehren! Die Anwendung kann die Benachrichtigung vor der Rückkehr an den Hauptthread übertragen.
Überprüfen, ob die Datenbank bereit ist
Wenn die App zum ersten Mal auf einem Gerät geöffnet ist, kann sie eine neue Kopie der Datenbank von einem anderen Knoten herunterladen. Bis es fertig ist, können wir nicht auf die Datenbank zugreifen.
Wir können den Synchronisierungsstatus abrufen und den db_is_ready Variable überprüfen.
Überprüfen Sie die folgenden grundlegenden App-Beispiele.
Wie verwende ich es in meiner App?
Es gibt 3 Schritte:
1 Ersetzen Sie die SQLite-Bibliothek durch die mit LiteSync
2 Ändern Sie die URI-Verbindungszeichenfolge
3 Überprüfen Sie den Status der Datenbankbereitschaft
Beim Kompilieren von C- und C ++ - Apps müssen Sie Ihre Anwendung mit der LiteSync-Bibliothek verknüpfen.
Für andere Sprachen muss das richtige wrapper installiert sein.
Beispiel für einen Primärknoten
Der Primärknoten kann eine normale Anwendung sein, genau dieselbe App wie die Sekundärknoten, jedoch mit einem anderen URI.
Oder wir können eine App verwenden, die als primärer Knoten dient.
Eine eigenständige Basisanwendung, die ausschließlich zum Halten eines zentralisierten Datenbankknotens verwendet wird, sieht folgendermaßen aus:
Wähle eine Sprache -->
#include <sqlite3.h> char *uri = "file:/path/to/app.db?node=primary&bind=tcp://0.0.0.0:1234"; int main() { sqlite3 *db; sqlite3_open(uri, &db); /* open the database */ while(1) sleep(1); /* keep the app open */ }
import litesync as sqlite3 conn = sqlite3.connect('file:/path/to/app.db?node=primary&bind=tcp://0.0.0.0:1234') # keep the app open import time while True: time.sleep(60) # in seconds
const uri = 'file:app.db?node=primary&bind=tcp://0.0.0.0:1234'; const options = { verbose: console.log }; const db = require('better-sqlite3-litesync')(uri, options); // keep the app open setInterval(function(){}, 5000);
import java.sql.Connection; import java.sql.DriverManager; public class Sample { public static void main(String[] args) { String uri = "file:/path/to/app.db?node=primary&bind=tcp://0.0.0.0:1234"; Connection connection = DriverManager.getConnection("jdbc:sqlite:" + uri); // keep the app open while (true) { Thread.sleep(5000); } } }
using SQLite; public class Program { public static void Main() { // open the database var uri = "file:app.db?node=primary&bind=tcp://0.0.0.0:1234"; var db = new SQLiteConnection(uri); // keep the app open while(true) { System.Threading.Thread.Sleep(5000); } } }
Imports SQLite Public Class Program Public Shared Sub Main() ' open the database Dim db As New SQLiteConnection("file:app.db?node=primary&bind=tcp://0.0.0.0:1234") ' keep the app open Do System.Threading.Thread.Sleep(5000) Loop End Sub End Class
Option Explicit Declare Sub Sleep Lib "kernel32.dll" (ByVal dwMilliseconds As Long) Public Sub Main() Dim URI As String Dim Conn As New ADODB.Connection ' Open the connection URI = "file:C:\app\mydb.db?node=primary&bind=tcp://0.0.0.0:1234" Conn.Open "DRIVER=SQLite3 ODBC Driver;Database=" & URI ' Keep the app open Do: Sleep(5000): Loop End Sub
<?php // with sqlite3: $db = new SQLite3("file:/path/to/app.db?node=primary&bind=tcp://0.0.0.0:1234"); // with pdo_sqlite: $pdo = new PDO("sqlite:file:/path/to/app.db?node=primary&bind=tcp://0.0.0.0:1234"); // keep the app open - it should not be used with apache while(1) sleep(5); ?>
use DBI; my $dbh = DBI->connect("dbi:SQLite:uri=file:/path/to/app.db?node=primary&bind=tcp://0.0.0.0:1234"); // keep the app open - it should not be used with apache sleep;
require 'sqlite3' db = SQLite3::Database.new "file:/path/to/app.db?node=primary&bind=tcp://0.0.0.0:1234" # keep the app open loop do sleep(1) end
local sqlite3 = require("lsqlite3") local db = sqlite3.open('file:/path/to/app.db?node=primary&bind=tcp://0.0.0.0:1234') -- keep the app open local lsocket = require("lsocket") while true do lsocket.select(5000) end
package main import ( "database/sql" _ "github.com/litesync/go-sqlite3" "time" ) func main() { db, err := sql.Open("sqlite3", "file:/path/to/app.db?node=primary&bind=tcp://0.0.0.0:1234") // keep the app open for { time.Sleep(1000 * time.Millisecond) } }
Beispiel für eine einfache App
Eine grundlegende App, die in die lokale Datenbank schreibt, sieht folgendermaßen aus:
Wähle eine Sprache -->
#include <sqlite3.h> char *uri = "file:/path/to/app.db?node=secondary&connect=tcp://myserver.ddns.net:1234"; int main() { sqlite3 *db; /* open the database */ sqlite3_open(&db, uri); /* check if the db is ready */ while(1){ char *json_str = sqlite3_query_value_str(db, "PRAGMA sync_status"); BOOL db_is_ready = sqlite3_json_get_bool(json_str, "db_is_ready"); sqlite3_free(json_str); if (db_is_ready) break; sleep_ms(250); } /* access the database */ start_access(db); }
import litesync as sqlite3 import json import time conn = sqlite3.connect('file:/path/to/app.db?node=secondary&connect=tcp://myserver.ddns.net:1234') # check if the db is ready while not conn.is_ready(): time.sleep(0.250) start_access(conn)
const uri = 'file:test.db?node=secondary&connect=tcp://127.0.0.1:1234'; const options = { verbose: console.log }; const db = require('better-sqlite3-litesync')(uri, options); db.on('ready', function() { // the database is ready to be accessed db.exec('CREATE TABLE IF NOT EXISTS users (name, email)'); ... });
import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.Statement; import org.json.*; public class Sample { public static void main(String[] args) { String uri = "file:/path/to/app.db?node=secondary&connect=tcp://myserver.ddns.net:1234"; Connection connection = DriverManager.getConnection("jdbc:sqlite:" + uri); Statement statement = connection.createStatement(); // check if the db is ready while (true) { ResultSet rs = statement.executeQuery("PRAGMA sync_status"); rs.next(); JSONObject obj = new JSONObject(rs.getString(1)); if (obj.getBoolean("db_is_ready")) break; Thread.sleep(250); } // now we can access the db start_access(connection); } }
using SQLite; public class Program { public static void Main() { // open the database var uri = "file:app.db?node=secondary&connect=tcp://server:port"; var db = new SQLiteConnection(uri); // wait until the db is ready while (!db.IsReady()) { System.Threading.Thread.Sleep(250); } // now we can use the database db.CreateTable<TodoItem>(CreateFlags.AutoIncPK); ... } }
Imports SQLite Public Class Program Public Shared Sub Main() ' open the database Dim db As New SQLiteConnection("file:app.db?node=secondary&connect=tcp://server:port") ' wait until the db is ready While Not db.IsReady() System.Threading.Thread.Sleep(250) End While ' now we can use the database db.CreateTable(Of TodoItem)(CreateFlags.AutoIncPK) ' ... End Sub End Class
Option Explicit Declare Sub Sleep Lib "kernel32.dll" (ByVal dwMilliseconds As Long) Public Sub Main() Dim Conn As New ADODB.Connection Dim Rst As ADODB.Recordset Dim URI As String URI = "file:C:\app\mydb.db?node=secondary&connect=tcp://myserver.ddns.net:1234" Conn.Open "DRIVER=SQLite3 ODBC Driver;Database=" & URI ' Check if the database is ready Do Set Rst = New ADODB.Recordset Rst.Open "PRAGMA sync_status", Conn, , , adCmdText If InStr(Rst!sync_status, """db_is_ready"": true") > 0 Then Exit Do Sleep 200 Loop ' Now we can access the db StartDbAccess(Conn) End Sub
<?php // with sqlite3: $db = new SQLite3("file:/path/to/app.db?node=secondary&connect=tcp://myserver.ddns.net:1234"); // with pdo_sqlite: $pdo = new PDO("sqlite:file:/path/to/app.db?node=secondary&connect=tcp://myserver.ddns.net:1234"); // check if the db is ready while(1) { $results = $db->query('PRAGMA sync_status'); $row = $results->fetchArray(); $status = json_decode($row[0], true); if ($status['db_is_ready'] == true) break; sleep(0.25); } // now we can access the db start_access($db); ?>
use DBI; use JSON qw( decode_json ); my $dbh = DBI->connect("dbi:SQLite:uri=file:/path/to/app.db?node=secondary&connect=tcp://myserver.ddns.net:1234"); // check if the db is ready - it should not be used with apache while (1) { my ($result) = $dbh->selectrow_array("PRAGMA sync_status"); my $status = decode_json($result); if ($status->{'db_is_ready'}) last; sleep; } // now we can access the db ...
require 'sqlite3' require 'json' db = SQLite3::Database.new "file:/path/to/app.db?node=secondary&connect=tcp://myserver.ddns.net:1234" # check if the db is ready loop do result = db.get_first_value "PRAGMA sync_status" status = JSON.parse(result) break if status["db_is_ready"] == true sleep 0.25 end # now we can access the db start_access(db)
local sqlite3 = require "lsqlite3" local json = require "json" local db = sqlite3.open('file:/path/to/app.db?node=secondary&connect=tcp://myserver.ddns.net:1234') -- check if the db is ready local lsocket = require("lsocket") while true do local result = db:rows("PRAGMA sync_status") local status = json:decode(result[0]) if status["db_is_ready"] == true then break end lsocket.select(250) end -- now we can access the db start_access(db)
package main import ( "database/sql" _ "github.com/litesync/go-sqlite3" "time" ) func main() { db, err := sql.Open("sqlite3", "file:/path/to/app.db?node=secondary&connect=tcp://myserver.ddns.net:1234") // wait until the db is ready for !db.IsReady() { time.Sleep(1000 * time.Millisecond) } // now we can access the db start_access(db) }
AKTUELLE EINSCHRÄNKUNGEN
1 Nicht-deterministische Funktionen (die jedes Mal unterschiedliche Werte zurückgeben, wenn sie aufgerufen werden) sind blockiert, wie random() und date('now'). Verwenden Sie explizit erzeugte Werte in Ihrer App
2 Das Schlüsselwort AUTOINCREMENT wird nicht unterstützt - aber Sie benötigen es nicht! (Details im Video)
3 Nur eine einzige Anwendung kann gleichzeitig auf die Datenbank zugreifen. Jede Instanz muss ihre eigene Datenbank verwenden, die dann mit LiteSync repliziert und synchronisiert wird