Высокаўзроўневая рэплікацыя ў СКБД Tarantool

Прывітанне, я займаюся стварэннем дадаткаў для СКБД Tarantool – гэта распрацаваная ў Mail.ru Group платформа, якая сумяшчае ў сабе высокапрадукцыйную СКБД і сервер прыкладанняў на мове Lua. Высокая хуткасць працы рашэнняў, заснаваных на Tarantool, дасягаецца ў прыватнасці за рахунак падтрымкі in-memory рэжыму СКБД і магчымасці выканання бізнэс-логікі прыкладання ў адзінай адраснай прасторы з дадзенымі. Пры гэтым забяспечваецца персістэнтнасць дадзеных з выкарыстаннем ACID-транзакцый (на дыску вядзецца WAL-часопіс). У Tarantool маецца ўбудаваная падтрымка рэплікацыі і шаліраванні. Пачынаючы з версіі 2.1, падтрымліваюцца запыты на мове SQL. Tarantool мае адчынены зыходны код і распаўсюджваецца пад ліцэнзіяй Simplified BSD. Таксама ёсць камерцыйная Enterprise-версія.

Высокаўзроўневая рэплікацыя ў СКБД Tarantool
Feel the power! (…aka enjoy the performance)

Усё пералічанае робіць Tarantool прывабнай платформай для стварэння высоканагружаных прыкладанняў, якія працуюць з БД. У такіх дадатках часта ўзнікае неабходнасць рэплікацыі дадзеных.

Як было сказана вышэй, у Tarantool ёсць убудаваная рэплікацыя дадзеных. Прынцып яе працы складаецца ў паслядоўным выкананні на рэпліках усіх транзакцый, якія змяшчаюцца ў часопісе майстра (WAL). Звычайна такая рэплікацыя (будзем далей зваць яе нізкаўзроўневай) выкарыстоўваецца для забеспячэння адмоваўстойлівасці прыкладання і / або для размеркавання нагрузкі на чытанне паміж нодамі кластара.

Высокаўзроўневая рэплікацыя ў СКБД Tarantool
Мал. 1. Рэплікацыя ўнутры кластара

Прыкладам альтэрнатыўнага сцэнара можа служыць перадача дадзеных, створаных у адной БД, у іншую БД для апрацоўкі/маніторынгу. У апошнім выпадку зручнейшым рашэннем можа апынуцца выкарыстанне. высокаўзроўневай рэплікацыі - рэплікацыі дадзеных на ўзроўні бізнес-логікі прыкладання. Г.зн. мы не выкарыстоўваем гатовае рашэнне, убудаванае ў СКБД, а сваімі сіламі рэалізуем рэплікацыю ўнутры распрацоўванага намі прыкладання. Такі падыход мае як перавагі, так і недахопы. Пералічоны плюсы.

1. Эканомія трафіку:

  • можна перадаваць не ўсе дадзеныя, а толькі іх частка (напрыклад, можна перадаваць толькі некаторыя табліцы, некаторыя іх слупкі ці запісы, якія адпавядаюць вызначанаму крытэру);
  • у адрозненне ад нізкаўзроўневай рэплікацыі, якая выконваецца бесперапынна ў асінхронным (рэалізавана ў бягучай версіі Tarantool - 1.10) або сінхронным (будзе рэалізавана ў наступных версіях Tarantool) рэжыме, высокаўзроўневую рэплікацыю можна выконваць сеансамі (г.зн. дадатак спачатку выконвае дадзенымі, затым надыходзіць паўза ў рэплікацыі, пасля якой адбываецца наступны сеанс абмену і г.д.);
  • калі запіс змяніўся некалькі разоў, можна перадаць толькі яго апошнюю версію (у адрозненне ад нізкаўзроўневай рэплікацыі, пры якой на рэпліках будуць паслядоўна прайграныя ўсе змены, зробленыя на майстру).

