Tere, ma loon rakendusi DBMS-i jaoks
Tundke jõudu! (… ehk naudi etendust)
Kõik eelnev muudab Tarantooli atraktiivseks platvormiks andmebaasidega töötavate suure koormusega rakenduste loomiseks. Sellistes rakendustes on sageli vajadus andmete replikatsiooni järele.
Nagu eespool mainitud, on Tarantoolil sisseehitatud andmete replikatsioon. Selle toimimise põhimõte on sooritada koopiates järjestikku kõik põhilogis (WAL) sisalduvad tehingud. Tavaliselt selline replikatsioon (nimetame seda edasi madalal tasemel) kasutatakse rakenduse veataluvuse tagamiseks ja/või lugemiskoormuse jaotamiseks klastri sõlmede vahel.
Riis. 1. Replikatsioon klastris
Alternatiivse stsenaariumi näide oleks ühes andmebaasis loodud andmete ülekandmine töötlemiseks/seireks teise andmebaasi. Viimasel juhul võib mugavam lahendus olla kasutamine kõrge tase replikatsioon – andmete replikatsioon rakenduse äriloogika tasemel. Need. Me ei kasuta DBMS-i sisseehitatud valmislahendust, vaid rakendame arendatava rakenduse sees replikatsiooni iseseisvalt. Sellel lähenemisviisil on nii eeliseid kui ka puudusi. Loetleme eelised.
1. Liikluse kokkuhoid:
- Te ei saa edastada kõiki andmeid, vaid ainult osa neist (näiteks saate edastada ainult mõned tabelid, mõned nende veerud või kirjed, mis vastavad teatud kriteeriumile);
- Erinevalt madala taseme replikatsioonist, mida teostatakse pidevalt asünkroonses (rakendatud Tarantooli praeguses versioonis - 1.10) või sünkroonses (rakendatud Tarantooli järgmistes versioonides) režiimis, saab kõrgetasemelist replikatsiooni teostada seansside kaupa (st rakendus sünkroonib esmalt andmed – vahetusseansi andmed, seejärel tekib replikatsioonis paus, mille järel toimub järgmine vahetusseanss jne);
- kui kirjet on mitu korda muudetud, saate üle kanda ainult selle uusima versiooni (erinevalt madala taseme replikatsioonist, kus kõik põhifailis tehtud muudatused esitatakse koopiatel järjestikku).
2. HTTP-vahetuse juurutamine, mis võimaldab sünkroonida kaugandmebaase, ei tekita raskusi.
Riis. 2. Replikatsioon HTTP kaudu
3. Andmebaasistruktuurid, mille vahel andmeid edastatakse, ei pea olema samad (pealegi on üldjuhul võimalik kasutada isegi erinevaid DBMS-e, programmeerimiskeeli, platvorme jne).
Riis. 3. Replikatsioon heterogeensetes süsteemides
Negatiivne külg on see, et programmeerimine on keskmiselt keerulisem/kulukam kui konfigureerimine ning sisseehitatud funktsionaalsuse kohandamise asemel tuleb ise juurutada.
Kui teie olukorras on ülaltoodud eelised üliolulised (või on vajalik tingimus), siis on mõistlik kasutada kõrgetasemelist replikatsiooni. Vaatame mitmeid viise kõrgetasemelise andmete replikatsiooni rakendamiseks Tarantool DBMS-is.
Liikluse minimeerimine
Seega on üks kõrgetasemelise replikatsiooni eeliseid liikluse kokkuhoid. Selle eelise täielikuks realiseerimiseks on vaja minimeerida iga vahetusseansi jooksul edastatavate andmete hulka. Muidugi ei tasu unustada, et seansi lõpus tuleb andmevastuvõtja sünkroniseerida allikaga (vähemalt selle osa andmete puhul, mis on seotud replikatsiooniga).
Kuidas minimeerida kõrgetasemelise replikatsiooni käigus edastatavate andmete hulka? Lihtne lahendus võiks olla andmete valimine kuupäeva ja kellaaja järgi. Selleks saab kasutada tabelis juba olemasolevat kuupäeva-kellaaja välja (kui see on olemas). Näiteks võib "tellimuse" dokumendil olla väli "nõutav tellimuse täitmise aeg" - delivery_time
. Selle lahenduse probleem seisneb selles, et selle välja väärtused ei pea olema järjekorras, mis vastab tellimuste loomisele. Seega me ei mäleta välja maksimaalset väärtust delivery_time
, mis edastati eelmise vahetusseansi ajal, ja valige järgmise vahetusseansi ajal kõik kõrgema väljaväärtusega kirjed delivery_time
. Vahetusseansside vahel võib olla lisatud madalama väljaväärtusega kirjeid delivery_time
. Samuti võis järjekord läbi viia muudatusi, mis siiski väljakut ei puudutanud delivery_time
. Mõlemal juhul ei kanta muudatusi allikast sihtkohta. Nende probleemide lahendamiseks peame edastama andmed "kattuvad". Need. igal vahetusseansil edastame kõik andmed koos välja väärtusega delivery_time
, ületades mõnda minevikupunkti (näiteks N tundi praegusest hetkest). Siiski on ilmne, et suurte süsteemide puhul on see lähenemisviis väga üleliigne ja võib vähendada liikluse kokkuhoidu, mille poole püüdleme. Lisaks ei pruugi teisaldataval tabelis olla välja, mis on seotud kuupäeva ja kellaajaga.
Teine lahendus, mis on rakendamise poolest keerulisem, on andmete vastuvõtmise kinnitamine. Sellisel juhul edastatakse iga vahetusseansi ajal kõik andmed, mille kättesaamist ei ole saaja kinnitanud. Selle rakendamiseks peate lähtetabelisse lisama Boole'i veeru (näiteks is_transferred
). Kui vastuvõtja kinnitab kirje kättesaamist, võtab vastav väli väärtuse true
, mille järel kanne enam vahetustega ei osale. Sellel rakendusvalikul on järgmised puudused. Esiteks tuleb iga ülekantud kirje kohta genereerida ja saata kinnitus. Jämedalt öeldes võib see olla võrreldav edastatavate andmete kahekordistamisega ja edasi-tagasi reiside arvu kahekordistamisega. Teiseks puudub võimalus saata sama kirjet mitmele vastuvõtjale (esimene vastuvõtja kinnitab kättesaamise nii endale kui ka kõigile teistele).
Meetod, millel pole ülaltoodud puudusi, on veeru lisamine ülekantud tabelisse, et jälgida selle ridade muutusi. Selline veerg võib olla kuupäeva ja kellaaja tüüpi ning seda peab rakendus seadistama/värskendama praegusele kellaajale iga kord, kui kirjeid lisatakse/muuttakse (koos lisamise/muudatusega). Näitena nimetame veergu update_time
. Salvestades selle veeru maksimaalse väljaväärtuse ülekantud kirjete jaoks, saame alustada järgmist vahetusseanssi selle väärtusega (valige välja väärtusega kirjed update_time
, ületades varem salvestatud väärtuse). Viimase lähenemisviisi probleem seisneb selles, et andmete muutmine võib toimuda partiidena. Veerus olevate välja väärtuste tulemusena update_time
ei pruugi olla ainulaadne. Seega ei saa seda veergu kasutada jaotatud (lehekülgede kaupa) andmete väljastamiseks. Andmete kuvamiseks lehekülgede kaupa peate leiutama lisamehhanisme, millel on tõenäoliselt väga madal efektiivsus (näiteks kõigi väärtusega kirjete otsimine andmebaasist update_time
kõrgem kui antud ja toodab teatud arvu kirjeid, alustades teatud nihkest valimi algusest).
Andmeedastuse tõhusust saate parandada, kui eelmist lähenemist pisut täiustada. Selleks kasutame muudatuste jälgimiseks veeruvälja väärtustena täisarvu tüüpi (pikk täisarv). Paneme veerule nime row_ver
. Selle veeru välja väärtust tuleb ikkagi määrata/värskendada iga kord, kui kirje luuakse/muuttakse. Kuid sel juhul ei omistata väljale praegust kuupäeva-kellaaega, vaid mõne loenduri väärtust, mida suurendatakse ühe võrra. Selle tulemusena veerg row_ver
sisaldab unikaalseid väärtusi ja seda saab kasutada mitte ainult "delta" andmete kuvamiseks (andmed on lisatud/muudetud alates eelmise vahetusseansi lõpust), vaid ka lihtsalt ja tõhusalt lehtedeks jaotamiseks.
Viimane pakutud meetod kõrgetasemelise replikatsiooni raames edastatavate andmete hulga minimeerimiseks tundub mulle kõige optimaalsem ja universaalsem. Vaatame seda üksikasjalikumalt.
Andmete edastamine reaversioonide loenduri abil
Serveri/peaosa juurutamine
MS SQL Serveris on selle lähenemisviisi rakendamiseks spetsiaalne veerutüüp - rowversion
. Igal andmebaasil on loendur, mis suureneb ühe võrra iga kord, kui kirje lisatakse/muuttakse tabelis, millel on veerg nagu rowversion
. Selle loenduri väärtus määratakse lisatud/muudetud kirjes automaatselt selle veeru väljale. Tarantooli DBMS-il pole sarnast sisseehitatud mehhanismi. Tarantoolis pole seda aga käsitsi keeruline rakendada. Vaatame, kuidas seda tehakse.
Esiteks natuke terminoloogiat: Tarantooli tabeleid nimetatakse tühikuteks ja kirjeid korteežideks. Tarantoolis saate luua järjestusi. Jadad pole midagi muud kui järjestatud täisarvude väärtuste nimelised generaatorid. Need. see on täpselt see, mida me oma eesmärkide saavutamiseks vajame. Allpool loome sellise jada.
Enne mis tahes andmebaasitoimingu sooritamist Tarantoolis peate käivitama järgmise käsu:
box.cfg{}
Selle tulemusena hakkab Tarantool kirjutama praegusesse kataloogi andmebaasi hetktõmmiseid ja tehingulogisid.
Loome järjestuse row_version
:
box.schema.sequence.create('row_version',
{ if_not_exists = true })
Variant if_not_exists
võimaldab luua skripti mitu korda: kui objekt on olemas, ei proovi Tarantool seda uuesti luua. Seda suvandit kasutatakse kõigis järgnevates DDL-käskudes.
Loome näitena ruumi.
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
})
Siin määrame ruumi nime (goods
), väljade nimed ja tüübid.
Tarantooli automaatse suurendamise väljad luuakse ka järjestuste abil. Loome väljade kaupa automaatselt suureneva primaarvõtme 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 toetab mitut tüüpi indekseid. Enim kasutatavad indeksid on TREE ja HASH tüübid, mis põhinevad nimele vastavatel struktuuridel. PUU on kõige mitmekülgsem indeksitüüp. See võimaldab teil andmeid organiseeritud viisil hankida. Kuid võrdseks valikuks sobib HASH paremini. Sellest lähtuvalt on soovitatav kasutada primaarvõtme jaoks HASH-i (mida me ka tegime).
Veeru kasutamiseks row_ver
muudetud andmete edastamiseks peate selle veeru väljadele siduma järjestuse väärtused row_ver
. Kuid erinevalt primaarvõtmest on veeru välja väärtus row_ver
peaks suurenema ühe võrra mitte ainult uute kirjete lisamisel, vaid ka olemasolevate muutmisel. Selleks saate kasutada käivitajaid. Tarantoolil on kahte tüüpi ruumipäästikuid: before_replace
и on_replace
. Päästikud käivitatakse alati, kui ruumis olevad andmed muutuvad (iga muudatustest mõjutatud korteeži jaoks käivitatakse trigeri funktsioon). Erinevalt on_replace
, before_replace
-triggerid võimaldavad teil muuta selle korteeži andmeid, mille jaoks triger käivitatakse. Sellest lähtuvalt sobivad meile viimast tüüpi päästikud.
box.space.goods:before_replace(function(old, new)
return box.tuple.new({new[1], new[2], new[3],
box.sequence.row_version:next()})
end)
Järgmine päästik asendab välja väärtuse row_ver
salvestatud korteež jada järgmisele väärtusele row_version
.
Et oleks võimalik kosmosest andmeid välja tõmmata goods
veeru järgi row_ver
, loome indeksi:
box.space.goods:create_index('row_ver', {
parts = { 'row_ver' },
unique = true,
type = 'TREE',
if_not_exists = true
})
Indeksi tüüp - puu (TREE
), sest peame eraldama andmed veerus olevate väärtuste kasvavas järjekorras row_ver
.
Lisame ruumi mõned andmed:
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}
Sest Esimene väli on automaatselt suurenev loendur; selle asemel edastame nulli. Tarantool asendab automaatselt järgmise väärtuse. Samamoodi veeruväljade väärtusena row_ver
võite jätta nulli või jätta väärtuse määramata, sest see veerg on ruumi viimasel kohal.
Kontrollime sisestamise tulemust:
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]
...
Nagu näete, täidetakse esimene ja viimane väli automaatselt. Nüüd on lihtne kirjutada funktsiooni ruumimuudatuste lehekülgede kaupa üleslaadimiseks 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
Funktsioon võtab parameetrina väärtuse row_ver
, millest alates on vaja muudatused maha laadida, ja tagastab osa muudetud andmetest.
Andmeproovide võtmine Tarantoolis toimub indeksite kaudu. Funktsioon get_goods
kasutab indeksi järgi iteraatorit row_ver
muudetud andmete vastuvõtmiseks. Iteraatori tüüp on GT (suurem kui, suurem kui). See tähendab, et iteraator läbib järjestikku indeksi väärtusi alates edasi antud võtmest (välja väärtus row_ver
).
Iteraator tagastab kordused. Selleks, et hiljem oleks võimalik andmeid HTTP kaudu edastada, on vaja kortereid teisendada järgnevaks serialiseerimiseks sobivaks struktuuriks. Näidis kasutab selleks standardfunktsiooni tomap
. Kasutamise asemel tomap
saate kirjutada oma funktsiooni. Näiteks võiksime välja nimetada ümber name
, ärge läbige põldu code
ja lisage väli 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
Väljundandmete lehekülje suurus (kirjete arv ühes osas) määratakse muutujaga page_size
. Näites väärtus page_size
on 5. Reaalses programmis loeb lehe suurus tavaliselt rohkem. See sõltub ruumikorpuse keskmisest suurusest. Optimaalse lehe suuruse saab määrata empiiriliselt, mõõtes andmeedastusaega. Mida suurem on lehe suurus, seda väiksem on edasi-tagasi reiside arv saatva ja vastuvõtva poole vahel. Nii saate muudatuste allalaadimiseks kuluvat üldist aega lühendada. Kui aga lehe suurus on liiga suur, kulutame liiga kaua proovi serialiseerimisele. Selle tulemusena võib esineda viivitusi muude serverisse saabuvate päringute töötlemisel. Parameeter page_size
saab laadida konfiguratsioonifailist. Iga edastatava ruumi jaoks saate määrata oma väärtuse. Enamiku tühikute jaoks võib vaikeväärtus (näiteks 100) siiski sobida.
Teostame funktsiooni 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
...
Võtame välja väärtuse row_ver
viimaselt realt ja kutsuge funktsioon uuesti:
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
...
Veel kord:
tarantool> get_goods(8)
---
- []
...
Nagu näete, tagastab funktsioon sel viisil kasutamisel kõik ruumikirjed lehekülgede kaupa goods
. Viimasele lehele järgneb tühi valik.
Teeme ruumis muudatusi:
box.space.goods:update(4, {{'=', 6, 'copybook'}})
box.space.goods:insert{nil, 'clip', 234}
box.space.goods:insert{nil, 'folder', 432}
Oleme muutnud välja väärtust name
ühe kirje jaoks ja lisas kaks uut kirjet.
Kordame viimast funktsioonikutset:
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
...
Funktsioon tagastas muudetud ja lisatud kirjed. Seega funktsioon get_goods
võimaldab vastu võtta andmeid, mis on muutunud pärast viimast kõnet, mis on vaadeldava replikatsioonimeetodi aluseks.
Jätame tulemuste väljastamise HTTP kaudu JSON-vormingus käesoleva artikli reguleerimisalast välja. Selle kohta saad lugeda siit:
Kliendi/orja osa rakendamine
Vaatame, kuidas vastuvõtva poole rakendamine välja näeb. Loome vastuvõtvale poolele allalaaditud andmete salvestamiseks ruumi:
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
})
Ruumi struktuur sarnaneb allikas oleva ruumi struktuuriga. Aga kuna me ei kavatse saadud andmeid mujale edastada, siis veerg row_ver
ei ole saaja ruumis. Põllul id
allika identifikaatorid salvestatakse. Seetõttu pole vastuvõtja poolel vaja seda automaatselt suurendada.
Lisaks vajame ruumi väärtuste salvestamiseks 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
})
Iga laaditud ruumi kohta (väli space_name
) salvestame siia viimati laaditud väärtuse row_ver
(väli value
). Veerg toimib esmase võtmena space_name
.
Loome funktsiooni ruumiandmete laadimiseks goods
HTTP kaudu. Selleks vajame teeki, mis rakendab HTTP-klienti. Järgmine rida laadib raamatukogu ja loob HTTP-kliendi:
local http_client = require('http.client').new()
Vajame ka jsoni deserialiseerimiseks teeki:
local json = require('json')
Sellest piisab andmete laadimise funktsiooni loomiseks:
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
Funktsioon täidab URL-i aadressile HTTP-päringu ja saadab selle row_ver
parameetrina ja tagastab päringu deserialiseeritud tulemuse.
Vastuvõetud andmete salvestamise funktsioon näeb välja järgmine:
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
Andmete ruumi salvestamise tsükkel goods
paigutatakse tehingusse (selleks kasutatakse funktsiooni box.atomic
), et vähendada kettatoimingute arvu.
Lõpuks kohaliku ruumi sünkroonimise funktsioon goods
allikaga saate seda rakendada järgmiselt:
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
Kõigepealt loeme eelnevalt salvestatud väärtuse row_ver
ruumi pärast goods
. Kui see puudub (esimene vahetusseanss), siis võtame seda kui row_ver
null. Järgmisena laadime tsüklis muudetud andmed lehekülgede kaupa alla määratud URL-i allikast. Igal iteratsioonil salvestame saadud andmed vastavasse lokaalsesse ruumi ja värskendame väärtust row_ver
(kosmoses row_ver
ja muutujas row_ver
) – võtke väärtus row_ver
laetud andmete viimaselt realt.
Juhusliku silmuse eest kaitsmiseks (programmis esineva vea korral) loop while
saab asendada for
:
for _ = 1, max_req do ...
Funktsiooni täitmise tulemusena sync_goods
ruumi goods
vastuvõtja sisaldab kõigi kosmosekirjete uusimaid versioone goods
allikas.
Ilmselgelt ei saa andmete kustutamist sel viisil edastada. Kui selline vajadus on olemas, võite kasutada kustutamismärki. Lisa ruumi goods
boolean väli is_deleted
ja kirje füüsilise kustutamise asemel kasutame loogilist kustutamist – määrame välja väärtuse is_deleted
tähendusse true
. Mõnikord Boolean välja asemel is_deleted
mugavam on põldu kasutada deleted
, mis salvestab kirje loogilise kustutamise kuupäeva-kellaaja. Pärast loogilise kustutamise sooritamist kantakse kustutamiseks märgitud kirje allikast sihtkohta (vastavalt ülalpool käsitletud loogikale).
Järjestus row_ver
saab kasutada andmete edastamiseks teistest ruumidest: iga edastatava ruumi jaoks pole vaja luua eraldi jada.
Vaatasime tõhusat viisi kõrgetasemeliseks andmete replikatsiooniks rakendustes, mis kasutavad Tarantool DBMS-i.
Järeldused
- Tarantool DBMS on atraktiivne ja paljutõotav toode suure koormusega rakenduste loomiseks.
- Kõrgetasemelisel andmete replikatsioonil on madala tasemega võrreldes mitmeid eeliseid.
- Artiklis käsitletud kõrgetasemeline replikatsioonimeetod võimaldab minimeerida ülekantavate andmete hulka, edastades ainult need kirjed, mis on muutunud pärast viimast vahetusseanssi.
Allikas: www.habr.com