Lisäävä kirjanpito pitää jaetut saldot täsmällisinä
Saldo on projektio, jonka voi heittää pois ja rakentaa uudelleen, ja tämä koneisto tekee siitä turvallista: nollasummakirjaukset, korjauksina kirjatut muokkaukset ja commit-järjestyksessä lähettävä outbox.
Saldo, jota kasvatat, on saldo, joka ryömii. Dimesum ei koskaan kasvata sellaista. Jokainen kulu kirjaa kahdenkertaisen kirjauksen, jonka kirjausten summa on täsmälleen nolla, ja ryhmänäkymäsi luku on projektio noiden kirjausten yli, jonka voimme poistaa ja rakentaa uudelleen yhdellä komennolla.
Voit testata tämän väitteen. Kun ainoa tapa, jolla raha liikkuu, on lisäävä kirjaus, virheellinen saldo lakkaa olemasta arvoitus ja muuttuu kyselyksi, jonka voimme ajaa.
Saldot ovat projektio, eivät luku, jota kasvatat
Dimesumin kirjanpito omistaa kolme taulua: journals, postings ja balances-projektion, joka on avainnettu ryhmän, jäsenen ja valuutan mukaan. Kirjaukset kantavat totuuden. Kirjaus on positiivinen, kun jäsen laittoi rahaa ryhmään, ja negatiivinen, kun hän kulutti arvoa, joten saldo on pelkkä SUM(amount_minor). Projektio on olemassa nopeutta, ei auktoriteettia varten: se kirjoitetaan kirjauksen oman transaktion sisällä, ja kirjausten täydellisyys pitää sen hävitettävänä.
Kaksi kerrosta väittää samaa nollaa, eikä kumpikaan riitä yksin
Go-kielessä buildPostings kieltäytyy avaamasta transaktiota, ellei kirjausjoukon summa ole nolla. Postgresissa viivästetty rajoiteliipaisin tarkistaa uudelleen SUM(amount_minor) = 0 kirjausta kohti commitissa, koska kirjaukset lisätään rivi kerrallaan ja rivikohtainen tarkistus hylkäisi jokaisen kirjauksen ensimmäisen erän. Assert nappaa laskurin bugin. Liipaisin nappaa kirjoittajan, joka ei koskaan kutsunut sitä.
Kolmas kerros on käyttöoikeus. UPDATE, DELETE ja TRUNCATE on peruttu ledger_app-roolilta molemmissa tauluissa, joten viallinen koodi ei voi kirjoittaa historiaa uudelleen, vaikka yrittäisi.
| Bugiluokka | Kasvatettava saldotaulu | Lisäävä kirjanpito |
|---|---|---|
| Velallista veloitetaan, maksajaa ei koskaan hyvitetä | Hiljaisesti väärin ikuisesti | Nollasummatarkistus epäonnistuu, kirjoitus hylätään |
| Osuus muuttuu, kokonaissumma ei | Saldot ryömivät hiljaa | Kirjaus ei voi commitata epätasapainossa |
| "Miksi saldoni on 412 euroa?" | Ei vastattavissa | Jokainen sentti palautuu kirjaukseen |
| Pikakorjaus tuotannossa | Seuraamaton muutos | Ainoa polku on uusi, auditoitu kirjaus |
Muokkaus kirjaa mitätöinnin, ei koskaan UPDATE-komentoa
Kulun muokkaaminen ei kirjoita mitään vanhan version päälle. Kirjanpito kuluttaa yhden expense.amended-tapahtuman ja kirjaa kaksi kirjausta yhdessä transaktiossa: EXPENSE_REVERSAL:n, jonka kirjaukset ovat korvattavan version tarkka erä erältä -mitätöinti, ja sitten tuoreen EXPENSE:n uudelle versiolle. Poisto pysähtyy mitätöinnin jälkeen.
Yksi transaktio on yhtä tärkeä kuin kaksi kirjausta. Jos mitätöinti commitattaisiin yksin, ryhmä ei hetkellisesti olisi velkaa mitään kulusta, jonka se on yhä velkaa.
Molemmat idempotenssiavaimet johdetaan versiosta sen sijaan että ne luotaisiin: expense:<id>:v<n> versiolle, ja edellisen version avain plus :reversal mitätöinnille. journals.idempotency_key on UNIQUE, joten uudelleen toimitettu tapahtuma löytää kirjauksensa jo paikoiltaan eikä tee mitään. Johtaminen on se, mikä tekee vähintään kerran -toimituksesta turvallista: uudelleenyritys laskee saman avaimen, jota ensimmäinen yritys käytti.
Commit-järjestyksen vesileima pitää muokkauksen kulunsa takana
Jokainen tapahtuma, jonka Dimesum julkaisee, kirjoitetaan outbox-tauluun samassa transaktiossa kuin liiketoimintakirjoitus. Jos kulu commitataan, ilmoitus on olemassa. Jos se perutaan, niin peruuntuu ilmoituskin. Kirjanpito ei koskaan kuule kulusta, jota ei ole olemassa.
Lähetysjärjestys on vaikeampi puolisko. Tunnisteet ovat UUIDv7 ja järjestyvät ajan mukaan, mutta ne koodaavat sen, milloin tunniste luotiin, eivät sitä, milloin sen transaktio commitattiin. Rele, joka lukee tunnistejärjestyksessä, voi ohittaa transaktion, joka otti aiemman tunnisteen ja commitattiin myöhemmin, joten muokkaus ohittaa kulun, jota se muokkaa.
Korjaus on yksi sarake ja yksi predikaatti. Jokainen outbox-rivi kantaa inserted_xid xid8 DEFAULT pg_current_xact_id():n, ja rele lukee vain rivit WHERE inserted_xid < pg_snapshot_xmin(pg_current_snapshot()), järjestettynä tuon xidin mukaan (katso PostgreSQL transaction id functions). Kausaalisesti myöhemmällä tapahtumalla on aina myöhempi xid, koska sen oli luettava aiempi rivi ollakseen olemassa.
Muokkauskuluttaja ei luota tuohon järjestykseen sokeasti. Muokkaus, jonka edeltäjällä ei ole kirjausta, toimitetaan uudelleen niin kauan kuin se on nuori, ja pysäköidään ihmistä varten, kun kahden minuutin lisäaika on ohitettu.
Raha on int64-alayksiköitä, eikä eksponentti ole aina kaksi
Jokainen summa on int64-luku alayksiköitä plus ISO 4217 -koodi, joten 1,234.56 euroa on {Minor: 123456, Currency: "EUR"}. Kokonaisluvut ovat rakenteeltaan täsmällisiä, eivät kurin ansiosta: mikään esitettävä arvo ei ole puoli senttiä, joten mikään operaatio ei voi hiljaa tuottaa sellaista. Python-sivuvaunu lukee oman AST:nsa ja hylkää testin, jos sana float esiintyy sen summamoduulissa.
Oletus, että alayksikkö on sadasosa, on ansa tämän alla. JPY:llä ei ole sitä lainkaan ja KWD:llä on kolme desimaalia, joten pääyksiköiden välillä noteerattu kurssi, jota sovelletaan alayksiköihin, on väärä kymmenen potenssilla. Otetaan 2,000 jeniä kurssilla 0.58: naiivi tulo on 1160, joka luetaan 11.60 euroksi, kun vastaus on 1,160.
WRITE_OFF on viides kirjaustyyppi, koska sen kirjaukset vastaavat suorituksen kirjauksia
Saatavasta luopuminen kirjaa samat kaksi erää kuin suoritus: velallinen nousee, velkoja laskee, saman verran. Sen sulauttaminen SETTLEMENT:iin hylättiin juuri siksi, että muodot vastaavat toisiaan. "Asha maksoi sinulle 500 euroa" ja "annoit Ashalle anteeksi 500 euroa" ovat eri faktoja, ja syöte, joka sekoittaisi ne, väittäisi jonkun maksaneen, vaikka kukaan ei maksanut.
Niinpä WRITE_OFF liittyi kirjaustyypin CHECK:iin 2026-08-21, omilla erillään. Niiden nimeäminen oli puolet pointista, koska SETTLE_PAY:tä uudelleen käyttävä saatavasta luopuminen tekisi jokaisesta "kuinka paljon on todella maksettu" -kyselystä hiljaisesti väärän.
| Kirjaustyyppi | Kirjataan kun | Erät |
|---|---|---|
EXPENSE | Luotu, tai uusi versio korvaa vanhan | PAID, SHARE |
EXPENSE_REVERSAL | Muokattu tai poistettu | Edellisen kirjauksen erät, mitätöityinä |
SETTLEMENT | Takaisinmaksu vahvistetaan | SETTLE_PAY, SETTLE_RECV |
SETTLEMENT_REVERSAL | Vastapuoli kiistää sen | Molemmat suorituserät, mitätöityinä |
WRITE_OFF | Velkoja luopuu saatavasta | WRITE_OFF_FORGIVEN, WRITE_OFF_GRANTED |
Kaksi alikomentoa muuttaa invariantit cron-työksi
evenly verify-ledger todistaa invariantit uudelleen koko skeeman yli ja päättyy nollasta poikkeavaan tilaan jokaisesta osumasta. Sen raportissa on neljä kenttää, ja jokaisen odotetaan olevan tyhjä: kirjaukset, joiden summa ei ole nolla, ryhmät, joiden summa ei ole nolla, projektiorivit, jotka poikkeavat uudelleen lasketusta SUM(postings):sta, sekä pysäköidyt tapahtumat, jotka odottavat ihmistä. Kaikki skannaukset jakavat yhden repeatable-read-tilannevedoksen, joten kesken varmistuksen saapuva kirjaus ei voi väärentää ristiriitaa.
Jokainen skannaus ryhmittelee valuutan sekä tunnisteen mukaan, ja virhe, jonka se välttää, on väärä negatiivinen. Kirjaus 500 euroa ja yksi miinus 500 jeniä summautuvat nollaan, kun kysely jättää valuuttasarakkeen huomiotta, joten kahdesti korruptoitunut kirjanpito luettaisiin puhtaana.
evenly rebuild-balances [group-id] on korjaus ja harjoitus. Se tyhjentää projektion ja laskee jokaisen rivin uudelleen kirjauksista, leimaten kunkin viimeisellä kirjauksella, joka liikutti kyseistä jäsentä, aivan kuten live-kirjoitus tekee. Rivi riviltä -yhtäläisyys on ominaisuus: kun uudelleenrakennus ja live-projektio ovat eri mieltä, kirjanpito on oikeassa.
Varmista, sitten rakenna uudelleen. Raportti nimeää tallennetun saldon ja uudelleen lasketun jokaiselle ryömivälle riville, ja uudelleenrakennus korvaa tallennetun arvon, joten uudelleenrakentaminen ensin tuhoaa todisteet.
Tee saldosta johdettava, niin ryömimisestä tulee kysely
Lisäävä kirjanpito ansaitsee toisen kirjauksensa vain jos voit todistaa sen, joten rakenna varmistin ja uudelleenrakennus ennen ominaisuutta, joka niitä tarvitsee. Uudelleenrakennus, jota kukaan ei ole ajanut, on toivo, ei varauloskäynti. Valitse riskialttein rahaprojektiosi tällä viikolla, kirjoita kysely, joka laskee sen uudelleen lähteestä, ja hälytä itsesi, kun nämä kaksi ovat eri mieltä.
Usein kysytyt kysymykset
Mikä on lisäävä kirjanpito kulujen jakosovelluksessa?
Lisäävä kirjanpito tallentaa jokaisen rahatapahtuman kirjauksina, joiden summa on nolla, eikä koskaan päivitä tai poista yhtäkään. Dimesumissa kulu, muokkaus, suoritus, kiista ja saatavasta luopuminen lisäävät kukin uuden kirjauksen. Saldot johdetaan sitten kirjausten summana, joten jokainen sentti palautuu tapahtumaan, joka sen liikutti.
Miten lisäävä kirjanpito käsittelee muokatun kulun?
Muokkaus kirjaa kaksi kirjausta samassa transaktiossa: EXPENSE_REVERSAL, joka mitätöi alkuperäisen erä erältä, ja sitten uusi EXPENSE, joka sisältää korvaavan version. Mitään ei kirjoiteta uudelleen, ja poisto kirjaa vain mitätöinnin. Molemmat kirjaukset saavat idempotenssiavaimet, jotka johdetaan kulun versiosta, joten uudelleen toimitettu tapahtuma löytää ne jo paikoiltaan eikä muuta mitään.
Miksi rahaa kannattaa tallentaa kokonaislukuina eikä liukulukuina?
Kokonaislukuina ilmaistut alayksiköt ovat rakenteeltaan täsmällisiä, kun taas binäärinen liukuluku ei voi esittää arvoa 0.1 täsmälleen ja ryömii ryhmän historian aikana. Dimesum tallentaa jokaisen summan int64-lukuna alayksiköitä sekä ISO 4217 -koodin. Eksponentti tulee valuutasta: JPY:llä ei ole alayksikköä lainkaan, joten sadasosan olettaminen on 100x virhe.
Mistä tietää, etteivät jaettujen kulujen saldot ole ryömineet?
Aja evenly verify-ledger, joka todistaa invariantit uudelleen koko skeeman yli ja päättyy nollasta poikkeavaan tilaan jokaisesta osumasta. Se raportoi kirjaukset, joiden summa ei ole nolla, ryhmät, joiden summa ei ole nolla, projektiorivit, jotka ovat ristiriidassa uudelleen lasketun SUM(postings):n kanssa, sekä pysäköidyt tapahtumat. Ajasta se öisin, ja korjaa virheellinen projektio komennolla evenly rebuild-balances.
Miksi saatavasta luopuminen tarvitsee oman kirjaustyypin?
Saatavasta luopuminen kirjaa täsmälleen samat erät kuin suoritus, ja juuri siksi kirjaustyypin oli oltava eri. Vain tuo sana erottaa lauseen 'Asha maksoi sinulle 500 euroa' lauseesta 'annoit Ashalle anteeksi 500 euroa', ja syöte, joka sekoittaisi ne, väittäisi jonkun maksaneen, vaikka kukaan ei maksanut. Sen erät on nimetty WRITE_OFF_FORGIVEN ja WRITE_OFF_GRANTED, jotta maksukyselyt pysyvät oikeina.
Suositut artikkelit
- Miksi jaetun kulun muokkaus vaatii jaon toistamisen6 min lukuaika
- Kuusi rahavirhettä monivaluuttaisessa kulujen jaossa7 min lukuaika
- Näin tasaat ryhmän kulut harvemmilla siirroilla4 min lukuaika
- Eritellyn laskun jakaminen reilusti7 min lukuaika
- Vuokran reilu jako kämppisten kesken4 min lukuaika