2. Адсутнічаюць складанасці з рэалізацыяй абмену па HTTP, што дазваляе сінхранізаваць выдаленыя БД.

Высокаўзроўневая рэплікацыя ў СКБД Tarantool
Мал. 2. Рэплікацыя па HTTP

3. Структуры БД, паміж якімі перадаюцца дадзеныя, не абавязаны быць аднолькавымі (больш за тое, у агульным выпадку магчыма нават выкарыстанне розных СКБД, моў праграмавання, платформаў і да т.п.).

Высокаўзроўневая рэплікацыя ў СКБД Tarantool
Мал. 3. Рэплікацыя ў гетэрагенных сістэмах

Мінус складаецца ў тым, што ў сярэднім праграмаванне складаней/выдатней, чым канфігураванне, і замест налады ўбудаванага функцыяналу прыйдзецца рэалізоўваць свой уласны.

Калі ў вашай сітуацыі прыведзеныя плюсы гуляюць вырашальнае значэнне (ці з'яўляюцца неабходнай умовай), тое мае сэнс выкарыстоўваць высокаўзроўневую рэплікацыю. Разгледзім некалькі спосабаў рэалізацыі высокаўзроўневай рэплікацыі дадзеных у СКБД Tarantool.

Мінімізацыя трафіку

Такім чынам, адной з пераваг высокаўзроўневай рэплікацыі з'яўляецца эканомія трафіку. Для таго каб гэтая перавага выявілася ў поўнай меры, неабходна мінімізаваць колькасць дадзеных, якія перадаюцца пры кожным сеансе абмену. Зразумела, пры гэтым не варта забываць аб тым, што ў канцы сеансу прымач дадзеных павінен быць сінхранізаваны з крыніцай (як мінімум па той частцы дадзеных, якая ўдзельнічае ў рэплікацыі).

Як жа мінімізаваць колькасць дадзеных, якія перадаюцца пры высокаўзроўневай рэплікацыі? Рашэннем "у лоб" можа быць адбор дадзеных па даце-часе. Для гэтага можна выкарыстоўваць ужо наяўнае ў табліцы поле даты-часу (калі яно ёсць). Напрыклад, у дакумента «заказ» можа быць поле «патрабаваны час выканання замовы». delivery_time. Праблема такога рашэння заключаецца ў тым, што значэння ў гэтым полі не абавязаны размяшчацца ў паслядоўнасці, якая адпавядае стварэнню заказаў. Такім чынам, мы не можам запомніць максімальнае значэнне поля delivery_time, перададзенае пры папярэднім сеансе абмену, і пры наступным сеансе абмену адабраць усе запісы з больш высокім значэннем поля delivery_time. У прамежку паміж сеансамі абмену маглі дадацца запісы з меншым значэннем поля delivery_time. Таксама замова магла зведаць змены, якія тым не менш не закранулі поле delivery_time. У абодвух выпадках змены не будуць перададзены з крыніцы ў прымач. Для вырашэння гэтых праблем нам спатрэбіцца перадаваць дадзеныя "внахлест". Г.зн. пры кожным сеансе абмену мы будзем перадаваць усе дадзеныя са значэннем поля delivery_time, Які перавышае некаторы момант у мінулым (напрыклад, N гадзін ад бягучага моманту). Аднак відавочна, што для буйных сістэм такі падыход з'яўляецца моцна залішнім і можа звесці эканомію трафіку, да якой мы імкнемся, на нішто. Акрамя таго, у перадаваемай табліцы можа не быць палі, звязанага з датай-часам.

Іншае рашэнне, больш складанае з пункту гледжання рэалізацыі, заключаецца ў пацвярджэнні атрымання даных. У гэтым выпадку пры кожным сеансе абмену перадаюцца ўсе даныя, атрыманне якіх не пацверджана атрымальнікам. Для рэалізацыі спатрэбіцца дадаць у табліцу-крыніцу булеўскую калонку (напрыклад, is_transferred). Калі прымач пацвярджае атрыманне запісу, якое адпавядае поле прымае значэнне true, пасля чаго запіс больш не ўдзельнічае ў абменах. Такі варыянт рэалізацыі мае наступныя мінусы. Па-першае, для кожнага перададзенага запісу неабходна згенераваць і адправіць пацверджанне. Грубіянска кажучы, гэта можа быць супастаўна з падваеннем колькасці перадаваных дадзеных і прывесці да падваення колькасці раундтрыпаў. Па-другое, адсутнічае магчымасць адпраўкі аднаго і таго ж запісу ў некалькі прымачоў (першы атрымалы прымач пацвердзіць атрыманне за сябе і за ўсіх астатніх).

Спосаб, пазбаўлены недахопаў, прыведзеных вышэй, складаецца ў даданні ў перадаваную табліцу калонкі для адсочвання змен яе радкоў. Такая калонка можа мець тып дата-час і павінна задавацца/абнаўляцца дадаткам на бягучы час кожны раз пры даданні/змене запісаў (атамарна з даданнем/зменай). У якасці прыкладу назавем калонку update_time. Захаваўшы максімальнае значэнне поля гэтай калонкі для перададзеных запісаў, мы зможам пачаць наступны сеанс абмену з гэтага значэння (адабраць запісы са значэннем поля update_time, якія перавышаюць захаванае раней значэнне). Праблема, злучаная з апошнім падыходам, складаецца ў тым, што змены дадзеных могуць адбывацца ў пакетным рэжыме. У выніку значэння палёў у калонцы update_time могуць быць не ўнікальнымі. Такім чынам, гэтая калонка не можа быць скарыстана для парцыённай (пастаронкавай) выдачы дадзеных. Для пастаронкавай выдачы дадзеных прыйдзецца вынаходзіць дадатковыя механізмы, якія, хутчэй за ўсё, будуць мець вельмі нізкую эфектыўнасць (напрыклад, выманне з БД усіх запісаў са значэннем update_time вышэй зададзенага і выдача вызначанай колькасці запісаў, пачынальна з некаторага зрушэння ад пачатку выбаркі).

Можна павысіць эфектыўнасць перадачы даных, крыху ўдасканаліўшы папярэдні падыход. Для гэтага ў якасці значэнняў палёў калонкі для адсочвання змен будзем выкарыстоўваць цэлалікавы тып (доўгае цэлае). Назавем калонку row_ver. Значэнне поля гэтай калонкі па-ранейшаму павінна задавацца/абнаўляцца кожны раз пры стварэнні/змене запісу. Але ў дадзеным выпадку полю будзе прысвойвацца не бягучы дата-час, а значэнне некаторага лічыльніка, павялічанага на адзінку. У выніку калонка row_ver будзе змяшчаць унікальныя значэнні і зможа быць выкарыстана не толькі для выдачы «дэльты» дадзеных (дадзеных, якія дадаліся/змяніліся пасля завяршэння папярэдняга сеансу абмену), але і для простай і эфектыўнай разбіўкі іх на старонкі.

Апошні прапанаваны спосаб мінімізацыі колькасці дадзеных, якія перадаюцца ў рамках высокаўзроўневай рэплікацыі, уяўляецца мне найболей аптымальным і ўніверсальным. Спынімся на ім падрабязней.

Перадача даных з выкарыстаннем лічыльніка версій радкоў

Рэалізацыя сервернай/master часткі

У MS SQL Server для рэалізацыі падобнага падыходу існуе спецыяльны тып калонкі. rowversion. Кожная БД мае лічыльнік, які павялічваецца на адзінку кожны раз пры даданні/змене запісу ў табліцы, якая мае калонку тыпу rowversion. Значэнне гэтага лічыльніка аўтаматычна прысвойваецца полю гэтай калонкі ў які дадаўся/змянілася запісу. СКБД Tarantool не мае аналагічнага ўбудаванага механізму. Аднак у Tarantool яго нескладана рэалізаваць уручную. Разгледзім, як гэта робіцца.

Для пачатку трохі тэрміналогіі: табліцы ў Tarantool завуцца спейсамі (space), а запісы - картэжамі (tuple). У Tarantool можна ствараць паслядоўнасці (sequence). Паслядоўнасці ўяўляюць з сябе не што іншае, як найменныя генератары спарадкаваных значэнняў цэлых лікаў. Г.зн. гэта якраз тое, што трэба для нашых мэт. Ніжэй мы створым такую ​​пасьлядоўнасьць.

Перш чым выканаць якую-небудзь аперацыю з базай дадзеных у Tarantool, неабходна выканаць наступную каманду:

box.cfg{}

У выніку Tarantool пачне запісваць у бягучы каталог здымкі БД (snapshot) і часопіс транзакцый.

Створым паслядоўнасць row_version:

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

опцыя if_not_exists дазваляе выконваць скрыпт стварэння шматкроць: калі аб'ект існуе, Tarantool не будзе спрабаваць стварыць яго паўторна. Гэтая опцыя будзе выкарыстоўвацца ва ўсіх наступных DDL-камандах.

Створым спейс для прыкладу.

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

Тут мы задалі імя спейсу (goods), імёны палёў і іх тыпы.

Аўтаінкрыментныя палі ў Tarantool ствараюцца таксама з дапамогай паслядоўнасцяў. Створым аўтаінкрыментны першасны ключ па полі 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 падтрымлівае некалькі тыпаў азначнікаў. Часцей за ўсё выкарыстоўваюцца індэксы тыпаў TREE і HASH, у аснове якіх ляжаць адпаведныя найменні структуры. TREE - найбольш універсальны тып індэкса. Ён дазваляе здабываць дадзеныя ва ўпарадкаваным выглядзе. Але для выбару па роўнасці больш падыходзіць HASH. Адпаведна, для першаснага ключа мэтазгодна выкарыстоўваць HASH (што мы і зрабілі).

Каб выкарыстоўваць калонку row_ver для перадачы якія змяніліся дадзеных, неабходна прывязаць да палёў гэтай калонкі значэння паслядоўнасці row_ver. Але ў адрозненні ад першаснага ключа, значэнне поля калонкі row_ver павінна павялічвацца на адзінку не толькі пры даданні новых запісаў, але і пры змене існых. Для гэтага можна выкарыстоўваць трыгеры. У Tarantool ёсць два тыпу трыгераў для спэйсаў: before_replace и on_replace. Трыгеры запускаюцца пры кожнай змене дадзеных у спосабе (для кожнага картэжа, закранутага зменамі, запускаецца функцыя трыгера). У адрозненне ад on_replace, before_replace-трыгеры дазваляюць мадыфікаваць дадзеныя картэжа, для якога выконваецца трыгер. Адпаведна, нам падыходзіць апошні тып трыгераў.

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

Прыведзены трыгер замяняе значэнне поля row_ver захоўваецца картэжа на чарговае значэнне паслядоўнасці row_version.

Для таго каб можна было здабываць дадзеныя са спейсу goods па калонцы row_ver, створым індэкс:

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

Тып індэкса - дрэва (TREE), т.я. дадзеныя нам запатрабуецца здабываць у парадку ўзрастання значэнняў у калонцы row_ver.

Дадамо ў спейс некаторыя дадзеныя:

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}

Т.к. першае поле - аўтаінкрыментны лічыльнік, перадаем замест яго nil. Tarantool аўтаматычна падставіць чарговае значэнне. Аналагічным чынам у якасці значэння палёў калонкі row_ver можна перадаць nil - ці не паказваць значэнне наогул, т.к. гэтая калонка займае апошнюю пазіцыю ў спейсе.

Праверым вынік устаўкі:

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]
...

Як бачым, першае і апошняе поле запоўніліся аўтаматычна. Цяпер будзе нескладана напісаць функцыю для пастаронкавай выгрузкі зменаў спейсу 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

Функцыя прымае ў якасці параметру значэнне row_ver, пачынаючы з якога неабходна ажыццявіць выгрузку змен, і вяртае порцыю якія змяніліся дадзеных.

Выбарка дадзеных у Tarantool робіцца праз індэксы. Функцыя get_goods выкарыстоўвае ітэратар па індэксе row_ver для атрымання якія змяніліся дадзеных. Тып ітэратара - GT (Greater Than, больш чым). Гэта азначае, што ітэратар будзе ажыццяўляць паслядоўны абыход значэнняў азначніка пачынальна з перададзенага ключа (значэнні поля row_ver).

Ітэратар вяртае картэжы. Каб пасля мець магчымасць перадаць дадзеныя па HTTP, неабходна выканаць пераўтварэнне картэжаў да структуры, зручнай для наступнай серыялізацыі. У прыкладзе для гэтага выкарыстоўваецца стандартная функцыя tomap. Замест выкарыстання tomap можна напісаць уласную функцыю. Напрыклад, мы можам захацець перайменаваць поле name, не перадаваць поле code і дадаць поле 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

Памер старонкі выдаваных дадзеных (колькасць запісаў у адной порцыі) вызначаецца зменнай page_size. У прыкладзе значэнне page_size роўна 5. У рэальнай праграме памер старонкі звычайна мае большае значэнне. Ён залежыць ад сярэдняга памеру картэжа спейсу. Аптымальны памер старонкі можна падабраць дасведчаным шляхам, замяраючы час перадачы дадзеных. Чым большы памер старонкі, тым меншая колькасць раўндтрыпаў паміж які перадае і прымае бокам. Так можна паменшыць агульны час выгрузкі змен. Аднак пры занадта вялікім памеры старонкі мы будзем занадта доўга займаць сервер серыялізацыяй выбаркі. У выніку могуць узнікнуць затрымкі пры апрацоўцы іншых запытаў, якія прыйшлі на сервер. Параметр page_size можна загрузіць з канфігурацыйнага файла. Для кожнага перадаецца спейсу можна задаць сваё значэнне. Пры гэтым для большасці спэйсаў можа падысці значэнне па змаўчанні (напрыклад, 100).

Выканаем функцыю 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
...

Возьмем значэнне поля row_ver з апошняга радка і зноў выклічам функцыю:

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
...

І яшчэ раз:

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

Як бачым, пры такім выкарыстанні функцыя пастаронкава вяртае ўсе запісы спейсу. goods. За апошняй старонкай ідзе пустая выбарка.

Унясем змены ў спейс:

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

Мы змянілі значэнне поля name для аднаго запісу і дадалі дзве новыя запісы.

Паўторым апошні выклік функцыі:

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
...

Функцыя вярнула які змяніўся і якія дадаліся запісы. Такім чынам, функцыя get_goods дазваляе атрымліваць дадзеныя, якія змяніліся з моманту яе апошняга выкліку, што і з'яўляецца асновай разгляданага спосабу рэплікацыі.

Пакінем выдачу вынікаў па HTTP у выглядзе JSON за рамкамі гэтага артыкула. Пра гэта можна прачытаць тут: https://habr.com/ru/company/mailru/blog/272141/

Рэалізацыя кліенцкай/slave часткі

Разгледзім, як выглядае рэалізацыя прымаючага боку. Створым на які прымае боку спейс для захоўвання загружаных дадзеных:

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

Структура спэйсу нагадвае структуру спэйсу ў крыніцы. Але паколькі мы не збіраемся перадаваць атрыманыя дадзеныя куды-небудзь яшчэ, калонка row_ver у спейсе атрымальніка адсутнічае. У полі id будуць запісвацца ідэнтыфікатары крыніцы. Таму на баку прымача няма неабходнасці рабіць яго аўтаінкрыментным.

Акрамя гэтага, нам спатрэбіцца спейс для захавання значэнняў 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
})

