Replikacja wysokiego poziomu w bazach danych Tarantool

Cześć, zajmuję się tworzeniem aplikacji dla baz danych. Tarantool — to platforma opracowana przez Mail.ru Group, która łączy w sobie wydajną bazę danych oraz serwer aplikacji w języku Lua. Wysoka prędkość rozwiązań opartych na Tarantool osiągana jest między innymi dzięki wsparciu trybu in-memory bazy danych oraz możliwości wykonywania logiki biznesowej aplikacji w tej samej przestrzeni adresowej co dane. Zapewnia to trwałość danych przy wykorzystaniu transakcji ACID (na dysku prowadzone jest dziennik WAL). Tarantool ma wbudowaną obsługę replikacji i shardingu. Od wersji 2.1 wspierane są zapytania w języku SQL. Tarantool ma otwarty kod źródłowy i jest dystrybuowany na zasadzie licencji Simplified BSD. Istnieje również komercyjna wersja Enterprise.

Replikacja wysokiego poziomu w bazach danych Tarantool
Poczuj moc! (…aka ciesz się wydajnością)

Wszystko to sprawia, że Tarantool jest atrakcyjną platformą do tworzenia wysoko obciążonych aplikacji współpracujących z bazą danych. W takich aplikacjach często pojawia się potrzeba replikacji danych.

Jak wspomniano wcześniej, w Tarantool istnieje wbudowana replikacja danych. Jej zasada działania polega na sekwencyjnym wykonywaniu wszystkich transakcji zawartych w dzienniku mastera (WAL) na replikach. Zwykle taka replikacja (nazywana dalej niskopoziomową) wykorzystywana jest do zapewnienia odporności aplikacji na awarie oraz/lub do rozkładania obciążenia na odczyty między węzłami klastra.

Replikacja wysokiego poziomu w bazach danych Tarantool
Rys. 1. Replikacja wewnątrz klastra

Przykładem alternatywnego scenariusza może być przesyłanie danych stworzonych w jednej bazie danych do innej bazy w celu przetwarzania/monitorowania. W ostatnim przypadku bardziej wygodnym rozwiązaniem może okazać się wykorzystanie wysokopoziomowej replikacji — replikacji danych na poziomie logiki biznesowej aplikacji. Tzn. nie wykorzystujemy gotowego rozwiązania, wbudowanego w bazę danych, a sami realizujemy replikację w naszej aplikacji. Takie podejście ma zarówno zalety, jak i wady. Wymieńmy plusy.

1. Oszczędność transferu:

  • można przekazywać nie wszystkie dane, a tylko ich wybrane fragmenty (na przykład, można przekazywać tylko niektóre tabele, wybrane ich kolumny lub rekordy, które spełniają określone kryterium);
  • W przeciwieństwie do niskopoziomowej replikacji, która odbywa się nieprzerwanie w trybie asynchronicznym (wdrożonym w bieżącej wersji Tarantool — 1.10) lub synchronicznym (wdrożonym w przyszłych wersjach Tarantool), replikację wysokopoziomową można realizować w sesjach (tj. aplikacja najpierw synchronizuje dane — sesja wymiany danych, następnie następuje przerwa w replikacji, po której odbywa się następna sesja wymiany itp.);
  • jeśli zapis zmienił się wielokrotnie, można przekazać tylko jego ostatnią wersję (w przeciwieństwie do niskopoziomowej replikacji, w której na replikach zostaną kolejno odtworzone wszystkie zmiany dokonane na masterze).

2. Brak skomplikowanych implementacji wymiany przez HTTP, co umożliwia synchronizację zdalnych baz danych.

Replikacja wysokiego poziomu w bazach danych Tarantool
Rys. 2. Replikacja przez HTTP

3. Struktury baz danych, między którymi są przesyłane dane, nie muszą być identyczne (co więcej, w ogólnym przypadku możliwe jest nawet użycie różnych DBMS, języków programowania, platform itp.).

