Replikacija visokog nivoa u Tarantool DBMS-u

Poštovani, pravim aplikacije za DBMS Tarantool je platforma koju je razvila Mail.ru Group koja kombinuje DBMS visokih performansi i server aplikacija na jeziku Lua. Velika brzina rješenja baziranih na Tarantool-u postignuta je, posebno, zahvaljujući podršci za in-memory mod DBMS-a i mogućnosti izvršavanja poslovne logike aplikacije u jednom adresnom prostoru sa podacima. U isto vrijeme, postojanost podataka je osigurana korištenjem ACID transakcija (WAL dnevnik se održava na disku). Tarantool ima ugrađenu podršku za replikaciju i dijeljenje. Počevši od verzije 2.1, podržani su upiti u SQL jeziku. Tarantool je otvorenog koda i licenciran pod Simplified BSD licencom. Postoji i komercijalna Enterprise verzija.

Replikacija visokog nivoa u Tarantool DBMS-u
Osjeti moć! (…aka uživajte u nastupu)

Sve navedeno čini Tarantool atraktivnom platformom za kreiranje aplikacija za baze podataka velikog opterećenja. U takvim aplikacijama često postoji potreba za replikacijom podataka.

Kao što je gore spomenuto, Tarantool ima ugrađenu replikaciju podataka. Princip njegovog rada je da uzastopno izvršava na replikama sve transakcije sadržane u glavnom dnevniku (WAL). Obično takva replikacija (dalje ćemo je zvati nizak nivo) se koristi za osiguranje tolerancije grešaka u aplikaciji i/ili za raspodjelu opterećenja čitanja između čvorova klastera.

Replikacija visokog nivoa u Tarantool DBMS-u
Rice. 1. Replikacija unutar klastera

Primjer alternativnog scenarija bi bio prijenos podataka kreiranih u jednoj bazi podataka u drugu bazu podataka radi obrade/nadgledanja. U potonjem slučaju, pogodnije rješenje može biti korištenje visoki nivo replikacija - replikacija podataka na razini poslovne logike aplikacije. One. Ne koristimo gotova rješenja ugrađena u DBMS, već sami implementiramo replikaciju unutar aplikacije koju razvijamo. Ovaj pristup ima i prednosti i nedostatke. Hajde da navedemo prednosti.

1. Uštede u saobraćaju:

  • Ne možete prenijeti sve podatke, već samo dio (na primjer, možete prenijeti samo neke tabele, neke njihove kolone ili zapise koji ispunjavaju određeni kriterijum);
  • Za razliku od replikacije niskog nivoa, koja se izvodi kontinuirano u asinhronom (implementiranom u trenutnoj verziji Tarantool-a - 1.10) ili sinhronom (koji će se implementirati u narednim verzijama Tarantool-a) načinu, replikacija na visokom nivou može se izvoditi u sesijama (tj. aplikacija prvo sinhronizuje podatke - podatke sesije razmene, zatim dolazi do pauze u replikaciji, nakon čega dolazi do sledeće sesije razmene itd.);
  • ako se zapis promijenio nekoliko puta, možete prenijeti samo njegovu najnoviju verziju (za razliku od replikacije niskog nivoa, u kojoj će se sve promjene napravljene na masteru reproducirati uzastopno na replikama).

2. Nema poteškoća sa implementacijom HTTP razmene, koja vam omogućava da sinhronizujete udaljene baze podataka.

Replikacija visokog nivoa u Tarantool DBMS-u
Rice. 2. Replikacija preko HTTP-a

3. Strukture baze podataka između kojih se prenose podaci ne moraju biti iste (štaviše, u opštem slučaju čak je moguće koristiti različite DBMS, programske jezike, platforme itd.).

Replikacija visokog nivoa u Tarantool DBMS-u
Rice. 3. Replikacija u heterogenim sistemima

Loša strana je u tome što je u prosjeku programiranje teže/skuplje od konfiguracije i umjesto prilagođavanja ugrađene funkcionalnosti, morat ćete implementirati vlastitu.

Ako su u vašoj situaciji gore navedene prednosti ključne (ili su neophodan uslov), onda ima smisla koristiti replikaciju na visokom nivou. Pogledajmo nekoliko načina za implementaciju replikacije podataka visokog nivoa u Tarantool DBMS.

Minimizacija saobraćaja

