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){ puts("Update erhalten"); } sqlite3_create_function(db, "update_notification", 1, SQLITE_UTF8, NULL, &on_db_update, NULL, NULL);
def on_db_update(arg): print("Update erhalten") con.create_function("update_notification", 1, on_db_update)
// using better-sqlite3: db.function('update_notification', function() { console.log('Update erhalten'); });
Function.create(conn, "update_notification", new Function() { protected void xFunc() { System.out.println("Update erhalten"); } });
// using System.Data.SQLite: [SQLiteFunction(Name = "update_notification", Arguments = 1, FuncType = FunctionType.Scalar)] class UpdateNotification : SQLiteFunction { public override object Invoke(object[] args) { System.WriteLine("Update erhalten"); return Null; } }
' using System.Data.SQLite: <SQLiteFunction(Name:="update_notification", Arguments:=1, FuncType:=FunctionType.Scalar)> Class UpdateNotification Inherits SQLiteFunction Public Overrides Function Invoke(ByVal args As Object()) As Object System.WriteLine("Update erhalten") Return Null End Function End Class
function on_db_update($string) { echo 'Update erhalten'; } // 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 { print 'Update erhalten' } );
db.create_function "update_notification", 1 do |func, db| puts 'Update erhalten' func.result = null end
db.create(function: "update_notification", argc: 1) { args in println("Update erhalten") return nil }
db:create_function('update_notification',1,function(ctx,db) print('Update erhalten') ctx:result_null() end))
func on_db_update() int64 { print('Update erhalten') 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 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
var sqlite3 = require('sqlite3').verbose(); var db = new sqlite3.Database('file:/path/to/app.db?node=primary&bind=tcp://0.0.0.0:1234'); // 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 System.Data; using System.Data.SQLite; public class Program { public static void Main() { string connStr = "FullUri=file:/path/to/app.db?node=primary&bind=tcp://0.0.0.0:1234"; SQLiteConnection connection = new SQLiteConnection(connStr); connection.Open(); // keep the app open while(true) { System.Threading.Thread.Sleep(5000); } } }
Imports System.Data Imports System.Data.SQLite Module Module1 Sub Main() Dim ConnStr As String = "FullUri=file:/path/to/app.db?node=primary&bind=tcp://0.0.0.0:1234" Dim Conn As New SQLite.SQLiteConnection(ConnStr) Conn.Open() ' keep the app open Do System.Threading.Thread.Sleep(5000) Loop End Sub End Module
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 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 True: result = conn.cursor().execute("PRAGMA sync_status").fetchone() status = json.loads(result[0]) if status["db_is_ready"]: break time.sleep(0.250) start_access(conn)
var sqlite3 = require('sqlite3').verbose(); var db = new sqlite3.Database('file:/path/to/app.db?node=secondary&connect=tcp://myserver.ddns.net:1234'); // check if the db is ready var id = setInterval(function() { db.get("PRAGMA sync_status", function(err, res) { if (err) return console.log('getting synchronization status: ' + err); var status = JSON.parse(res.sync_status); if (status.db_is_ready) { clearTimeout(id); start_access(db); } }); }, 250);
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 System.Data; using System.Data.SQLite; using System.Web.Script.Serialization; public class Program { public static void Main() { string connStr = "FullUri=file:/path/to/app.db?node=secondary&connect=tcp://myserver.ddns.net:1234"; SQLiteConnection connection = new SQLiteConnection(connStr); connection.Open(); // check if the db is ready var json = new JavaScriptSerializer(); while(true) { SQLiteCommand command = new SQLiteCommand("PRAGMA sync_status", connection); dynamic status = json.DeserializeObject((string)command.ExecuteScalar()); if (status["db_is_ready"]) break; System.Threading.Thread.Sleep(250); } // now we can access the db start_access(connection); } }
Imports System.Data Imports System.Data.SQLite Imports System.Web.Script.Serialization Module Module1 Sub Main() Dim ConnStr As String = "FullUri=file:/path/to/app.db?node=secondary&connect=tcp://myserver.ddns.net:1234" Dim Conn As New SQLite.SQLiteConnection(ConnStr) Conn.Open() ' check if the db is ready Dim json = New JavaScriptSerializer Do Dim Cmd As New SQLite.SQLiteCommand("PRAGMA sync_status", Conn) Dim status = json.DeserializeObject(Cmd.ExecuteScalar()) If (status!db_is_ready) Then Exit Do System.Threading.Thread.Sleep(250) Loop ' now we can access the db StartDbAccess(Conn) End Sub End Module
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" "github.com/buger/jsonparser" "time" ) func main() { db, err := sql.Open("sqlite3", "file:/path/to/app.db?node=secondary&connect=tcp://myserver.ddns.net:1234") // check if the db is ready for { rows, err := db.Query("PRAGMA sync_status") defer rows.Close() rows.Next() var result string err = rows.Scan(&resut) if jsonparser.GetBoolean(result, "db_is_ready") break time.Sleep(1000 * time.Millisecond) } // now we can access the db start_access(db) }
AKTUELLE EINSCHRÄNKUNGEN
1 Avoid mit Funktionen, die bei jedem Aufruf unterschiedliche Werte zurückgeben, wie random() und date('now')
2 Die AUTOINCREMENT Schlüsselwort wird nicht unterstützt
3 Wir müssen nur eine einzige Verbindung zu jeder Datenbankdatei verwenden. Lassen Sie nicht viele Apps auf dieselbe Datenbankdatei zugreifen. Jede Instanz muss eine eigene Datenbankdatei verwenden. Anschließend werden sie mit LiteSync repliziert und synchronisiert