Replikacja wysokiego poziomu w bazach danych Tarantool
Rys. 3. Replikacja w heterogenicznych systemach

Minusem jest to, że programowanie jest w średniej bardziej złożone/ kosztowne niż konfiguracja, a zamiast dostosowywania wbudowanej funkcjonalności, trzeba zrealizować własną.

Jeżeli w Waszej sytuacji przedstawione zalety mają decydujące znaczenie (lub są niezbędnym warunkiem), warto zastosować replikację wysokopoziomową. Rozważmy kilka sposobów wdrażania replikacji wysokopoziomowej danych w bazie danych Tarantool.

Minimalizacja ruchu

Zatem jednym z zalet replikacji wysokopoziomowej jest oszczędność ruchu. Aby ta zaleta mogła się w pełni ujawnić, należy zminimalizować ilość danych przesyłanych podczas każdej sesji wymiany. Oczywiście, nie można zapominać, że na końcu sesji odbiornik danych musi być zsynchronizowany z źródłem (co najmniej w zakresie danych biorących udział w replikacji).

Jak więc zminimalizować ilość danych przesyłanych przy replikacji wysokopoziomowej? Rozwiązaniem „z głowy” może być selekcja danych według daty-czasu. W tym celu można wykorzystać już istniejące w tabeli pole daty-czasu (jeśli istnieje). Na przykład dokument „zamówienie” może mieć pole „wymagana data realizacji zamówienia” — czas_dostawy. Problem tego rozwiązania polega na tym, że wartości w tym polu nie muszą być uporządkowane zgodnie z kolejnością składania zamówień. W związku z tym nie możemy zapamiętać maksymalnej wartości pola czas_dostawy, przekazanej podczas poprzedniej sesji wymiany, i podczas następnej sesji wymiany wybrać wszystkich rekordów z wyższą wartością pola czas_dostawy. W międzyczasie mogły zostać dodane rekordy z mniejszą wartością pola czas_dostawy. Ponadto zamówienie mogło ulec zmianom, które jednak nie wpłynęły na pole czas_dostawy. W obu przypadkach zmiany nie będą przesyłane z źródła do odbiorcy. Aby rozwiązać te problemy, będziemy musieli przesyłać dane "na zakładkę". To znaczy, że podczas każdej sesji wymiany będziemy przesyłać wszystkie dane z wartością pola czas_dostawy, przekraczającą pewny moment w przeszłości (na przykład N godzin od chwili obecnej). Jednak oczywiste jest, że dla dużych systemów takie podejście jest znacznie nieefektywne i może zniweczyć oszczędności w ruchu sieciowym, do których dążymy. Co więcej, w przesyłanej tabeli może nie być pola związanego z datą i czasem.

Inne rozwiązanie, bardziej skomplikowane pod względem realizacji, polega na potwierdzeniu odbioru danych. W takim przypadku podczas każdej sesji wymiany przesyłane są wszystkie dane, których odbiór nie został potwierdzony przez odbiorcę. Aby to zrealizować, konieczne będzie dodanie do tabeli-źródła kolumny logicznej (na przykład, is_transferred). Jeśli odbiorca potwierdza odbiór rekordu, odpowiednie pole przyjmuje wartość true, po czym rekord nie bierze już udziału w wymianach. Taka wersja realizacji ma następujące wady. Po pierwsze, dla każdego przesyłanego rekordu należy wygenerować i wysłać potwierdzenie. Mówiąc wprost, może to być porównywalne z podwojeniem ilości przesyłanych danych i doprowadzić do podwojenia liczby rund. Po drugie, nie ma możliwości przesyłania tego samego rekordu do kilku odbiorców (pierwszy odbiorca, który otrzyma, potwierdzi odbiór za siebie i za wszystkich innych).