Dakle, jedna od prednosti replikacije na visokom nivou je ušteda saobraćaja. Da bi se ova prednost u potpunosti iskoristila, potrebno je minimizirati količinu podataka koji se prenose tokom svake sesije razmjene. Naravno, ne treba zaboraviti da na kraju sesije prijemnik podataka mora biti sinkroniziran sa izvorom (barem za onaj dio podataka koji je uključen u replikaciju).

Kako minimizirati količinu podataka koji se prenose tokom replikacije na visokom nivou? Jednostavno rješenje bi moglo biti odabir podataka prema datumu i vremenu. Da biste to učinili, možete koristiti polje datum-vrijeme koje već postoji u tabeli (ako postoji). Na primjer, dokument "narudžbe" može imati polje "potrebno vrijeme izvršenja naloga" - delivery_time. Problem kod ovog rješenja je što vrijednosti u ovom polju ne moraju biti u redoslijedu koji odgovara kreiranju naloga. Dakle, ne možemo zapamtiti maksimalnu vrijednost polja delivery_time, prenešene tokom prethodne sesije razmene, a tokom sledeće sesije razmene izaberite sve zapise sa višom vrednošću polja delivery_time. Zapisi sa nižom vrijednošću polja su možda dodani između sesija razmjene delivery_time. Takođe, poredak je mogao da pretrpi promene, koje ipak nisu uticale na teren delivery_time. U oba slučaja, promjene se neće prenijeti sa izvora na odredište. Da bismo riješili ove probleme, morat ćemo prenijeti podatke koji se "preklapaju". One. u svakoj sesiji razmjene prenosit ćemo sve podatke sa vrijednošću polja delivery_time, prelazeći neku tačku u prošlosti (na primjer, N sati od trenutnog trenutka). Međutim, očigledno je da je za velike sisteme ovaj pristup veoma redundantni i može smanjiti uštede u saobraćaju kojima težimo na ništa. Osim toga, tablica koja se prenosi možda nema polje povezano s datumom i vremenom.

Drugo rješenje, složenije u smislu implementacije, je potvrda prijema podataka. U tom slučaju, tokom svake sesije razmjene, prenose se svi podaci čiji prijem nije potvrdio primalac. Da biste ovo implementirali, morat ćete dodati Boolean stupac izvornoj tablici (na primjer, is_transferred). Ako primalac potvrdi prijem zapisa, odgovarajuće polje uzima vrijednost true, nakon čega unos više nije uključen u razmjene. Ova opcija implementacije ima sljedeće nedostatke. Prvo, za svaki preneseni zapis, mora se generirati i poslati potvrda. Grubo govoreći, ovo bi se moglo uporediti sa udvostručavanjem količine prenesenih podataka i dovesti do udvostručavanja broja povratnih putovanja. Drugo, ne postoji mogućnost slanja istog zapisa na više primatelja (prvi primalac će potvrditi prijem za sebe i za sve ostale).

Metoda koja nema gore navedene nedostatke je dodavanje stupca prenesenoj tabeli kako bi se pratile promjene u njenim redovima. Takva kolona može biti tipa datum-vrijeme i mora biti postavljena/ažurirana od strane aplikacije na trenutno vrijeme svaki put kada se zapisi dodaju/promijene (atomski sa dodavanjem/promjenom). Kao primjer, nazovimo kolonu update_time. Čuvanjem maksimalne vrijednosti polja ove kolone za prenesene zapise, možemo započeti sljedeću sesiju razmjene sa ovom vrijednošću (odaberite zapise sa vrijednošću polja update_time, premašuje prethodno pohranjenu vrijednost). Problem s potonjim pristupom je što se promjene podataka mogu dogoditi u serijama. Kao rezultat vrijednosti polja u koloni update_time možda nije jedinstven. Stoga se ovaj stupac ne može koristiti za porcionirani (stranicu po stranicu) izlaz podataka. Da biste prikazali podatke stranicu po stranicu, morat ćete izmisliti dodatne mehanizme koji će najvjerovatnije imati vrlo nisku efikasnost (na primjer, preuzimanje iz baze podataka svih zapisa sa vrijednošću update_time veći od datog i proizvodi određeni broj zapisa, počevši od određenog pomaka od početka uzorka).

