Hallo, ich entwickle Anwendungen für Datenbanken — eine von der Mail.ru Group entwickelte Plattform, die eine leistungsstarke Datenbank und einen Anwendungsserver in Lua vereint. Die hohe Geschwindigkeit von Tarantool-basierten Lösungen wird unter anderem durch die Unterstützung des In-Memory-Datenbankmodus und die Möglichkeit erreicht, Geschäftslogik innerhalb des gleichen Adressraums wie die Daten auszuführen. Dabei wird die Persistenz der Daten durch ACID-Transaktionen gewährleistet (eine WAL-Protokolldatei wird auf der Festplatte geführt). Tarantool bietet integrierte Unterstützung für Replikation und Sharding. Seit Version 2.1 werden auch SQL-Abfragen unterstützt. Tarantool ist Open Source und steht unter der Simplified BSD-Lizenz zur Verfügung. Zudem gibt es eine kommerzielle Enterprise-Version.

Erleben Sie die Kraft! (…bzw. genießen Sie die Leistung)
All dies macht Tarantool zu einer attraktiven Plattform für die Entwicklung hochbelasteter Anwendungen, die mit Datenbanken arbeiten. In solchen Anwendungen besteht häufig die Notwendigkeit zur Datenreplikation.
Wie bereits erwähnt, bietet Tarantool eine integrierte Datenreplikation. Das Prinzip besteht darin, dass alle Transaktionen, die im Master-Log (WAL) enthalten sind, sequenziell auf den Replikaten ausgeführt werden. Diese Art der Replikation (im Folgenden als niederläufig) bezeichnet, wird typischerweise zur Gewährleistung der Ausfallsicherheit der Anwendung und/oder zur Verteilung der Lese-Last zwischen den Knoten des Clusters eingesetzt.

Abb. 1. Replikation innerhalb des Clusters
Ein Beispiel für ein alternatives Szenario könnte die Übertragung von Daten sein, die in einer Datenbank erstellt wurden, in eine andere Datenbank zur Verarbeitung/Überwachung. In diesem Fall könnte es vorteilhafter sein, höhergelegte Replikation zu verwenden – Datenreplikation auf der Ebene der Geschäftslogik der Anwendung. Das heißt, wir nutzen nicht die integrierte Lösung der DBMS, sondern implementieren die Replikation selbst innerhalb unserer Anwendung. Dieser Ansatz hat sowohl Vorteile als auch Nachteile. Lassen Sie uns die Vorteile auflisten.
1. Einsparung von Bandbreite:
- Es ist möglich, nicht alle Daten, sondern nur einen Teil davon zu übertragen (zum Beispiel kann man nur bestimmte Tabellen, einige ihrer Spalten oder Datensätze, die bestimmten Kriterien entsprechen, übertragen);
- Im Gegensatz zur Low-Level-Replikation, die kontinuierlich im asynchronen (in der aktuellen Version von Tarantool — 1.10 implementiert) oder synchronen (wird in zukünftigen Versionen von Tarantool umgesetzt) Modus erfolgt, kann die High-Level-Replikation in Sitzungen durchgeführt werden (das heißt, die Anwendung synchronisiert zunächst die Daten — Datenaustausch-Sitzung, danach gibt es eine Pause in der Replikation, gefolgt von der nächsten Datenaustausch-Sitzung usw.);
- Wenn ein Datensatz mehrmals geändert wurde, kann man nur die letzte Version davon übertragen (im Gegensatz zur Low-Level-Replikation, bei der alle Änderungen, die am Master vorgenommen wurden, nacheinander auf den Replikaten wiedergegeben werden);
2. Es gibt keine Schwierigkeiten bei der Implementierung des Austauschs über HTTP, was die Synchronisierung von entfernten Datenbanken ermöglicht.

Abb. 2. Replikation über HTTP
3. Datenbankstrukturen, über die Daten übertragen werden, müssen nicht identisch sein (im Allgemeinen ist sogar die Verwendung verschiedener Datenbanksysteme, Programmiersprachen, Plattformen usw. möglich).