Metoda wolna od powyższych wad polega na dodaniu do przesyłanej tabeli kolumny do śledzenia zmian jej wierszy. Taka kolumna może mieć typ data-czas i powinna być ustalana/aktualizowana przez aplikację na bieżący czas za każdym razem przy dodawaniu/zamianie rekordów (atomowo z dodawaniem/zmianą). Jako przykład nazwijmy tę kolumnę aktualizacja_czasu. Zachowując maksymalną wartość pola tej kolumny dla przesyłanych rekordów, będziemy mogli rozpocząć następna sesję wymiany od tej wartości (wybrać rekordy z wartością pola aktualizacja_czasu, przewyższającą wcześniej zapisaną wartość). Problem związany z ostatnim podejściem polega na tym, że zmiany danych mogą zachodzić w trybie wsadowym. W wyniku tego wartości pól w kolumnie aktualizacja_czasu mogą być nieunikalne. Tak więc ta kolumna nie może być używana do stronicowania danych. Do stronicowania danych będziemy musieli stworzyć dodatkowe mechanizmy, które najprawdopodobniej będą miały bardzo niską wydajność (na przykład pobieranie z bazy danych wszystkich rekordów z wartością aktualizacja_czasu wyższą od zadanej i wydanie określonej ilości rekordów, zaczynając od pewnego przesunięcia od początku próbki).

Można poprawić efektywność przesyłania danych, nieco udoskonalając poprzednie podejście. W tym celu jako wartości pól kolumny do śledzenia zmian będziemy używać typu numerycznego (długie całkowite). Nazwijmy kolumnę wersja_wiersza. Wartość pola tej kolumny powinna być nadal ustalana/aktualizowana za każdym razem przy tworzeniu/zamianie rekordu. Ale w tym przypadku polu zostanie przypisana nie bieżąca data-czas, lecz wartość pewnego licznika, zwiększonego o jeden. W rezultacie kolumna wersja_wiersza będzie zawierać unikalne wartości i będzie mogła być używana nie tylko do wydawania "delta" danych (danych, które zostały dodane/zmienione po zakończeniu poprzedniej sesji wymiany), ale także do prostego i efektywnego rozdzielania ich na strony.

Ostatnio zaproponowany sposób minimalizacji ilości przesyłanych danych w ramach wysokopoziomowej replikacji wydaje mi się najbardziej optymalny i uniwersalny. Zatrzymajmy się na nim dokładniej.

Przesyłanie danych z użyciem licznika wersji wierszy

Realizacja części serwerowej/master

W MS SQL Server do realizacji tego podejścia istnieje specjalny typ kolumny — rowversion. Każda baza danych ma licznik, który zwiększa się o jeden za każdym razem, gdy dodawany/zmieniany jest rekord w tabeli zawierającej kolumnę typu rowversion. Wartość tego licznika jest automatycznie przypisywana do pola tej kolumny w dodawanym/zmienianym rekordzie. SDB Tarantool nie ma odpowiedniego wbudowanego mechanizmu, jednak w Tarantool można to łatwo zaimplementować ręcznie. Zobaczmy, jak to zrobić.

Na początek trochę terminologii: tabele w Tarantool nazywane są spaces (przestrzeniami), a rekordy — krotkami (krotka). W Tarantool można tworzyć sekwencje (sequence). Sekwencje to nic innego jak nazwane generatory uporządkowanych wartości liczb całkowitych. Tzn. to dokładnie to, co potrzebujemy do naszych celów. Poniżej stworzymy taką sekwencję.

Zanim wykonasz jakąkolwiek operację z bazą danych w Tarantool, musisz wykonać następującą komendę:

box.cfg{}

W rezultacie Tarantool zacznie zapisywać w bieżącym katalogu zrzuty bazy danych (snapshot) oraz dziennik transakcji.

Stwórzmy sekwencję row_version:

box.schema.sequence.create('row_version',
    { if_not_exists = true })

Opcja if_not_exists pozwala na wielokrotne wykonanie skryptu tworzenia: jeśli obiekt istnieje, Tarantool nie będzie próbował go tworzyć ponownie. Ta opcja będzie używana we wszystkich kolejnych poleceniach DDL.

