Osui silmiin tämä keskustelu transaktioiden toteuttamisesta NoSQL-tietokannoissa. Eräs ehdotettu ratkaisumalli on kahdenkertainen kirjanpito, jossa pidetään yllä saldotiedon lisäksi joukkoa avoimia "velkakirjoja".

Ratkaistava ongelmahan on, että jos henkilöllä A on tilillään vaikkapa 100 euroa, niin miten hallitaan tilanne, jossa hän tekee yhtäaikaa kaksi tilisiirtoa henkilöille B ja C. Suoritettavat tarkistukset ja operaatiot ovat seuraavanlaiset:

  1. A >= 100 (tarkistetaan, että saldo riittää)
  2. B += 100 (siirretään B:lle 100 EUR)
  3. A -= 100 (vähennetään A:n saldosta 100 EUR)
  4. A >= 100 (tässä kohtaa käsittely keskeytyy koska katetta ei enää ole)

Jos operaatiot sattuvat tapahtumaan vähän eri järjestyksessä, lopputulos on erilainen:

  1. A >= 100
  2. A >= 100
  3. B += 100
  4. C += 100 (nyt C:kin saa 100 EUR)
  5. A -= 100
  6. A -= 100 (A:lta vähennetään kahteen kertaan saldosta 100 EUR)

A:n tili päätyy siis 100 EUR miinukselle. Järjestyksen vaihtumisen lisäksi saattaa käydä niin, että osa operaatioista jää toteutumatta sähkökatkon takia. Silloin voisi käydä näin:

  1. A >= 100
  2. B += 100
  3. Sähkökatko

Eli summa ehditään lisätä B:lle mutta ei poistaa A:lta, jolloin järjestelmään syntyy 100 EUR virhe.

Kahdenkertainen kirjanpito

Kahdenkertaisella kirjanpidolla nämä ongelmat voidaan ratkaista velkakirjaperiaatteella. Velkakirja on tietokantaan lisättävä rivi, joka kertoo, että A on 100 EUR velkaa, ja että B:lle taas ollaan 100 EUR velkaa. Tilin saldoa tarkistettaessa on siis huomioitava sekä varsinainen saldokenttä että avoimet velkakirjat. Operaatiot voi nyt merkitä näin:

  1. A >= 100
  2. B += 100, A -= 100 (molemmat atomisesti samalla velkakirjarivillä)
  3. A >= 100 (käsittely keskeytyy tähän, koska velkakirjarivi huomioidaan ja A == 0)

Jäljelle jää kuitenkin vielä alkuperäinen järjestysongelma, eli saattaa käydä näin:

  1. A >= 100
  2. A >= 100
  3. B += 100, A -= 100 (velkakirja 1)
  4. C += 100, A -= 100 (velkakirja 2)

Tämä voidaan ratkaista tekemällä saldotarkistus aina vasta velkakirjan luomisen jälkeen, ja poistamalla velkakirja sitten, jos saldo ei riittänyt:

  1. B += 100, A -= 100 (velkakirja 1)
  2. C += 100, A -= 100 (velkakirja 2)
  3. A >= 0 (tässä vaiheessa A == -100, joten perutaan velkakirja 1)
  4. A >= 0 (tässä vaiheessa A == 0, joten velkakirja 2 menee läpi)

Normaalitilanteessa tämä johtaa siihen, että tilin saldo ei jää koskaan alle 0:n ja epäonnistuneet transaktiot perutaan saman tien. Jos kuitenkin tulee sähkökatko, niin tilin saldo saattaa jäädä 100 miinukselle "ylimääräisen" velkakirjan takia. Silloin järjestelmän pitäisi virran palatessa osata tunnistaa tilanne ja poistaa velkakirjoja käänteisessä aikajärjestyksessä niin paljon, että saldo on taas >= 0.

Käyttäjälle transaktion kerrotaan onnistuneen vasta sitten, kun velkakirja on luotu ja saldotarkistus onnistunut. Sähkökatkotilanteessa hän saa jonkinlaisen virheilmoituksen, joka ilmaisee transaktion tilan jääneen epämääräiseksi. Oikea tilanne näkyy tilihistoriassa myöhemmin järjestelmän toivuttua.