Abb. 3. Replikation in heterogenen Systemen
Der Nachteil ist, dass Programmierung im Durchschnitt komplexer/kostenintensiver ist als die Konfiguration, und anstelle der Anpassung der integrierten Funktionalitäten muss man eigene Lösungen entwickeln.
Wenn die genannten Vorteile in Ihrer Situation entscheidend sind (oder eine notwendige Bedingung darstellen), lohnt es sich, eine hochklassige Replikation zu verwenden. Lassen Sie uns einige Möglichkeiten zur Implementierung einer hochklassigen Datenreplikation in der Datenbank Tarantool betrachten.
Minimierung des Datenverkehrs
Eines der Vorteile der hochgradigen Replikation ist die Einsparung von Datenverkehr. Damit dieses Vorteil voll zum Tragen kommt, ist es wichtig, die Menge der bei jedem Austausch gesendeten Daten zu minimieren. Natürlich sollte auch nicht vergessen werden, dass der Datenempfänger am Ende des Austauschs mit der Quelle synchronisiert werden muss (mindestens in Bezug auf die Daten, die an der Replikation beteiligt sind).
Wie kann man die Menge der bei hochgradiger Replikation gesendeten Daten minimieren? Eine direkte Lösung könnte die Auswahl von Daten nach Datum und Uhrzeit sein. Dazu kann das bereits in der Tabelle vorhandene Feld für Datum und Uhrzeit verwendet werden (falls vorhanden). Zum Beispiel könnte das Dokument "Bestellung" ein Feld für "erforderlichen Lieferzeitpunkt" haben - delivery_time. Das Problem dieser Lösung besteht darin, dass die Werte in diesem Feld nicht unbedingt in einer Reihenfolge vorliegen, die mit der Erstellung der Bestellungen übereinstimmt. Somit können wir den maximalen Wert des Feldes delivery_time, der beim vorherigen Austausch übertragen wurde, nicht speichern und bei dem nächsten Austausch alle Einträge mit einem höheren Wert des Feldes auswählen. delivery_time. Zwischen den Austausch-Sitzungen könnten Datensätze mit einem geringeren Wert im Feld hinzugefügt worden sein. delivery_time. Auch könnte die Bestellung Änderungen erfahren haben, die jedoch das Feld nicht betroffen haben. delivery_time. In beiden Fällen werden die Änderungen nicht vom Quell- zum Zielsystem übertragen. Um diese Probleme zu lösen, müssen wir die Daten "überlappend" übertragen. Das bedeutet, dass wir bei jeder Austausch-Sitzung alle Daten mit einem Feldwert übermitteln werden, delivery_time, der einen bestimmten Zeitpunkt in der Vergangenheit überschreitet (zum Beispiel N Stunden von jetzt an). Es ist jedoch offensichtlich, dass dieser Ansatz für große Systeme stark überdimensioniert ist und die Einsparungen beim Datenverkehr, die wir anstreben, zunichte machen kann. Darüber hinaus könnte das übertragene Tabellenfeld kein Datum-Uhrzeit-Feld enthalten.
Eine andere Lösung, die aus Implementierungssicht komplizierter ist, besteht darin, den Erhalt der Daten zu bestätigen. In diesem Fall werden bei jeder Austausch-Sitzung alle Daten übertragen, deren Erhalt vom Empfänger nicht bestätigt wurde. Für die Umsetzung muss eine boolesche Spalte in der Quelltabelle hinzugefügt werden (zum Beispiel, is_transferred). Wenn der Empfänger den Erhalt des Datensatzes bestätigt, nimmt das entsprechende Feld den Wert an. true, nach dem die Aufzeichnung nicht mehr am Austausch teilnimmt. Diese Implementierungsvariante hat folgende Nachteile. Erstens muss für jede übertragene Aufzeichnung eine Bestätigung generiert und gesendet werden. Grob gesagt kann dies mit einer Verdopplung der übertragenen Daten verglichen werden und zu einer Verdopplung der Roundtrips führen. Zweitens besteht keine Möglichkeit, dieselbe Aufzeichnung an mehrere Empfänger zu senden (der erste empfangende Empfänger bestätigt den Erhalt für sich und alle anderen).
Eine Möglichkeit, die die oben genannten Nachteile vermeidet, besteht darin, der übertragenden Tabelle eine Spalte zur Nachverfolgung der Änderungen an ihren Zeilen hinzuzufügen. Diese Spalte kann den Typ Datum-Zeit haben und sollte von der Anwendung bei jedem Hinzufügen/Ändern von Aufzeichnungen (atomar mit dem Hinzufügen/Ändern) auf die aktuelle Zeit gesetzt/aktualisiert werden. Ein Beispiel für eine solche Spalte könnte sein update_time. Indem wir den maximalen Wert dieses Spaltenfeldes für die übertragenen Aufzeichnungen speichern, können wir die nächste Austauschsession mit diesem Wert beginnen (Aufzeichnungen mit dem Wert des Feldes auswählen. update_time, die den zuvor gespeicherten Wert überschreitet). Das Problem bei diesem Ansatz besteht darin, dass Datenänderungen im Batch-Modus erfolgen können. Dadurch sind die Werte der Felder in der Spalte möglicherweise update_time nicht einzigartig. Diese Spalte kann somit nicht für die paginierte Ausgabe von Daten verwendet werden. Um eine paginierte Ausgabe zu ermöglichen, müssen zusätzliche Mechanismen entwickelt werden, die wahrscheinlich eine sehr geringe Effizienz aufweisen (zum Beispiel das Abrufen aller Datensätze aus der Datenbank mit einem Wert update_time über dem festgelegten Wert und die Ausgabe einer bestimmten Anzahl von Datensätzen, die von einem bestimmten Offset vom Anfang der Auswahl ausgehen).
Die Effizienz der Datenübertragung kann gesteigert werden, indem der vorherige Ansatz leicht verbessert wird. Dazu verwenden wir für die Werte der Felder in der Spalte zur Verfolgung von Änderungen einen ganzzahligen Datentyp (long). Wir nennen die Spalte row_ver. Der Wert dieses Feldes muss weiterhin bei jeder Erstellung oder Änderung eines Datensatzes festgelegt bzw. aktualisiert werden. In diesem Fall wird jedoch dem Feld nicht das aktuelle Datum und die Uhrzeit zugewiesen, sondern der Wert eines Zählers, der um eins erhöht wird. Infolgedessen enthält die Spalte row_ver eindeutige Werte und kann nicht nur zur Ausgabe von „Delta“-Daten (Daten, die nach Abschluss der vorherigen Austauschsession hinzugefügt oder geändert wurden) verwendet werden, sondern auch für eine einfache und effektive Paginierung.
Die letzte vorgeschlagene Methode zur Minimierung der übermittelten Daten im Rahmen der hochgradigen Replikation erscheint mir als die optimalste und universellste. Lassen Sie uns näher darauf eingehen.
Datenübertragung unter Verwendung eines Zeilenversionszählers
Implementierung des Server-/Masterteils
In MS SQL Server gibt es einen speziellen Spaltentyp zur Umsetzung eines solchen Ansatzes — rowversion. Jede Datenbank hat einen Zähler, der jedes Mal um eins erhöht wird, wenn ein Datensatz in einer Tabelle mit dieser Spalte hinzugefügt oder geändert wird. rowversion. Der Wert dieses Zählers wird automatisch dem Feld dieser Spalte im neuen oder geänderten Datensatz zugewiesen. Die Tarantool-Datenbank hat kein entsprechendes integriertes Mechanismus. Allerdings kann dies in Tarantool leicht manuell implementiert werden. Lassen Sie uns ansehen, wie dies funktioniert.
Zunächst etwas Terminologie: Tabellen in Tarantool werden als Spaces bezeichnet, und Datensätze sind Tupel. In Tarantool können Sequenzen erstellt werden. Sequenzen sind nichts anderes als benannte Generatoren für geordnete Ganzzahlen. Das ist genau das, was wir für unsere Zwecke brauchen. Im Folgenden werden wir eine solche Sequenz erstellen.
Bevor Sie eine Operation mit der Datenbank in Tarantool durchführen, müssen Sie den folgenden Befehl ausführen:
box.cfg{}Infolgedessen beginnt Tarantool, Snapshots der Datenbank und ein Transaktionsprotokoll im aktuellen Verzeichnis zu speichern.
Erstellen wir eine Sequenz row_version:
box.schema.sequence.create('row_version',
{ if_not_exists = true }) Option if_not_exists ermöglicht es, das Erstellungsskript mehrmals auszuführen: Wenn das Objekt existiert, wird Tarantool nicht versuchen, es erneut zu erstellen. Diese Option wird in allen folgenden DDL-Befehlen verwendet.
Lassen Sie uns einen Space als Beispiel erstellen.
box.schema.space.create('goods', {
format = {
{
name = 'id',
type = 'unsigned'
},
{
name = 'name',
type = 'string'
},
{
name = 'code',
type = 'unsigned'
},
{
name = 'row_ver',
type = 'unsigned'
}
},
if_not_exists = true
}) Hier haben wir den Namen des Spaces gesetzt (goods), die Feldnamen und deren Typen.
Auto-Inkrement-Felder in Tarantool werden ebenfalls mit Hilfe von Sequenzen erstellt. Lassen Sie uns einen auto-increment Primärschlüssel für das Feld erstellen. id:
box.schema.sequence.create('goods_id',
{ if_not_exists = true })
box.space.goods:create_index('primary', {
parts = { 'id' },
sequence = 'goods_id',
unique = true,
type = 'HASH',
if_not_exists = true
})Tarantool unterstützt mehrere Indextypen. Am häufigsten werden die Indexarten TREE und HASH verwendet, die auf den jeweiligen Strukturen basieren. TREE ist der vielseitigste Indextyp. Er ermöglicht das Abrufen von Daten in geordneter Form. Für gleichheitsbasierte Abfragen eignet sich jedoch HASH besser. Daher ist es sinnvoll, HASH für den Primärschlüssel zu verwenden (was wir auch getan haben).
Um die Spalte row_ver für die Übertragung geänderter Daten zu verwenden, müssen die Werte der Sequenz an die Felder dieser Spalte gebunden werden. row_ver. Im Gegensatz zu einem Primärschlüssel muss der Wert des Feldes in der Spalte row_ver bei jeder neuen Eintragung nicht nur um eins erhöht werden, sondern auch bei Änderungen an bestehenden Daten. Dazu können Trigger verwendet werden. In Tarantool gibt es zwei Arten von Triggern für Spaces: before_replace und on_replace. Trigger werden bei jeder Änderung der Daten in einem Space ausgeführt (für jedes von Änderungen betroffene Tuple wird die Trigger-Funktion aufgerufen). Anders als bei on_replace, before_replace-Triggern ermöglichen es, die Daten des Tuples, für das der Trigger ausgeführt wird, zu modifizieren. Daher passt uns der letzte Typ von Triggern.
box.space.goods:before_replace(function(old, new)
return box.tuple.new({new[1], new[2], new[3],
box.sequence.row_version:next()})
end) Der oben genannte Trigger ersetzt den Wert des Feldes row_ver des gespeicherten Tuples durch den nächsten Wert der Sequenz. row_version.
Um Daten aus dem Space goods basierend auf der Spalte row_verzu extrahieren, erstellen wir einen Index:
box.space.goods:create_index('row_ver', {
parts = { 'row_ver' },
unique = true,
type = 'TREE',
if_not_exists = true
}) Der Indextyp ist Baum (TREE), da wir die Daten in aufsteigender Reihenfolge der Werte in der Spalte abrufen müssen. row_ver.
Fügen wir einige Daten in den Space ein:
box.space.goods:insert{nil, 'Stift', 123}
box.space.goods:insert{nil, 'Bleistift', 321}
box.space.goods:insert{nil, 'Pinsel', 100}
box.space.goods:insert{nil, 'Aquarellfarbe', 456}
box.space.goods:insert{nil, 'Album', 101}
box.space.goods:insert{nil, 'Notizbuch', 800}
box.space.goods:insert{nil, 'Radiergummi', 531}
box.space.goods:insert{nil, 'Lineal', 135} Da das erste Feld ein Auto-Inkrement-Zähler ist, übergeben wir stattdessen nil. Tarantool fügt automatisch den nächsten Wert ein. Entsprechend kann für die Werte der Spaltenfelder row_ver nil übergeben werden – oder der Wert kann ganz weggelassen werden, da diese Spalte die letzte Position im Raum einnimmt.
Überprüfen wir das Ergebnis der Einfügung:
tarantool> box.space.goods:select()
---
- - [1, 'Stift', 123, 1]
- [2, 'Bleistift', 321, 2]
- [3, 'Pinsel', 100, 3]
- [4, 'Aquarellfarbe', 456, 4]
- [5, 'Album', 101, 5]
- [6, 'Notizbuch', 800, 6]
- [7, 'Radiergummi', 531, 7]
- [8, 'Lineal', 135, 8]
... Wie wir sehen, wurden das erste und das letzte Feld automatisch ausgefüllt. Jetzt wird es nicht schwer sein, eine Funktion zur seitenweisen Abgleichung der Änderungen im Raum zu schreiben. goods:
local page_size = 5
local function get_goods(row_ver)
local index = box.space.goods.index.row_ver
local goods = {}
local counter = 0
for _, tuple in index:pairs(row_ver, {
iterator = 'GT' }) do
local obj = tuple:tomap({ names_only = true })
table.insert(goods, obj)
counter = counter + 1
if counter >= page_size then
break
end
end
return goods
end Die Funktion nimmt als Parameter den Wert row_ver, ab dem Änderungen ausgegeben werden müssen, und gibt einen Satz von geänderten Daten zurück.
Datenabfragen in Tarantool erfolgen über Indizes. Die Funktion get_goods verwendet einen Iterator über den Index row_ver um die geänderten Daten zu erhalten. Der Iterator-Typ ist GT (Greater Than, größer als). Das bedeutet, dass der Iterator die Werte des Index fortlaufend ab dem übergebenen Schlüssel (dem Wert des Feldes row_ver).
den Tupel zurückgibt. Um die Daten später über HTTP übertragen zu können, ist es notwendig, die Tupel in ein Format umzuwandeln, das für die spätere Serialisierung geeignet ist. Im Beispiel wird dafür die Standardfunktion tomapverwendet. Anstelle der Nutzung von tomap kann eine eigene Funktion geschrieben werden. Beispielsweise könnten wir das Feld nameumbenennen, das Feld code auslassen und das Feld comment:
lokale Funktion unflatten_goods(tuple)
lokale obj = {}
obj.id = tuple.id
obj.goods_name = tuple.name
obj.comment = 'ein Kommentar'
obj.row_ver = tuple.row_ver
return obj
end Die Seitengröße der ausgegebenen Daten (die Anzahl der Datensätze in einer Menge) wird durch die Variable page_size. Im Beispiel beträgt der Wert page_size 5 ist das Minimum. In der realen Anwendung hat die Seitengröße oft eine größere Bedeutung. Sie hängt von der durchschnittlichen Größe der Tupel im Raum ab. Optimal lässt sich die Seitenstruktur durch Experimentieren finden, indem man die Übertragungszeiten misst. Je größer die Seiten, desto weniger Rundreisen gibt es zwischen Sender und Empfänger. Das reduziert die gesamte Zeit, die benötigt wird, um Änderungen zu übertragen. Allerdings kann eine zu große Seitengröße dazu führen, dass der Server zu lange mit der Serialisierung der Abfragen beschäftigt ist. Dies kann Verzögerungen bei der Verarbeitung anderer Anfragen verursachen, die den Server erreichen. Der Parameter page_size kann aus einer Konfigurationsdatei geladen werden. Für jeden übertragenen Raum kann ein eigenes Wert festgelegt werden. Für die meisten Räume kann jedoch der Standardwert (z.B. 100) verwendet werden.
Führen wir die Funktion aus get_goods:
tarantool> get_goods(0)
---
- - row_ver: 1
code: 123
name: Stift
id: 1
- row_ver: 2
code: 321
name: Bleistift
id: 2
- row_ver: 3
code: 100
name: Pinsel
id: 3
- row_ver: 4
code: 456
name: Aquarellfarbe
id: 4
- row_ver: 5
code: 101
name: Album
id: 5
... Nehmen wir den Wert des Feldes row_ver aus der letzten Zeile und rufen die Funktion erneut auf:
tarantool> get_goods(5)
---
- - row_ver: 6
code: 800
name: notebook
id: 6
- row_ver: 7
code: 531
name: gummi
id: 7
- row_ver: 8
code: 135
name: lineal
id: 8
...Und noch einmal:
tarantool> get_goods(8)
---
- []
... Wie wir sehen, gibt die Funktion bei dieser Verwendung alle Einträge des Spaces seitenweise zurück. goodsNach der letzten Seite folgt eine leere Auswahl.
Lassen Sie uns Änderungen im Space vornehmen:
box.space.goods:update(4, {{'=', 6, 'schreibblock'}})
box.space.goods:insert{nil, 'klammer', 234}
box.space.goods:insert{nil, 'ordner', 432} Wir haben den Wert des Feldes name für einen Eintrag geändert und zwei neue Einträge hinzugefügt.
Lassen Sie uns den letzten Funktionsaufruf wiederholen:
tarantool> get_goods(8)
---
- - row_ver: 9
code: 800
name: schreibblock
id: 6
- row_ver: 10
code: 234
name: klammer
id: 9
- row_ver: 11
code: 432
name: ordner
id: 10
... Die Funktion hat die geänderten und hinzugefügten Einträge zurückgegeben. Somit get_goods ermöglicht die Funktion das Abrufen von Daten, die sich seit ihrem letzten Aufruf geändert haben, was die Grundlage des beschriebenen Replikationsverfahrens darstellt.
Die Ausgabe der Ergebnisse über HTTP im JSON-Format bleibt außerhalb dieses Artikels. Dazu können Sie hier lesen:
Implementierung des Client-/Slave-Teils
Betrachten wir, wie die Implementierung der empfangenden Seite aussieht. Erstellen wir auf der empfangenden Seite einen Space zum Speichern der hochgeladenen Daten:
box.schema.space.create('goods', {
format = {
{
name = 'id',
type = 'unsigned'
},
{
name = 'name',
type = 'string'
},
{
name = 'code',
type = 'unsigned'
}
},
if_not_exists = true
})
box.space.goods:create_index('primary', {
parts = { 'id' },
sequence = 'goods_id',
unique = true,
type = 'HASH',
if_not_exists = true
}) Die Struktur des Spaces ähnelt der Struktur des Spaces in der Quelle. Da wir jedoch nicht beabsichtigen, die erhaltenen Daten irgendwo anders zu übertragen, fehlt die Spalte row_ver im Ziel-Space. Im Feld id werden die Identifikatoren der Quelle gespeichert. Daher ist es auf der Empfängerseite nicht notwendig, diesen automatisch inkrementierend zu gestalten.
Darüber hinaus benötigen wir einen Space zur Speicherung der Werte row_ver:
box.schema.space.create('row_ver', {
format = {
{
name = 'space_name',
type = 'string'
},
{
name = 'value',
type = 'string'
}
},
if_not_exists = true
})
box.space.row_ver:create_index('primary', {
parts = { 'space_name' },
unique = true,
type = 'HASH',
if_not_exists = true
}) Für jeden geladenen Space (Feld space_name) speichern wir hier den zuletzt geladenen Wert row_ver (Feld value). Die Spalte space_name.
dient als primärer Schlüssel. Erstellen wir eine Funktion zum Laden der Daten des Spaces. goods über HTTP. Dazu benötigen wir eine Bibliothek, die den HTTP-Client implementiert. Die folgende Zeile lädt die Bibliothek und erstellt ein Exemplar des HTTP-Clients:
local http_client = require('http.client').new()Wir benötigen auch eine Bibliothek zur Deserialisierung von JSON:
local json = require('json')Das reicht aus, um eine Funktion zum Laden von Daten zu erstellen:
local function load_data(url, row_ver)
local url = ('%s?rowVer=%s'):format(url,
tostring(row_ver))
local body = nil
local data = http_client:request('GET', url, body, {
keepalive_idle = 1,
keepalive_interval = 1
})
return json.decode(data.body)
end Die Funktion führt eine HTTP-Anfrage an die Adresse url aus und übergibt row_ver sie als Parameter und gibt das deserialisierte Ergebnis der Anfrage zurück.
Die Funktion zum Speichern der erhaltenen Daten sieht folgendermaßen aus:
local function save_goods(goods)
local n = #goods
box.atomic(function()
for i = 1, n do
local obj = goods[i]
box.space.goods:put(
obj.id, obj.name, obj.code)
end
end)
end Die Schleife zum Speichern der Daten im Space goods wurde in eine Transaktion eingefügt (dazu wird die Funktion box.atomic) verwendet, um die Anzahl der Festplattenoperationen zu reduzieren.
Schließlich kann die Funktion zur Synchronisierung des lokalen Spaces goods mit der Quelle wie folgt implementiert werden:
local function sync_goods()
local tuple = box.space.row_ver:get('goods')
local row_ver = tuple and tuple.value or 0
—— set your url here:
local url = 'http://127.0.0.1:81/test/goods/list'
while true do
local goods = load_goods(url, row_ver)
local count = #goods
if count == 0 then
return
end
save_goods(goods)
row_ver = goods[count].rowVer
box.space.row_ver:put({'goods', row_ver})
end
end Zuerst lesen wir den zuvor gespeicherten Wert row_ver für den Space goods. Wenn es nicht vorhanden ist (erste Austauschsession), nehmen wir als row_ver null. Danach führen wir im Loop eine seitenweise Ladeoperation der geänderten Daten von der angegebenen URL durch. Bei jeder Iteration speichern wir die erhaltenen Daten im entsprechenden lokalen Space und aktualisieren den Wert row_ver (im Space row_ver und in der Variable row_ver) — wir nehmen den Wert row_ver aus der letzten Zeile der geladenen Daten.
Zum Schutz vor unbeabsichtigtem Schleifen (im Falle eines Programmfehlers) kann die Schleife while durch für:
for _ = 1, max_req do ... Eine Ausführung der Funktion sync_goods Space goods im Empfänger wird die letzten Versionen aller Einträge des Spaces goods im Quelle enthalten.
Es ist offensichtlich, dass auf diese Weise keine Datenlöschung übertragen werden kann. Wenn eine solche Notwendigkeit besteht, kann ein Löschmarkierung verwendet werden. Wir fügen dem Space goods ein boolesches Feld is_deleted hinzu und verwenden anstelle der physischen Löschung des Eintrags eine logische Löschung — setzen den Wert des Feldes is_deleted auf true. Manchmal ist es anstelle eines booleschen Feldes is_deleted bequemer, ein Feld zu verwenden deleted, in dem das Datum und die Uhrzeit der logischen Löschung des Datensatzes gespeichert werden. Nach der Durchführung der logischen Löschung wird der zum Löschen markierte Datensatz aus dem Quellsystem an den Empfänger übermittelt (entsprechend der oben beschriebenen Logik).
Sequenz row_ver kann verwendet werden, um Daten anderer Spaces zu übertragen: Es ist nicht erforderlich, für jeden übertragenen Space eine separate Sequenz zu erstellen.
Wir haben einen effektiven Ansatz für hochgradige Datenreplikation in Anwendungen erörtert, die das Datenbankmanagementsystem Tarantool verwenden.
Fazit
- Das Datenbankmanagementsystem Tarantool ist ein attraktives, zukunftsträchtiges Produkt zur Erstellung stark ausgelasteter Anwendungen.
- Die hochgradige Datenreplikation bietet eine Reihe von Vorteilen im Vergleich zur niedriggradigen Replikation.
- Die in diesem Artikel besprochene Methode der hochgradigen Replikation minimiert die zu übertragenden Datenmengen, indem nur die Datensätze übertragen werden, die sich seit dem letzten Austausch geändert haben.
Quelle: habr.com