Stwórzmy przestrzeń dla przykładu.

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
})

Tutaj określiliśmy nazwę przestrzeni (goods), nazwy pól i ich typy.

Pola z autoincrementem w Tarantool również są tworzone za pomocą sekwencji. Stwórzmy klucz główny autoincrementowy według pola 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 obsługuje kilka typów indeksów. Najczęściej używane to indeksy typu TREE i HASH, które opierają się na odpowiednich strukturach. TREE to najbardziej uniwersalny typ indeksu. Umożliwia on wydobycie danych w uporządkowany sposób. Natomiast do wyborów na podstawie równości lepiej nadaje się HASH. Dlatego do klucza głównego sensowne jest użycie HASH (co właśnie zrobiliśmy).

Aby użyć kolumny wersja_wiersza do przesyłania zmienionych danych, należy powiązać z polami tej kolumny wartości sekwencji wersja_wiersza. Jednak w przeciwieństwie do klucza głównego wartość pola kolumny wersja_wiersza powinna wzrastać o jeden nie tylko przy dodawaniu nowych rekordów, ale także przy modyfikacji istniejących. W tym celu można użyć wyzwalaczy. W Tarantool istnieją dwa typy wyzwalaczy dla przestrzeni: before_replace i on_replace. Wyzwalacze uruchamiają się przy każdej zmianie danych w przestrzeni (dla każdego krotki dotkniętego zmianami uruchamiana jest funkcja wyzwalacza). W przeciwieństwie do on_replace, before_replace-wyzwalacze pozwalają modyfikować dane krotki, dla której wykonywany jest wyzwalacz. Dlatego pasuje nam ostatni typ wyzwalaczy.

box.space.goods:before_replace(function(old, new)
    return box.tuple.new({new[1], new[2], new[3],
        box.sequence.row_version:next()})
end)

Podany wyzwalacz zmienia wartość pola wersja_wiersza przechowywanej krotki na kolejne wartość sekwencji row_version.

Aby można było wydobywać dane z przestrzeni goods na podstawie kolumny wersja_wiersza, stworzymy indeks:

box.space.goods:create_index('row_ver', {
    parts = { 'row_ver' },
    unique = true,
    type = 'TREE',
    if_not_exists = true
})

Typ indeksu — drzewo (TREE), ponieważ będziemy potrzebować wydobywać dane w porządku rosnącym wartości w kolumnie wersja_wiersza.

Dodamy do przestrzeni kilka danych:

box.space.goods:insert{nil, 'pen', 123}
box.space.goods:insert{nil, 'pencil', 321}
box.space.goods:insert{nil, 'brush', 100}
box.space.goods:insert{nil, 'watercolour', 456}
box.space.goods:insert{nil, 'album', 101}
box.space.goods:insert{nil, 'notebook', 800}
box.space.goods:insert{nil, 'rubber', 531}
box.space.goods:insert{nil, 'ruler', 135}

Ponieważ pierwsze pole to licznik autoincrement, przekazujemy zamiast niego nil. Tarantool automatycznie wstawi wartość kolejnego. W podobny sposób jako wartości pól kolumny wersja_wiersza można przekazać nil — lub w ogóle nie wskazywać wartości, ponieważ ta kolumna zajmuje ostatnią pozycję w przestrzeni.

Sprawdźmy wynik wstawienia:

tarantool> box.space.goods:select()
---
- - [1, 'długopis', 123, 1]
  - [2, 'ołówek', 321, 2]
  - [3, 'pędzel', 100, 3]
  - [4, 'akwarela', 456, 4]
  - [5, 'album', 101, 5]
  - [6, 'zeszyt', 800, 6]
  - [7, 'gumka', 531, 7]
  - [8, 'linijka', 135, 8]
...

Jak widać, pierwsze i ostatnie pole wypełniły się automatycznie. Teraz łatwo będzie napisać funkcję do paginacji zmian w przestrzeni. 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