Hajautetut velkakirjatransaktiot

Yllä kuvailtu periaate toimii myös silloin, kun tietokantapalvelimia on useita, ja niiden sisällöt replikoituvat toisiinsa pienellä viiveellä. Viive aiheuttaa kuitenkin sen, että palvelimet voivat hetken aikaa kuvitella saldon riittävän transaktioon.

Ensimmäisellä palvelimella:

  1. B += 100, A -= 100 (velkakirja 1)
  2. A >= 0 (transaktio menee läpi)

Toisella palvelimella samaan aikaan:

  1. C += 100, A -= 100 (velkakirja 2)
  2. A >= 0 (transaktio menee läpi)

Replikoinnin jälkeen velkakirjat synkronoituvat molemmille palvelimille. Järjestelmän pitää silloin huomata, että A:n saldo on -100 ja tuhota jompi kumpi velkakirjoista. Tämä on käytännössä sama operaatio kuin sähkökatkon jälkeinen saldotarkistus, eli siitä voi huolehtia jokin säännöllisesti ajettava taustaprosessi.

Käyttäjän kannalta saattaa syntyä tilanne, jossa hänelle on jo kerrottu transaktion onnistuneen, mutta hetken päästä synkronoinnin yhteydessä se perutaankin. Jos synkronointiviive on kohtuullinen (joitakin sekunteja), käyttöliittymä voi ehkä odotella hetken aikaa, tarkistaa onko transaktio yhä onnistuneena tietokannassa, ja vasta sitten ilmoittaa onnistumisesta käyttäjälle. Muuten peruuntuminen näkyy vain tilihistoriassa.

Velkakirjojen konsolidointi saldoihin

Jos velkakirjoja syntyy ajan mittaan paljon, saattaa saldon laskeminen käydä työlääksi. Siksi on tarpeen suorittaa välillä konsolidointia, jossa velkakirjojen kredit- ja debit-summat yhdistetään tilien saldoihin ja velkakirjat poistetaan.

Monen palvelimen ympäristössä konsolidointi voidaan suorittaa yhdellä palvelimella, jolloin ei tarvitse huolehtia replikointikonflikteista. Operaatiot voidaan kohdistaa aikaleimojen perusteella ainoastaan riittävän vanhoihin velkakirjoihin, jotka ovat varmasti synkronoituneet onnistuneesti kaikille palvelimille. Koska operaatiot tehdään yhdellä palvelimella ja peräkkäin, ne tapahtuvat aina oikeassa järjestyksessä.

Ongelmaksi jää siis lähinnä sähkökatkotilanne, jossa summa on jo siirretty tilin saldoon, mutta velkakirjaa ei ole vielä poistettu tietokannasta. Monissa NoSQL-kannoissa on onneksi olemassa työkalut, joilla tämä operaatio voidaan suorittaa atomisesti molempiin dokumentteihin. Esimerkiksi CouchDB:ssä on all_or_nothing-päivitys, joka muokkaa useaa dokumenttia kerralla, tai sitten peruu kaikki muutokset sähkökatkon sattuessa.

Yhteenveto

Tässä kuvailtu tapa toteuttaa transaktioita on eventual consistency -periaatteen mukainen. Järjestelmä voi olla joskus hetken aikaa "miinuksella", mutta se palaa lopuksi aina konsistenttiin tilaan. Käytännössä näitä miinustilanteita syntyy hyvin harvoin, ellei käyttäjä tahallaan yritä suorittaa yhtaikaisia transaktioita usealla palvelimella, tai ellei tietokantapalvelinten välinen replikointi pysähdy.

Näin rakentuva järjestelmä skaalautuu periaatteessa rajattomaan määrään tietokantapalvelimia, jos niiden synkronointiin liittyvä aikaviive vain on hyväksyttävissä. Transaktioita voidaan suorittaa millä tahansa palvelimella, eli myös järjestelmän kirjoitusnopeus skaalautuu palvelimia lisäämällä.