Ana sayfa / Blog / Mühendislik
MühendislikBakiyeleri tam tutan salt ekleme defteri
Bölüşülen masraf bakiyelerini tam tutan salt ekleme defterinin mekanizması: sıfır toplamlı günlükler, düzeltme kayıtları ve commit sırasıyla gönderen bir outbox.
Artırdığınız bir bakiye, sapan bir bakiyedir. Dimesum hiçbir bakiyeyi artırmaz. Her masraf, kayıtları tam olarak sıfıra toplanan çift taraflı bir günlük yazar ve grup ekranınızdaki sayı, bu kayıtların üzerinde tek bir komutla silip yeniden kurabileceğimiz bir izdüşümdür.
Bu iddiayı sınayabilirsiniz. Paranın hareket etmesinin tek yolu salt ekleme yapılan bir günlükse, yanlış bir bakiye bir gizem olmaktan çıkar ve çalıştırabileceğimiz bir sorguya dönüşür.
Bakiye artırdığınız bir sayı değil, bir izdüşümdür
Dimesum'un defteri üç tabloya sahiptir: journals, postings ve gruba, üyeye ve para birimine göre anahtarlanan bir balances izdüşümü. Gerçeği kayıtlar taşır. Bir üye gruba para koyduğunda kayıt pozitif, değer tükettiğinde negatiftir, dolayısıyla bakiye sade bir SUM(amount_minor) değeridir. İzdüşüm yetki için değil, hız için vardır: günlüğün kendi transaction'ı içinde yazılır ve kayıtların eksiksiz olması onu atılabilir kılar.
İki katman aynı sıfırı doğrular ve hiçbiri tek başına yeterli değildir
Go tarafında buildPostings, kayıt kümesi sıfıra toplanmadıkça bir transaction açmayı reddeder. Postgres tarafında ertelenmiş bir kısıt tetikleyicisi, commit anında her günlük için SUM(amount_minor) = 0 koşulunu yeniden denetler, çünkü kayıtlar satır satır eklenir ve satır bazlı bir denetim her günlüğün ilk ayağını reddederdi. Assert, hesaplayıcıdaki bir hatayı yakalar. Tetikleyici, onu hiç çağırmayan bir yazıcıyı yakalar.
Üçüncü katman bir yetkilendirmedir. UPDATE, DELETE ve TRUNCATE, her iki tabloda ledger_app rolünden geri alınmıştır, böylece hatalı kod denese bile geçmişi yeniden yazamaz.
| Hata sınıfı | Artırılan bakiye tablosu | Salt ekleme defteri |
|---|---|---|
| Borçludan alındı, ödeyene hiç alacak yazılmadı | Sonsuza dek sessizce yanlış | Sıfır toplam denetimi başarısız olur, yazma reddedilir |
| Bir pay değişir, toplam değişmez | Bakiyeler sessizce sapar | Günlük dengesiz olarak commit edilemez |
| "Bakiyem neden 412 lira?" | Yanıtlanamaz | Her kuruş bir günlüğe kadar izlenir |
| Üretimde bir acil düzeltme | İzlenmeyen değişiklik | Tek yol yeni ve denetlenen bir günlüktür |
Düzenleme bir ters kayıt yazar, asla UPDATE değil
Bir masrafı düzenlemek, eski sürümün üzerine hiçbir şey yazmaz. Defter, tek bir expense.amended olayını tüketir ve tek bir transaction içinde iki günlük yazar: değiştirilen sürümün ayak ayak tam olumsuzlaması olan bir EXPENSE_REVERSAL, ardından yenisi için taze bir EXPENSE. Bir silme, ters kayıttan sonra durur.
Tek transaction, iki günlük kadar önemlidir. Ters kayıt tek başına commit edilseydi, bir grup hâlâ borçlu olduğu bir masraf için kısa süreliğine hiçbir şey borçlu olmazdı.
Her iki idempotency anahtarı da üretilmez, sürümden türetilir: sürüm için expense:<id>:v<n> ve olumsuzlama için önceki sürümün anahtarı artı :reversal. journals.idempotency_key UNIQUE'tir, böylece yeniden teslim edilen bir olay günlüklerini zaten orada bulur ve hiçbir şey yapmaz. En az bir kez teslimatı güvenli kılan şey türetmedir: bir yeniden deneme, ilk denemenin kullandığı anahtarı hesaplar.
Commit sırası filigranı, bir değişikliği masrafının ardında tutar
Dimesum'un yayımladığı her olay, iş yazımıyla aynı transaction içinde bir outbox tablosuna yazılır. Masraf commit edilirse, duyuru vardır. Geri alınırsa, duyuru da geri alınır. Defter, var olmayan bir masraftan asla haberdar olmaz.
Gönderim sırası daha zor olan yarıdır. Kimlikler UUIDv7'dir ve zamana göre sıralanır, ancak kimliğin ne zaman üretildiğini kodlarlar, transaction'ının ne zaman commit edildiğini değil. Kimlik sırasına göre okuyan bir aktarıcı, daha erken bir kimlik alıp daha sonra commit eden bir transaction'ı atlayabilir, böylece bir değişiklik, değiştirdiği masrafın önüne geçer.
Çözüm bir sütun ve bir yüklemdir. Her outbox satırı inserted_xid xid8 DEFAULT pg_current_xact_id() taşır ve aktarıcı yalnızca WHERE inserted_xid < pg_snapshot_xmin(pg_current_snapshot()) satırlarını, o xid'e göre sıralı okur (bkz. PostgreSQL transaction kimliği işlevleri). Nedensel olarak sonraki bir olay her zaman daha büyük bir xid taşır, çünkü var olabilmek için önceki satırı okuması gerekmiştir.
Değişiklik tüketicisi bu sıralamaya güvenerek yaklaşmaz. Öncülünün günlüğü olmayan bir değişiklik, henüz yeniyken yeniden teslim edilir ve iki dakikalık bir tolerans aşıldığında bir insan için bekletmeye alınır.
Para int64 alt birimdir ve üs her zaman iki değildir
Her tutar, alt birimlerin bir int64 sayımı artı bir ISO 4217 kodudur, yani 1.234,56 rupi {Minor: 123456, Currency: "INR"} olur. Tam sayılar disiplinle değil, yapısı gereği kesindir: temsil edilebilir hiçbir değer yarım kuruş değildir, dolayısıyla hiçbir işlem sessizce böyle bir değer üretemez. Python yardımcı bileşeni kendi AST'sini okur ve float sözcüğü tutar modülünde geçiyorsa bir testi başarısız kılar.
Asıl tuzak, alt birimin yüzde bir olduğunu varsaymaktır. JPY'nin hiç alt birimi yoktur ve KWD'nin üç ondalığı vardır, dolayısıyla ana birimler arasında verilip alt birimlere uygulanan bir kur, on kat yanlış olur. 2.000 yeni 0,58 ile alın: naif çarpım 1160'tır, bu 11,60 rupi olarak okunur, oysa yanıt 1.160'tır.
WRITE_OFF beşinci günlük türüdür çünkü kayıtları bir ödeşmeninkiyle aynıdır
Bir alacak silme, bir ödeşmenin yazdığı aynı iki ayağı yazar: borçlu yükselir, alacaklı düşer, aynı tutarda. Bunu SETTLEMENT içine katlamak, tam da şekillerin örtüşmesi nedeniyle reddedildi. "Asha sana 500 lira ödedi" ile "Asha'nın 500 lirasını bağışladın" farklı gerçeklerdir ve ikisini birbirine karıştıran bir akış, kimse ödemediği hâlde birinin ödediğini söylerdi.
Böylece WRITE_OFF, 2026-08-21 tarihinde kendi ayaklarıyla birlikte günlük türü CHECK'ine katıldı. Onları adlandırmak işin yarısıydı, çünkü SETTLE_PAY'i yeniden kullanan bir alacak silme, her "gerçekte ne kadar ödendi" sorgusunu sessizce yanlış yapardı.
| Günlük türü | Ne zaman yazılır | Ayaklar |
|---|---|---|
EXPENSE | Oluşturulduğunda ya da yeni bir sürüm birinin yerine geçtiğinde | PAID, SHARE |
EXPENSE_REVERSAL | Düzenlendiğinde ya da silindiğinde | Önceki günlüğün ayakları, olumsuzlanmış |
SETTLEMENT | Bir geri ödeme bildirildiğinde | SETTLE_PAY, SETTLE_RECV |
SETTLEMENT_REVERSAL | Karşı taraf itiraz ettiğinde | Her iki ödeşme ayağı, olumsuzlanmış |
WRITE_OFF | Bir alacaklı bir talepten vazgeçtiğinde | WRITE_OFF_FORGIVEN, WRITE_OFF_GRANTED |
İki alt komut, değişmezleri bir cron işine dönüştürür
evenly verify-ledger, değişmezleri tüm şema üzerinde yeniden kanıtlar ve herhangi bir isabet olduğunda sıfırdan farklı çıkar. Raporunun dört alanı vardır ve hepsinin boş olması beklenir: sıfıra toplanmayan günlükler, sıfıra toplanmayan gruplar, yeniden hesaplanan bir SUM(postings) ile ayrışan izdüşüm satırları ve bir insanı bekleyen bekletmedeki olaylar. Tüm taramalar tek bir repeatable-read anlık görüntüsünü paylaşır, böylece doğrulama sırasında düşen bir günlük bir uyumsuzluk uyduramaz.
Her tarama, kimliğe olduğu kadar para birimine göre de gruplar ve bununla kaçınılan başarısızlık bir yanlış negatiftir. 500 rupilik bir kayıt ile eksi 500 yenlik bir kayıt, bir sorgu para birimi sütununu göz ardı ettiğinde sıfıra toplanır, böylece iki kat bozuk bir defter temiz görünürdü.
evenly rebuild-balances [group-id] hem onarım hem de tatbikattır. İzdüşümü temizler ve her satırı kayıtlardan yeniden hesaplar, canlı yazımın yaptığı gibi her birini o üyeyi hareket ettiren son günlükle damgalar. Özellik satır satır eşitliktir: bir yeniden kurma ile canlı izdüşüm uyuşmadığında, haklı olan defterdir.
Önce doğrula, sonra yeniden kur. Rapor, sapan her satır için saklanan bakiyeyi ve yeniden hesaplananı adlandırır ve bir yeniden kurma saklanan değerin üzerine yazar, dolayısıyla önce yeniden kurmak kanıtı yok eder.
Bakiyeyi türetilebilir yapın, sapma bir sorguya dönüşsün
Salt ekleme yapılan bir defter, ikinci günlüğünü ancak kanıtlayabildiğinizde hak eder, bu yüzden doğrulayıcıyı ve yeniden kurmayı, onlara ihtiyaç duyan özellikten önce inşa edin. Hiç kimsenin çalıştırmadığı bir yeniden kurma bir kaçış kapağı değil, bir umuttur. Bu hafta en riskli para izdüşümünüzü seçin, onu kaynaktan yeniden hesaplayan sorguyu yazın ve ikisi uyuşmadığında kendinize çağrı gönderin.
Sık sorulan sorular
Masraf paylaşım uygulamasında salt ekleme defteri nedir?
Salt ekleme defteri, her para olayını sıfıra toplanan kayıtlardan oluşan bir günlük olarak kaydeder ve hiçbirini güncellemez ya da silmez. Dimesum'da bir masraf, bir düzenleme, bir ödeşme, bir itiraz ve bir alacak silme, her biri yeni bir günlük ekler. Bakiyeler daha sonra kayıtlar toplanarak türetilir; böylece her kuruş, onu hareket ettiren olaya kadar izlenir.
Salt ekleme defteri düzenlenen bir masrafı nasıl işler?
Bir düzenleme, tek transaction içinde iki günlük yazar: özgün kaydı ayak ayak olumsuzlayan bir EXPENSE_REVERSAL, ardından yerine gelen sürümü taşıyan yeni bir EXPENSE. Hiçbir şey yeniden yazılmaz ve bir silme yalnızca ters kaydı yazar. Her iki günlük de masraf sürümünden türetilen idempotency anahtarları alır; böylece yeniden teslim edilen bir olay onları zaten orada bulur ve hiçbir şeyi değiştirmez.
Para neden float yerine tam sayı olarak saklanır?
Tam sayı alt birimler yapısı gereği kesindir, ikili kayan nokta ise 0,1'i tam olarak temsil edemez ve bir grubun geçmişi boyunca sapar. Dimesum her tutarı, alt birimlerin bir int64 sayımı artı bir ISO 4217 kodu olarak saklar. Üs para biriminden gelir: JPY'nin hiç alt birimi yoktur, dolayısıyla yüzde bir varsaymak 100 kat hatadır.
Bölüşülen masraf bakiyelerinin sapmadığını nasıl anlarsınız?
evenly verify-ledger komutunu çalıştırın; değişmezleri tüm şema üzerinde yeniden kanıtlar ve herhangi bir isabet olduğunda sıfırdan farklı çıkar. Sıfıra toplanmayan günlükleri, sıfıra toplanmayan grupları, yeniden hesaplanan bir SUM(postings) ile uyuşmayan izdüşüm satırlarını ve bekletmedeki olayları raporlar. Onu her gece cron ile çalıştırın ve bozuk bir izdüşümü evenly rebuild-balances ile onarın.
Alacak silme neden kendi günlük türüne ihtiyaç duyar?
Bir alacak silme, bir ödeşmeninkiyle birebir aynı ayakları yazar; günlük türünün farklı olması gerekmesinin nedeni tam da budur. 'Asha sana 500 lira ödedi' ile 'Asha'nın 500 lirasını bağışladın' ifadelerini yalnızca o sözcük ayırır ve ikisini karıştıran bir akış, kimse ödemediği hâlde birinin ödediğini söylerdi. Ayakları WRITE_OFF_FORGIVEN ve WRITE_OFF_GRANTED olarak adlandırılır, böylece ödeme sorguları doğru kalır.
Popüler yazılar
- Masrafı düzenlemek bölüşümü neden yeniden belirtmeli7 dakika okuma
- Çok para birimli giderlerde altı para hatası8 dakika okuma
- Hesaplaşma: grup harcamalarını daha az transferle kapatın4 dakika okuma
- Paylaşılmayan bir yemek olduğunda hesap nasıl bölünür7 dakika okuma
- Kira paylaşım rehberi: ev arkadaşlarıyla adil bölüşün4 dakika okuma