Funkcja przyjmuje jako parametr wartość wersja_wiersza, od której należy rozpocząć wyodrębnianie zmian i zwraca porcję zmienionych danych.

Wyciąganie danych w Tarantool odbywa się przez indeksy. Funkcja get_goods używa iteratora po indeksie wersja_wiersza do pozyskiwania zmienionych danych. Typ iteratora — GT (Greater Than, większe niż). Oznacza to, że iterator będzie kolejno przechodził wartości indeksu, zaczynając od podanego klucza (wartości pola wersja_wiersza).

Iterator zwraca krotki. Aby następnie móc przesłać dane przez HTTP, należy przekształcić krotki w strukturę wygodną do późniejszej serializacji. W przykładzie do tego używana jest standardowa funkcja tomap. Zamiast używać tomap można napisać własną funkcję. Na przykład możemy chcieć zmienić nazwę pola name, nie przesyłać pola code i dodać pole komentarz:

local function unflatten_goods(tuple)
    local obj = {}
    obj.id = tuple.id
    obj.goods_name = tuple.name
    obj.comment = 'jakiś komentarz'
    obj.row_ver = tuple.row_ver
    return obj
end

Rozmiar strony wydawanych danych (liczba rekordów w jednej porcji) określany jest przez zmienną page_size. W przykładzie wartość page_size wynosi 5. W rzeczywistym programie rozmiar strony ma zwykle większe znaczenie. Zależy od średniego rozmiaru krotki przestrzeni. Optymalny rozmiar strony można dobrać empirycznie, mierząc czas transferu danych. Im większy rozmiar strony, tym mniejsza liczba rund pomiędzy stroną wysyłającą a odbierającą. Można w ten sposób skrócić całkowity czas przesyłania zmian. Jednak przy zbyt dużym rozmiarze strony zbyt długo zajmujemy serwer serializacją zbioru. W rezultacie mogą wystąpić opóźnienia w przetwarzaniu innych żądań, które dotarły do serwera. Parametr page_size można załadować z pliku konfiguracyjnego. Dla każdej przesyłanej przestrzeni można ustawić swoją wartość. Przy tym dla większości przestrzeni odpowiednia może być wartość domyślna (na przykład 100).

Wykonajmy funkcję get_goods:

tarantool> get_goods(0)

---
- - row_ver: 1
    code: 123
    name: długopis
    id: 1
  - row_ver: 2
    code: 321
    name: ołówek
    id: 2
  - row_ver: 3
    code: 100
    name: pędzel
    id: 3
  - row_ver: 4
    code: 456
    name: akwarela
    id: 4
  - row_ver: 5
    code: 101
    name: album
    id: 5
...

Weźmy wartość pola wersja_wiersza z ostatniego wiersza i ponownie wywołajmy funkcję:

tarantool> get_goods(5)

---
- - row_ver: 6
    code: 800
    name: zeszyt
    id: 6
  - row_ver: 7
    code: 531
    name: gumka
    id: 7
  - row_ver: 8
    code: 135
    name: linijka
    id: 8
...

I jeszcze raz:

tarantool> get_goods(8)
---
- []
...

Jak widzimy, przy takim użyciu funkcja stronnicowo zwraca wszystkie zapisy przestrzeni goods. Za ostatnią stroną następuje pusta kwerenda.

Wprowadźmy zmiany w przestrzeni:

box.space.goods:update(4, {{'=', 6, 'zeszyt'}})
box.space.goods:insert{nil, 'klips', 234}
box.space.goods:insert{nil, 'folder', 432}

Zmieniliśmy wartość pola name dla jednego rekordu i dodaliśmy dwa nowe rekordy.

Powtórzmy ostatnie wywołanie funkcji:

tarantool> get_goods(8)
---



- - row_ver: 9
    code: 800
    name: zeszyt
    id: 6
  - row_ver: 10
    code: 234
    name: klips
    id: 9
  - row_ver: 11
    code: 432
    name: folder
    id: 10
...

