1. Soalan
Apabila pesanan dibuat, perkhidmatan pesanan mesti melakukan komit pada keadaan dan menerbitkan peristiwa OrderCreated untuk pengguna inventori dan pemberitahuan. Pangkalan data dan broker tidak mempunyai komit dua fasa (two-phase commit) yang dikongsi. Reka bentuk transactional outbox supaya ranap sistem proses tidak menghilangkan peristiwa secara senyap, sementara penghantaran pendua dan tunggakan geganti kekal terkawal.
2. Kekangan dan penjelasan
- Data pesanan dan jadual outbox berkongsi satu pangkalan data transaksional tempatan.
- Broker menyediakan penghantaran sekurang-kurangnya sekali (at-least-once delivery), tanpa pengisihan global atau penghantaran transaksional.
- Ketekalan akhirnya (eventual consistency) boleh diterima; pengguna inventori mestilah idempoten.
- Terangkan pengisihan mengikut agregat, sama ada pengisihan rentas agregat diperlukan, dan tetingkap pengekalan/pemadaman.
3. Pendekatan teras
Tulis perubahan pesanan dan satu baris outbox dalam transaksi pangkalan data yang sama. Baris tersebut membawa event_id unik, kunci agregat, jenis peristiwa, jujukan, muatan, masa penciptaan, dan keadaan terbitan. Komit yang berjaya menjadikan kedua-dua data perniagaan dan peristiwa yang belum selesai tahan lama; pengembalian (rollback) tidak mendedahkan kedua-duanya, sekali gus menghapuskan tetingkap penulisan dwi di peringkat aplikasi.
Geganti bebas melakukan pengundian (polling) atau melanggan outbox, menerbitkan ke broker, dan kemudian menandakan baris tersebut sebagai dihantar. Jika proses terhenti antara pengesahan penerimaan (acknowledgement) broker dan kemas kini status, peristiwa tersebut boleh diterbitkan semula. Oleh itu, pengguna menyahduplikasi mengikut event_id dan bukannya menganggap penghantaran tepat sekali (exactly-once delivery).
4. Pelaksanaan rujukan
createOrder(command):
begin transaction
order = insert orders(...)
event = insert outbox(
event_id=uuid(), aggregate_id=order.id,
aggregate_version=order.version, type="OrderCreated",
payload=serialize(order), status="pending"
)
commit
return order.id
relayBatch():
rows = select pending outbox rows
order by aggregate_id, aggregate_version, created_at
for update skip locked limit BATCH_SIZE
for row in rows:
try:
broker.publish(key=row.aggregate_id, id=row.event_id, body=row.payload)
mark_sent(row.event_id) // conditional update
except transient_error:
increment_attempts_and_schedule_retry(row.event_id)
consume(message):
begin transaction
inserted = insert processed_messages(message.id) on conflict do nothing
if inserted:
apply_business_change(message)
commit5. Kebolehpercayaan dan ketepatan
Jika transaksi perniagaan dikomit tetapi geganti ranap sebelum penerbitan, baris yang belum selesai akan ditemui melalui imbasan kemudian. Jika penerbitan berjaya tetapi kemas kini status ranap, pusingan seterusnya akan menerbitkannya semula. Oleh itu, semantik hujung-ke-hujung adalah sekurang-kurangnya sekali; jadual penyahduplikasian pengguna atau kunci keidempotensian perniagaan mesti berkongsi transaksi dengan kemas kini perniagaan pengguna.
Pengisihan mengikut agregat boleh menggunakan versi monotonik dan pemetakan mengikut kunci agregat; jangan menjanjikan pengisihan global merentas agregat. Medan SELECT ... FOR UPDATE SKIP LOCKED atau pajakan (lease) menghalang berbilang geganti daripada menuntut baris yang sama, tetapi ia tidak menggantikan keidempotensian pengguna. Indeks keadaan, masa percubaan semula, dan masa penciptaan, kemudian arkibkan atau padamkan baris lama dengan selamat untuk mengehadkan pertumbuhan jadual.
6. Tindakan susulan dan perangkap
- Memadamkan baris serta-merta selepas penerbitan "berjaya" boleh mewujudkan jurang yang tidak boleh dipulihkan jika pengesahan penerimaan hilang; kekalkan keadaan penghantaran atau simpan rekod audit terlebih dahulu.
- Had masa tamat pengesahan penerimaan broker tidak membuktikan bahawa broker terlepas mesej tersebut, jadi percubaan semula mesti bertolak ansur dengan pendua.
- Menulis ke pangkalan data terlebih dahulu dan kemudian memanggil broker di dalam pengendalian ralat aplikasi masih mempunyai perlumbaan penulisan dwi; try/catch tidak dapat menjadikannya atomik.
- Jika outbox dan jadual perniagaan tidak boleh berkongsi sempadan transaksi, gunakan CDC, pemesejan transaksional, atau takrifkan semula jaminan ketekalan.
7. Bacaan lanjut
Bandingkan geganti pengundian dengan geganti CDC: pengundian lebih mudah digunakan tetapi menambah imbasan dan kependaman, manakala CDC mengurangkan kependaman dengan kos tangkapan log dan kebergantungan operasi. Bincangkan mesej beracun (poison messages), penundaan eksponen (exponential backoff), baris gilir surat mati (dead-letter queues), pemantauan usia tertunda, dan keserasian skema pengguna.
8. Mata pemarkahan temu duga
Boleh mengesan tetingkap penulisan dwi
Calon harus menerangkan sebab transaksi tempatan biasa tidak boleh mengomitkan kemas kini pangkalan data dan penghantaran broker bersama-sama, kemudian meletakkan perubahan pesanan dan baris outbox dalam satu transaksi.
Boleh menerangkan sekurang-kurangnya sekali dan keidempotensian
Mereka harus menerangkan tetingkap ranap geganti yang mewujudkan pendua dan membuat pengguna menyahduplikasi mengikut ID peristiwa dalam transaksi yang sama seperti kemas kini perniagaannya.
Boleh mengendalikan pengisihan dan kebersamaan
Mereka harus membezakan susunan mengikut agregat daripada susunan global dan menerangkan cara kunci petak, versi, kunci (locks), atau pajakan mengehadkan tuntutan serentak.
Boleh merangkumi sempadan operasi
Mereka harus mencadangkan penundaan percubaan semula, surat mati, amaran tunggakan, pembersihan arkib, dan evolusi skema dan bukannya hanya berhenti pada definisi jadual.