Možete poboljšati efikasnost prijenosa podataka blagim poboljšanjem prethodnog pristupa. Da bismo to učinili, koristit ćemo cjelobrojni tip (dugi cijeli broj) kao vrijednosti polja stupca za praćenje promjena. Nazovimo kolonu row_ver. Vrijednost polja ove kolone i dalje mora biti postavljena/ažurirana svaki put kada se zapis kreira/modi. Ali u ovom slučaju, polju neće biti dodijeljen trenutni datum-vrijeme, već vrijednost nekog brojača, uvećana za jedan. Kao rezultat toga, kolona row_ver će sadržavati jedinstvene vrijednosti i može se koristiti ne samo za prikaz "delta" podataka (podaci dodani/promijenjeni od kraja prethodne sesije razmjene), već i za jednostavno i efikasno rastavljanje na stranice.

Posljednja predložena metoda minimiziranja količine podataka prenesenih u okviru replikacije na visokom nivou čini mi se najoptimalnijim i univerzalnim. Pogledajmo to detaljnije.

Prenošenje podataka pomoću brojača verzija reda

Implementacija serverskog/master dijela

U MS SQL Serveru postoji poseban tip kolone za implementaciju ovog pristupa - rowversion. Svaka baza podataka ima brojač koji se povećava za jedan svaki put kada se zapis doda/promijeni u tablici koja ima stupac poput rowversion. Vrijednost ovog brojača se automatski dodjeljuje polju ove kolone u dodanom/promijenjenom zapisu. Tarantool DBMS nema sličan ugrađeni mehanizam. Međutim, u Tarantoolu nije teško implementirati ga ručno. Pogledajmo kako se to radi.

Prvo, malo terminologije: tabele u Tarantoolu se zovu razmaci, a zapisi se zovu tuple. U Tarantoolu možete kreirati sekvence. Sekvence nisu ništa drugo do imenovani generatori uređenih cjelobrojnih vrijednosti. One. to je upravo ono što nam je potrebno za naše potrebe. U nastavku ćemo kreirati takav niz.

Prije izvođenja bilo koje operacije baze podataka u Tarantool-u, morate pokrenuti sljedeću naredbu:

box.cfg{}

Kao rezultat toga, Tarantool će početi pisati snimke baze podataka i dnevnike transakcija u trenutni direktorij.

Kreirajmo niz row_version:

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

Opcija if_not_exists dozvoljava da se skripta za kreiranje izvrši više puta: ako objekat postoji, Tarantool neće pokušati da ga ponovo kreira. Ova opcija će se koristiti u svim narednim DDL naredbama.

Napravimo prostor kao primjer.

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

Ovdje postavljamo ime prostora (goods), imena polja i njihove vrste.

Polja sa automatskim povećanjem u Tarantoolu se također kreiraju pomoću sekvenci. Kreirajmo primarni ključ koji se automatski povećava po polju 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 podržava nekoliko tipova indeksa. Najčešće korišćeni indeksi su tipovi TREE i HASH, koji se zasnivaju na strukturama koje odgovaraju imenu. TREE je najsvestraniji tip indeksa. Omogućava vam da dohvatite podatke na organizovan način. Ali za odabir jednakosti, HASH je prikladniji. Shodno tome, preporučljivo je koristiti HASH za primarni ključ (što smo i uradili).

Za korištenje kolone row_ver da biste prenijeli promijenjene podatke, trebate povezati vrijednosti sekvence za polja ove kolone row_ver. Ali za razliku od primarnog ključa, vrijednost polja stupca row_ver treba povećati za jedan ne samo prilikom dodavanja novih zapisa, već i prilikom promjene postojećih. Za ovo možete koristiti okidače. Tarantool ima dvije vrste okidača prostora: before_replace и on_replace. Okidači se aktiviraju kad god se podaci u prostoru promijene (za svaki tuple na koji promjene utiču, pokreće se funkcija okidača). Za razliku od on_replace, before_replace-okidači vam omogućavaju da modificirate podatke tuple-a za koji se okidač izvršava. U skladu s tim, posljednji tip okidača nam odgovara.

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

Sljedeći okidač zamjenjuje vrijednost polja row_ver pohranjeni tuple na sljedeću vrijednost niza row_version.

Da bi mogli izvući podatke iz svemira goods po koloni row_ver, napravimo indeks:

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

Vrsta indeksa - stablo (TREE), jer morat ćemo izdvojiti podatke u rastućem redoslijedu vrijednosti u koloni row_ver.

Dodajmo malo podataka u prostor:

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}