Funkcja zwróciła zmienione oraz dodane zapisy. W ten sposób funkcja get_goods umożliwia uzyskiwanie danych, które zmieniły się od chwili ostatniego wywołania, co stanowi podstawę omawianego sposobu replikacji.

Zostawimy wydawanie wyników przez HTTP w formie JSON poza zakresem niniejszego artykułu. Można o tym przeczytać tutaj: https://habr.com/ru/company/mailru/blog/272141/

Implementacja części klienckiej/slave

Rozważmy, jak wygląda implementacja strony odbierającej. Stworzymy na stronie odbierającej przestrzeń do przechowywania załadowanych danych:

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
})

Struktura przestrzeni przypomina strukturę przestrzeni w źródle. Ale ponieważ nie zamierzamy przesyłać uzyskanych danych gdzie indziej, kolumna wersja_wiersza w przestrzeni odbiorcy jest nieobecna. W polu id będą zapisywane identyfikatory źródła. Dlatego po stronie odbiorcy nie ma potrzeby, aby był autoinkrementalny.

Ponadto potrzebujemy przestrzeni do przechowywania wartości wersja_wiersza:

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
})

Dla każdej ładowanej przestrzeni (pole space_name) będziemy tu przechowywać ostatnią załadowaną wartość wersja_wiersza (pole value). Jako klucz główny występuje kolumna space_name.

Stworzymy funkcję do ładowania danych przestrzeni goods przez HTTP. W tym celu potrzebujemy biblioteki, która realizuje klienta HTTP. Następna linia ładuje bibliotekę i tworzy instancję klienta HTTP:

local http_client = require('http.client').new()

Potrzebujemy również biblioteki do deserializacji json:

local json = require('json')

To wystarczy do stworzenia funkcji ładującej dane:

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

Funkcja wykonuje żądanie HTTP pod adresem url, przekazuje w nim wersja_wiersza jako parametr i zwraca zdeserializowany wynik zapytania.

Funkcja zapisywania uzyskanych danych wygląda następująco:

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

Pętla zapisywania danych w przestrzeni goods umieszczona jest w transakcji (do tego używana jest funkcja box.atomic) aby zmniejszyć liczbę operacji z dyskiem.

Na końcu funkcję synchronizacji lokalnej przestrzeni goods ze źródłem można zaimplementować w ten sposób:

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

Najpierw odczytujemy wcześniej zapisane wartość wersja_wiersza dla przestrzeni goods. Jeśli go nie ma (pierwsza sesja wymiany), to bierzemy jako wersja_wiersza zero. Then, in the loop, we perform page-by-page loading of modified data from the source at the specified url. At each iteration, we save the retrieved data in the corresponding local space and update the value wersja_wiersza (in the space wersja_wiersza and in the variable wersja_wiersza) — we take the value wersja_wiersza from the last line of the loaded data.

To protect against accidental looping (in case of a program error), the loop while can be replaced with for:

for _ = 1, max_req do ...

As a result of the function execution sync_goods the space goods in the receiver will contain the latest versions of all records from the space goods in the source.

It is obvious that this method cannot be used to transmit data deletion. If such a need exists, a deletion marker can be used. We add to the space goods a boolean field is_deleted and instead of physically deleting a record, we use logical deletion — we set the value of the field is_deleted na wartość true. Sometimes, instead of a boolean field is_deleted it is more convenient to use a field deletedthat stores the date-time of logical deletion of the record. After logical deletion is performed, the marked record for deletion will be transferred from the source to the receiver (according to the logic discussed above).

The sequence wersja_wiersza can be used to transfer data from other spaces: there is no need to create a separate sequence for each space being transferred.

We have considered an effective method of high-level data replication in applications using the Tarantool DBMS.

Wnioski

  1. The Tarantool DBMS is an attractive, promising product for creating high-load applications.
  2. High-level data replication has several advantages over low-level replication.
  3. The method of high-level replication discussed in the article allows minimizing the amount of data transmitted by only transferring those records that have changed since the last exchange session.

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster