MongoDB - Atomic Operations (Atomske Operacije)

MongoDB ne podržava "multi-document" atomske transakcije. Ali obezbeđuje atomske operacije nad jednim dokumentom. Tako da ako dokument ima puno polja koje će upit update-ovati, ili će se update-ovati sva polja ili ni jedno, na taj način se održava "valentnost" na nivou dokumenta.

Zbog ovoga potrebno je dobro planiranje modela podataka, pa je preporučen pristup da bi se održala valentnost dokumenata je da se sve povezane informacije, koje se često zajedno update-uju čuvaju unutar jednog dokumenta koristeći se ugrađenim "embedded" dokumentima. Na ovaj način bi se osiguralo da su sva ažuriranja nad jednim dokumentom atomska.

Za sledeći zadatak koristićemo sledeći dokument

{
  "student_id":"2012/0112",
  "ime":"Emilie Eric",
  "smer":"ISiT",
  "broj_preostalih_ispita": "8",
  "polozeni_ispiti":[
    {
        "predmet":"EPOS",
        "ocena":"10",
        "datumPolaganja":"20.3.2015",
        "semestar":"V"
    },
    {
        "predmet":"ITEH",
        "ocena":"9",
        "datumPolaganja":"20.4.2016",
        "semestar":"VII"
    },
    {
        "predmet":"IIU",
        "ocena":"7",
        "datumPolaganja":"21.5.2016",
        "semestar":"VIII"
    }
  ]
}

Ubacimo ga u bazu sa nazivom "student_test" i neka je dokument unutar kolekcije student.

> use student_test
switched to db student_test
> db.student.insert({
      "student_id":"2012/0112",
      "ime":"Emilie Eric",
      "smer":"ISiT",
      "broj_preostalih_ispita": 8,
      "polozeni_ispiti":[
        {
          "predmet":"EPOS",
          "ocena":"10",
          "datumPolaganja":"20.3.2015",
          "semestar":"V"
        },
        {
          "predmet":"ITEH",
          "ocena":"9",
          "datumPolaganja":"20.4.2016",
          "semestar":"VII"
        },
        {
          "predmet":"IIU",
          "ocena":"7",
          "datumPolaganja":"21.5.2016",
          "semestar":"VIII"
        }
      ]
    })
WriteResult({ "nInserted" : 1 })

Zadatak 52 - Upisati navedeni predmet u listu položenih ispita studenta Emilie Eric, pri čemu treba smanjiti broj preostalih ispita za jedan. Ukoliko je broj preostalih ispita jednak nuli prekinuti unos ispita. Ovaj student se nalazi unutar kolekcije student koja se nalazi unutar baze padataka student_test.

Novi položeni predmet koji treba upisati:

{
    "predmet":"Internet Marketing",
    "ocena":"8",
    "datumPolaganja":"15.7.2016",
    "semestar":"VIII"
}

Vidimo da unutar dokumenta imamo listu položenih ispita u koju trebamo upisati novi predmet, pri čemu će se broj preostalih ispita smanjiti. I ispit se ne treba upisati ukoliko je broj preostalih ispita jednak nuli. U ovakvim slučajevima kada hoćemo da update-ujemo nešto ali update zavisi od drugoh polja možemo koristiti findAndModify().

Rešenje

> use student_test
switched to db student_test
> db.student.findAndModify({ query: {"ime":"Emilie Eric", "broj_preostalih_ispita": {$gt: 0}}, update: { $inc: {"broj_preostalih_ispita": -1}, $push:{"polozeni_ispiti":{"predmet":"Internet Marketing", "ocena":"8", "datumPolaganja":"15.7.2016", "semestar":"VIII"}}} })
{
    "_id" : ObjectId("591c667a95cfdeac509f2620"),
    "student_id" : "2012/0112",
    "ime" : "Emilie Eric",
    "smer" : "ISiT",
    "broj_preostalih_ispita" : 8,
    "polozeni_ispiti" : [
        {
            "predmet" : "EPOS",
            "ocena" : "10",
            "datumPolaganja" : "20.3.2015",
            "semestar" : "V"
        },
        {
            "predmet" : "ITEH",
            "ocena" : "9",
            "datumPolaganja" : "20.4.2016",
            "semestar" : "VII"
        },
        {
            "predmet" : "IIU",
            "ocena" : "7",
            "datumPolaganja" : "21.5.2016",
            "semestar" : "VIII"
        }
    ]
}

I sada ako proverimo novo stanje

> db.student.find().pretty()
{
    "_id" : ObjectId("591c667a95cfdeac509f2620"),
    "student_id" : "2012/0112",
    "ime" : "Emilie Eric",
    "smer" : "ISiT",
    "broj_preostalih_ispita" : 7,
    "polozeni_ispiti" : [
        {
            "predmet" : "EPOS",
            "ocena" : "10",
            "datumPolaganja" : "20.3.2015",
            "semestar" : "V"
        },
        {
            "predmet" : "ITEH",
            "ocena" : "9",
            "datumPolaganja" : "20.4.2016",
            "semestar" : "VII"
        },
        {
            "predmet" : "IIU",
            "ocena" : "7",
            "datumPolaganja" : "21.5.2016",
            "semestar" : "VIII"
        },
        {
            "predmet" : "Internet Marketing",
            "ocena" : "8",
            "datumPolaganja" : "15.7.2016",
            "semestar" : "VIII"
        }
    ]
}

Dodatak

Na ovaj način koristeći findAndModify() osiguravamo se da će će se polozeni ispiti update-ovati samo ako je "query" ispunjen.


Two Phase Commits

Gore navedeni primer daje rešenje samo kada je u pitanju jedan dokument.
Postoji patern koji se koristi za "multi-document" ažuriranje ili "multi-document" transakcije, on koristi "two-phase commit" pristup (čuvanje stanja u dve faze). Ovaj proces se može dodatno poriširiti kako bi obezbedio "rollback-like" funkcionalnost.

Kako dokumenti mogu biti jako kompleksi i mogu da sadrže više ugnježdenih dokumenata, "single-document atomicity" obezbeđuje dovoljnu podršku za većinu realnih slučajeva (use case). Međutim, bez obzira na "moć" "single-document atomic" operacija, postoje slučajevi kada je rad sa multi dokument transakcijama neophodan. Prilikom izvršenja transakcija koje su sačinjene od više operacija, određeni rizici za gubitak nekih osobina rastu, kao što su:

  • Valentnost (Atomicity) : Ako se jedna operacija ne izvrši pravilno, prethodne operacije unutar transakcije moraju se "rollback"-ovati na prethodno stanje.
  • Konzistencija (Consistency): ako dođe do nekog većeg problema (pada mreže ili hardware-a) koji prekine transakciju, baza podataka mora da bude u mogućnosti da sve vrati na početno stanje.

Za ovakve situacije koristi se "Two-phase commit" koji osigurava da je data konzistentna i da je u slučaju greške moguće povratiti stanje koje je bilo pre transakcije.

Treba imati na umu, pošto su samo operacije na pojedinačnim dokumentima "atomic" u MongoDB-u, dvo-fazni "commit" su samo "transaction-like".


Za sledeći zadatak napravićemo bazu podataka "banka" koja će imati dve kolekcije - "racuni" i "transakcije".
I rešenje sledećeg zadatka predstavlja korake koji bi bili pozivani od strane aplikacije koja bi bila zadužena za izvršavanje transakcija.

Zadatak 53 - Prebaciti 5000 sa računa A na račun B koristeći "Two-Phase commits" pristup.

Zadatak traži da se određena cifra novca prebaci sa računa A na račun B. U relacionim bazama ovo bi se izvelo jednostavnim oduzimanjem sredstava sa računa A i dodavanjem iste cifre na račun B koristeći se jednom "multi-statement" transakcijom. U MongoDB se koristi dvofazni commit da bi se ostvarila slična funkcionalnost.

> use banka
switched to db banka
> db.akaunti.insert(
       [
         { _id: "A", stanje: 10000, trenutnaTransakcija: [] },
         { _id: "B", stanje: 10000, trenutnaTransakcija: [] }
       ]
    )