Для кожнага загружанага спейсу (поле space_name) будзем захоўваць тут апошняе загружанае значэнне row_ver (поле value). У якасці першаснага ключа выступае калонка space_name.

Створым функцыю для загрузкі дадзеных спейсу goods па HTTP. Для гэтага нам спатрэбіцца бібліятэка, якая рэалізуе HTTP-кліент. Наступны радок загружае бібліятэку і стварае асобнік HTTP-кліента:

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

Таксама нам спатрэбіцца бібліятэка для дэсерыялізацыі json:

local json = require('json')

Гэтага дастаткова для стварэння функцыі загрузкі дадзеных:

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

Функцыя выконвае HTTP-запыт па адрасе url. row_ver у якасці параметру і вяртае дэсерыялізаваны вынік запыту.

Функцыя захавання атрыманых дадзеных выглядае наступным чынам:

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

Цыкл захавання дадзеных у спейс goods змешчаны ў транзакцыю (для гэтага выкарыстоўваецца функцыя box.atomic) для памяншэння колькасці аперацый з дыскам.

Нарэшце, функцыю сінхранізацыі лакальнага спейсу goods з крыніцай можна рэалізаваць так:

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

Спачатку счытваем захаванае раней значэнне row_ver для спейсу goods. Калі яно адсутнічае (першы сеанс абмену), то бярэм у якасці row_ver нуль. Далей у цыкле вырабляем пастаронкавую загрузку змененых дадзеных з крыніцы па паказаным url. На кожнай ітэрацыі захоўваем атрыманыя дадзеныя ў які адпавядае лакальны спейс і абнаўляем значэнне row_ver (у спейсе row_ver і ў зменнай row_ver) - бярэм значэнне row_ver з апошняга радка загружаных дадзеных.

Для абароны ад выпадковага зацыклення (у выпадку памылкі ў праграме) цыкл while можна замяніць на for:

for _ = 1, max_req do ...

У выніку выканання функцыі sync_goods спейс goods у прымачы будзе змяшчаць апошнія версіі ўсіх запісаў спейсу goods у крыніцы.

Відавочна, што такім спосабам нельга трансляваць выдаленне дадзеных. Калі такая неабходнасць існуе, можна выкарыстоўваць пазнаку на выдаленне. Дадаем у спейс goods булеўскае поле is_deleted і замест фізічнага выдалення запісу выкарыстоўваем лагічнае выдаленне - выстаўляем значэнне поля is_deleted у значэнне true. Часам замест булеўскага поля is_deleted зручней выкарыстоўваць поле deleted, у якім захоўваецца дата-час лагічнага выдалення запісу. Пасля выканання лагічнага выдалення пазначаны на выдаленне запіс будзе перададзены з крыніцы ў прымач (згодна з разгледжанай вышэй логікай).

паслядоўнасць row_ver можна выкарыстоўваць для перадачы дадзеных іншых спэйсаў: няма неабходнасці ў стварэнні асобнай паслядоўнасці для кожнага перадаецца спейсу.

Мы разгледзелі эфектыўны спосаб высокаўзроўневай рэплікацыі дадзеных у дадатках, якія выкарыстоўваюць СКБД Tarantool.

Высновы

  1. СКБД Tarantool - прывабны, перспектыўны прадукт для стварэння высоканагружаных прыкладанняў.
  2. Высокаўзроўневая рэплікацыя дадзеных мае шэраг пераваг у параўнанні з нізкаўзроўневай рэплікацыяй.
  3. Разгледжаны ў артыкуле спосаб высокаўзроўневай рэплікацыі дазваляе мінімізаваць колькасць перадаваных дадзеных шляхам перадачы толькі тых запісаў, якія змяніліся пасля апошняга сеансу абмену.

Крыніца: habr.com

Дадаць каментар