Jer Prvo polje je brojač koji se automatski povećava; umjesto toga prosljeđujemo nulu. Tarantool će automatski zamijeniti sljedeću vrijednost. Slično, kao vrijednost polja kolone row_ver možete proslijediti nil - ili uopće ne specificirati vrijednost, jer ova kolona zauzima posljednju poziciju u prostoru.

Provjerimo rezultat umetanja:

tarantool> box.space.goods:select()
---
- - [1, 'pen', 123, 1]
  - [2, 'pencil', 321, 2]
  - [3, 'brush', 100, 3]
  - [4, 'watercolour', 456, 4]
  - [5, 'album', 101, 5]
  - [6, 'notebook', 800, 6]
  - [7, 'rubber', 531, 7]
  - [8, 'ruler', 135, 8]
...

Kao što vidite, prvo i posljednje polje se popunjavaju automatski. Sada će biti lako napisati funkciju za učitavanje stranica po stranicu promjena prostora 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

Funkcija uzima kao parametar vrijednost row_ver, počevši od kojeg je potrebno izbaciti promjene, i vraća dio promijenjenih podataka.

Uzorkovanje podataka u Tarantoolu se vrši preko indeksa. Funkcija get_goods koristi iterator po indeksu row_ver za primanje izmijenjenih podataka. Tip iteratora je GT (Greater Than, larger than). To znači da će iterator uzastopno prelaziti vrijednosti indeksa počevši od proslijeđenog ključa (vrijednost polja row_ver).

Iterator vraća tuple. Da bi se naknadno mogli prenositi podaci putem HTTP-a, potrebno je konvertirati tuple u strukturu pogodnu za naknadnu serijalizaciju. Primjer koristi standardnu ​​funkciju za ovo tomap. Umjesto korištenja tomap možete napisati svoju vlastitu funkciju. Na primjer, možda bismo željeli preimenovati polje name, ne prolazi kroz polje code i dodajte polje comment:

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

Veličina stranice izlaznih podataka (broj zapisa u jednom dijelu) određena je varijablom page_size. U primjeru vrijednost page_size je 5. U pravom programu, veličina stranice je obično važnija. Zavisi od prosječne veličine prostornog tuple-a. Optimalna veličina stranice može se odrediti empirijski mjerenjem vremena prijenosa podataka. Što je veća veličina stranice, manji je broj povratnih putovanja između strane koja šalje i primaoca. Na ovaj način možete smanjiti ukupno vrijeme za preuzimanje promjena. Međutim, ako je veličina stranice prevelika, predugo ćemo provesti na serveru koji serializira uzorak. Kao rezultat toga, može doći do kašnjenja u obradi drugih zahtjeva koji dolaze na server. Parametar page_size može se učitati iz konfiguracijske datoteke. Za svaki preneseni prostor možete postaviti vlastitu vrijednost. Međutim, za većinu prostora zadana vrijednost (na primjer, 100) može biti prikladna.

Izvršimo funkciju get_goods:

tarantool> get_goods(0)

---
- - row_ver: 1
    code: 123
    name: pen
    id: 1
  - row_ver: 2
    code: 321
    name: pencil
    id: 2
  - row_ver: 3
    code: 100
    name: brush
    id: 3
  - row_ver: 4
    code: 456
    name: watercolour
    id: 4
  - row_ver: 5
    code: 101
    name: album
    id: 5
...

Uzmimo vrijednost polja row_ver iz zadnjeg reda i ponovo pozovite funkciju:

tarantool> get_goods(5)

---
- - row_ver: 6
    code: 800
    name: notebook
    id: 6
  - row_ver: 7
    code: 531
    name: rubber
    id: 7
  - row_ver: 8
    code: 135
    name: ruler
    id: 8
...

Ponovo:

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

Kao što vidite, kada se koristi na ovaj način, funkcija vraća sve zapise o prostoru stranicu po stranicu goods. Poslednju stranicu prati prazan izbor.

Učinimo promjene u prostoru:

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

Promijenili smo vrijednost polja name za jedan unos i dodala dva nova unosa.

Ponovimo zadnji poziv funkcije:

tarantool> get_goods(8)
---



- - row_ver: 9
    code: 800
    name: copybook
    id: 6
  - row_ver: 10
    code: 234
    name: clip
    id: 9
  - row_ver: 11
    code: 432
    name: folder
    id: 10
...

Funkcija je vratila izmijenjene i dodane zapise. Dakle, funkcija get_goods omogućava vam da primite podatke koji su se promijenili od njegovog posljednjeg poziva, što je osnova metode replikacije koja se razmatra.

Izdavanje rezultata putem HTTP-a u obliku JSON-a ostavit ćemo izvan okvira ovog članka. O ovome možete pročitati ovdje: https://habr.com/ru/company/mailru/blog/272141/

Implementacija klijent/slave dio

Pogledajmo kako izgleda implementacija na strani koja prima. Napravimo prostor na strani primaoca za pohranjivanje preuzetih podataka:

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 prostora podsjeća na strukturu prostora u izvoru. Ali pošto primljene podatke nećemo prenositi nigde drugde, kolonu row_ver nije u prostoru primaoca. Na terenu id izvorni identifikatori će biti snimljeni. Stoga, na strani prijemnika nema potrebe da se on automatski povećava.

Osim toga, potreban nam je prostor za spremanje vrijednosti 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
})

Za svaki učitani prostor (polje space_name) ovdje ćemo sačuvati posljednju učitanu vrijednost row_ver (polje value). Kolona se ponaša kao primarni ključ space_name.

Kreirajmo funkciju za učitavanje podataka o prostoru goods putem HTTP-a. Da bismo to uradili, potrebna nam je biblioteka koja implementira HTTP klijenta. Sljedeća linija učitava biblioteku i instancira HTTP klijenta:

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

Također nam je potrebna biblioteka za json deserializaciju:

local json = require('json')

Ovo je dovoljno za kreiranje funkcije učitavanja podataka:

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

Funkcija izvršava HTTP zahtjev na url adresu i šalje ga row_ver kao parametar i vraća deserializirani rezultat zahtjeva.

Funkcija za spremanje primljenih podataka izgleda ovako:

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

Ciklus pohranjivanja podataka u prostor goods stavljen u transakciju (funkcija se koristi za to box.atomic) za smanjenje broja operacija na disku.

Konačno, funkcija sinkronizacije lokalnog prostora goods sa izvorom to možete implementirati ovako:

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

Prvo čitamo prethodno sačuvanu vrijednost row_ver za prostor goods. Ako nedostaje (prva sesija razmjene), onda to uzimamo kao row_ver nula. Sljedeće u ciklusu vršimo preuzimanje stranica po stranicu promijenjenih podataka iz izvora na navedenom URL-u. Na svakoj iteraciji primljene podatke spremamo u odgovarajući lokalni prostor i ažuriramo vrijednost row_ver (u svemiru row_ver i u varijablu row_ver) - uzeti vrijednost row_ver iz zadnje linije učitanih podataka.

Za zaštitu od slučajnog petlje (u slučaju greške u programu), petlja while može se zamijeniti sa for:

for _ = 1, max_req do ...

Kao rezultat izvršavanja funkcije sync_goods prostor goods prijemnik će sadržavati najnovije verzije svih svemirskih zapisa goods u izvoru.

Očigledno, brisanje podataka se ne može emitovati na ovaj način. Ako postoji takva potreba, možete koristiti oznaku za brisanje. Dodajte u prostor goods boolean field is_deleted i umjesto fizičkog brisanja zapisa, koristimo logičko brisanje - postavljamo vrijednost polja is_deleted u značenje true. Ponekad umjesto logičkog polja is_deleted zgodnije je koristiti polje deleted, koji pohranjuje datum i vrijeme logičkog brisanja zapisa. Nakon izvršenja logičkog brisanja, zapis označen za brisanje će se prenijeti od izvora do odredišta (prema gore opisanoj logici).

Slijed row_ver može se koristiti za prijenos podataka iz drugih prostora: nema potrebe za kreiranjem zasebnog niza za svaki preneseni prostor.

Razmotrili smo efikasan način replikacije podataka visokog nivoa u aplikacijama koje koriste Tarantool DBMS.

nalazi

  1. Tarantool DBMS je atraktivan, obećavajući proizvod za kreiranje aplikacija visokog opterećenja.
  2. Replikacija podataka na visokom nivou ima brojne prednosti u odnosu na replikaciju niskog nivoa.
  3. Metoda replikacije visokog nivoa o kojoj se govori u članku omogućava vam da minimizirate količinu prenesenih podataka prenosom samo onih zapisa koji su se promijenili od posljednje sesije razmjene.

izvor: www.habr.com

Dodajte komentar