BulkWriteResult({
    "writeErrors" : [ ],
    "writeConcernErrors" : [ ],
    "nInserted" : 2,
    "nUpserted" : 0,
    "nMatched" : 0,
    "nModified" : 0,
    "nRemoved" : 0,
    "upserted" : [ ]
})

Inicijalizacija transfera

Za svaki transfer koji treba da se izvrši, ubaci se dokument unutar kolekcije "transakcije" sa svim potrebnim informacijama o transakciji. Polja koja su potrebna su sledeća:

  • posiljalac i primalac, polja koja treba da predstavljaju _id polja iz "akaunt" kolekcije
  • suma, polje kojim se definise suma koja treba da se prebaci sa jednog računa na drugi
  • status, polje koji prikazuje trenutno stanje transfera. Ono može da ima vrednosti kao što su "zapocet", "neresen", "primenjen", "zavrsen", "prekinut" itd
  • poslednjiPutPromenjen, polje koje prikazuje vreme poslednje modifikacije

Želim da napomenem da su imena ovim poljima data proizvoljno, ovo je samo primer.

Da bi se započelo transfer sa jednog računa na drugi u vrednosti od 5000, ubacićemo jedan dokument unutar kolekcije "transakcije", koji će sadržati gore pomenuta polja.

> db.transakcije.insert(
    { _id: 1, posiljalac: "A", primalac: "B", suma: 5000, stanje: "zapocet", poslednjiPutPromenjen: new Date() }
  )
WriteResult({ "nInserted" : 1 })

Transfer sredstava između dva računa

Korak 1

Prvi korak bi bio nalaženje transakcije sa statusom "zapocet"

> var t = db.transakcije.findOne( { status: "zapocet" } )

Da bi pogledali šta se nalazi u varijabli t, potrebno je da u konzoli ukucate samo "t"

> t
{
    "_id" : 1,
    "posiljalac" : "A",
    "primalac" : "B",
    "suma" : 5000,
    "status" : "zapocet",
    "poslednjiPutPromenjen" : ISODate("2017-05-31T11:03:36.326Z")
}
Korak 2

Sledeći korak je menjanje statusa iz "zapocet" u "neresen", kao i korišćenje $currentDate operatora za menjanje polja "poslednjiPutPromenjen"

> db.transakcije.update(
      { _id: t._id, status: "zapocet" },
      {
        $set: { status: "neresen" },
        $currentDate: { poslednjiPutPromenjen: true }
      }
   )
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
Korak 3

Sledeće je ažuriranje oba računa. Treba primeniti transakciju t na oba računa koristeći update() metod AKO transakcija nije bila primenjena na oba računa. U uslovu upita treba proveriti da li unutar liste "trenutnaTransakcija" svakog računa se nalazi "t._id" kako be se izbeglo ponavljanje dupliranje transakcije.

U ažuriranje računa sada ulaze promene na poljima "stanje" i "trenutnaTransakcija".

Prvo se ažurira pošiljalac

> db.akaunti.update(
    { _id: t.posiljalac, trenutnaTransakcija: { $ne: t._id } },
    { $inc: { stanje: -t.suma }, $push: { trenutnaTransakcija: t._id } }
 )
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })

A zatim i primalac

> db.akaunti.update(
    { _id: t.primalac, trenutnaTransakcija: { $ne: t._id } },
    { $inc: { stanje: t.suma }, $push: { trenutnaTransakcija: t._id } }
 )
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
Korak 4

Kada se prethodni korak uspešno izvrši, treba ažurirati "status" i "poslednjiPutPromenjen" polja transakcije

> db.transakcije.update(
    { _id: t._id, stanje: "neresen" },
    {
      $set: { stanje: "primenjen" },
      $currentDate: { poslednjiPutPromenjen: true }
    }
 )
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
Korak 5

Kako se transakcija završila, potrebno je da je uklonimo iz nizova "trenutnaTransakcija" koji se nalaze unutar računa

Prvo pošiljalac

> db.akaunti.update(
    { _id: t.posiljalac, trenutnaTransakcija: t._id },
    { $pull: { trenutnaTransakcija: t._id } }
 )
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })

Zatim primalac

> db.akaunti.update(
    { _id: t.primalac, trenutnaTransakcija: t._id },
    { $pull: { trenutnaTransakcija: t._id } }
 )
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 }
Korak 6

I kao finalni korak jeste menjanje statusa transakcije u završen

> db.transakcije.update(
    { _id: t._id, stanje: "primenjen" },
    {
      $set: { stanje: "zavrsen" },
      $currentDate: { poslednjiPutPromenjen: true }
    }
 )
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })

Oporavak usled neuspelih transakcija

Najbitniji deo ove transakcione procedure nije u ovom navedenom primeru (Zadatak 53) već mogućnost povratka u normalno stanje usled raznih loših scenarija koji mogu da se dogode i onemoguće uspešno završavanje transakcije.

Operacije oporavka

"Two-phase commit" patern omogućava aplikaciji da izvršava niz transakcija koje dovode bazu do konzistentnog stanja. Pokretanjem ovih operacija oporavka u nekim određenim intervalima omogućavaju hvatanje bilo kakvih nedovršenih transakcija.

Sledeći primer koristi "poslednjiPutPromenjen" kao indikator da li je određenoj transakciji potreban oporavak. Na primer, ako se transakcije sa statusom "primljen" ili "zapocet" nisu ažurirale u poslednjih 30 minuta, verovatno im je potreban oporavak.

Primer 1

Da bi se povratilo iz greške nakon koraka pri kome se status ažurira u status "neresen" (korak 2) ali pre koraka kada stanje postaje "primenjen" (korak 4), povratili bi transakciju iz kolekcije čije je status "neresen" kao transakciju kod koje je došlo do gresške

> var proveraVremena = new Date();
> proveraVremena.setMinutes(proveraVremena.getMinutes() - 30);
1496316597337
> var t = db.transakcije.findOne( { stanje: "primenjen", poslednjiPutPromenjen: { $lt: proveraVremena }} );

I onda se nastavilo od koraka 5.

Rollback operacije

U nekim slučajevima, potrebno je da se vrati (rollback-uje) transakcija, na primer kada je potrebno da se prekine transakcija ili ako jedan od računa ne postoji ili neka sličan slučaj.

Transakcije u stanju "primenjen"

Nakon koraka kada se stanje transakcije prebacuje u "primenjen", ne treba vršiti "rollback" transakcije. Umesto toga, bolje je da se transakcija obavi do kraja a zatim da se kreira nova transakcija koja će odraditi suprotnu operaciju od prethodne i na taj način vratiti stanje na prethodno.

Transakcije u stanju "neresen"

Nakon koraka 2 (prelaska stanja transakcije u stanje "neresen") a pre koraka 4 (prelaska stanja transakcije u stanje "primenjen) moguće je izvršiti rollback transakcije i to sledećim koracima:

Korak 1

Ažuriranje statusa transakcije u "prekidanje"

db.transakcije.update(
   { _id: t._id, status: "neresen" },
   {
     $set: { status: "prekidanje" },
     $currentDate: { poslednjiPutPromenjen: true }
   }
)
Korak 2

Prekinuti transakcije na oba akaunta, vraćajući ih na prethodno stanje. To će se postići time što će se "stanje" vratiti kao i lista "trenutnaTransakcija".

Provo račun primaoca

> db.akaunti.update(
   { _id: t.primalac, trenutnaTransakcija: t._id },
   {
     $inc: { stanje: -t.value },
     $pull: { trenutnaTransakcija: t._id }
   }
)

A zatim i pošiljaoca

> db.akaunti.update(
   { _id: t.posiljalac, trenutnaTransakcija: t._id },
   {
     $inc: { stanje: t.value},
     $pull: { trenutnaTransakcija: t._id }
   }
)
Korak 3

Poslednji korak jeste postavljanje statusa transakcije na "prekinut"

> db.transakcije.update(
   { _id: t._id, status: "prekidanje" },
   {
     $set: { status: "prekinut" },
     $currentDate: { poslednjiPutPromenjen: true }
   }
)

results matching ""

    No results matching ""