Prompt dan Konteks yang Berlaku
PostgreSQL adalah source of truth untuk data produk. Redis menyimpan snapshot tampilan yang diberi kunci product_id. Sistem ini menangani sekitar 20.000 read dan 500 write per detik di sekitar 2.000 produk aktif. Nama, gambar, dan teks tampilan boleh usang (stale) paling lama 5 detik. Pemotongan inventaris, harga checkout, izin (permission), dan saldo berada di luar kontrak cache tersebut karena menentukan kebenaran bisnis (business correctness).
Rancang jalur cache-aside read, update, dan invalidasi. Cakup kasus-kasus berikut:
- database melakukan commit, tetapi proses mengalami crash sebelum menghapus entri cache;
- sebuah reader mendapatkan versi database yang lama dan menyelesaikan fill-nya hanya setelah writer menginvalidasi cache;
- sebuah fill membaca data yang tertinggal (lagging) dari replika database asinkron;
- event invalidasi terduplikasi, urutannya tertukar, atau mengalami backlog;
- Redis tidak tersedia untuk sementara;
- tim produk menginginkan “paling lama usang 5 detik” menjadi kontrak yang dipantau dan diuji.
Throughput, jumlah active key, dan anggaran 5 detik adalah asumsi wawancara, bukan pengaturan universal. Materi wawancara backend dan caching tingkat senior publik tahun 2026 saat ini secara eksplisit mencakup cache-aside, invalidasi setelah write, dan konsistensi cache. Keahlian utamanya adalah protokol backend dan semantik kegagalan setelah commit database, sehingga kategorinya adalah backend. Ini berbeda dari mencegah cache stampede saat hot key kedaluwarsa: pengendalian stampede membatasi concurrent read ke sumber data, sedangkan masalah ini menanyakan kapan nilai lama dapat bertahan setelah operasi write, mengapa nilai tersebut bertahan, dan berapa lama nilai itu boleh ada.
Apa yang Dievaluasi oleh Pewawancara
Pertama, dapatkah kandidat mendefinisikan target konsistensi sebelum memilih pola? “Database dan cache selalu konsisten” tidak memerinci hasil read mana yang sah. Jawaban yang kuat memisahkan bounded staleness 5 detik untuk data tampilan, read-your-writes untuk pemanggil (caller) yang melakukan pembaruan, dan authoritative read untuk inventaris atau izin. Kontrak-kontrak tersebut membutuhkan jalur yang berbeda.
Kedua, dapatkah mereka menjelaskan urutan write? Jalur write cache-aside yang umum melakukan commit ke database lalu menghapus entri cache. Menghapus terlebih dahulu menciptakan race condition yang jelas: sebuah reader mengalami miss di antara kedua operasi tersebut, mengambil nilai database yang lama, dan menaruhnya kembali ke dalam cache. Mendahulukan database tetap bukan merupakan dual write yang atomik. Ada jendela commit-ke-delete yang singkat, dan penghapusan yang gagal dapat meninggalkan entri lama hingga batas TTL-nya.
Ketiga, dapatkah mereka menggambarkan linimasa late stale-fill? Sebuah reader mungkin mengambil versi 41 sebelum pembaruan database. Seorang writer kemudian melakukan commit versi 42 dan menghapus cache, setelah itu reader lama menaruh versi 41 kembali. Menambahkan versi hanya pada nilai yang di-cache tidak selalu cukup. Setelah penghapusan, tidak ada versi ter-cache yang dapat dibandingkan, sehingga aturan seperti “tulis ketika versi sumber setidaknya sama dengan versi ter-cache” akan menerima versi 41. Desain ini memerlukan version fence yang bertahan melewati penghapusan nilai, lease fill yang diinvalidasi oleh operasi write, atau pemeriksaan versi otoritatif lainnya sebelum proses fill.
Keempat, dapatkah mereka membedakan invalidasi yang andal dari batas waktu yang terhingga? Transactional outbox atau change data capture menutup celah di mana database melakukan commit tetapi tidak ada pesan invalidasi yang tercatat secara tahan lama (durable). Pengiriman at-least-once dan penghapusan idempoten dapat mentoleransi duplikasi. Percobaan ulang (retry) memastikan pemrosesan akhirnya terjadi (eventual processing); hal itu tidak membuktikan penyelesaian dalam waktu 5 detik. Bounded staleness secara tambahan membutuhkan hard TTL, invalidation-lag gate, atau aturan yang melewati (bypass) cache begitu pipeline melebihi anggarannya.
Kelima, dapatkah mereka menangani source of truth, replika database, dan verifikasi operasional? Commit database yang berhasil menciptakan fakta baru. Melakukan fill dari replika yang tertinggal dapat memasukkan kembali nilai lama setelah invalidasi yang benar. Jawaban yang kuat membatasi sumber fill, mengamati versi event dan cache, serta menguji linimasa konkurensi terkontrol dan kegagalan alih-alih hanya melaporkan cache hit rate.
Pertanyaan yang Perlu Diklarifikasi Sebelum Menjawab
- Apa kontrak konsistensinya? Jika keusangan hingga 5 detik diizinkan, invalidasi asinkron ditambah batas waktu ketat dapat digunakan. Jika read-your-writes diperlukan, permintaan berikutnya dari writer harus melewati cache secara singkat atau membawa versi minimum. Jika pembacaan data usang tidak diizinkan sama sekali, bacalah storage otoritatif atau gunakan jalur penyimpanan dengan kontrak konsistensi yang diperlukan.
- Field mana yang mengotorisasi tindakan yang tidak dapat dibatalkan (irreversible)? Nama tampilan atau gambar boleh saja usang. Validasi inventaris, kelayakan diskon, izin, saldo, dan harga yang ditagihkan tidak boleh memperlakukan snapshot cache sebagai otoritas. Jika field-field tersebut berada dalam satu objek, pisahkan kontrak read-nya atau baca ulang state otoritatif selama tindakan kritis berlangsung.
- Kapan penghitungan waktu 5 detik dimulai? Dalam prompt ini, waktu dimulai saat commit database. Jika dimulai saat nilai masuk ke Redis, replika yang sudah tertinggal 4 detik dapat di-cache selama 5 detik lagi, membuat usia data sebenarnya mendekati 9 detik.
- Apakah satu layanan mengontrol setiap jalur write? Batch job, admin tool, dan layanan lain dapat melewati
DELtingkat API. Letakkan catatan invalidasi dalam transaksi database yang sama atau tangkap setiap perubahan yang didukung dari log database. - Apakah cache miss melakukan fill dari primary atau replika? Tentukan batas lag replika dan apakah read-your-writes tingkat sesi tersedia. Jika replika tidak dapat dibuktikan mutakhir dalam batas anggaran, fill pertama setelah invalidasi harus menggunakan primary atau memerlukan hasil yang setidaknya sama barunya dengan versi minimum milik caller.
- Berapa banyak traffic fill yang dapat diserap database? Dengan TTL 5 detik, 2.000 active key yang kedaluwarsa secara seragam dan semuanya dibaca menghasilkan rata-rata sekitar
2000 / 5 = 400fill per detik. Distribusi akses, jitter, dan request coalescing akan mengubah nilai sebenarnya. Jika database tidak memiliki kapasitas anggaran tersebut, memperpendek TTL tidak serta-merta memenuhi konsistensi. - Ketika Redis tidak tersedia, mana yang lebih penting: kesegaran (freshness) atau ketersediaan (availability)? Data tampilan dapat mengalami degradasi dalam batas nilai usang yang eksplisit. Field otoritatif harus menggunakan jalur sumber yang terlindungi. Jika database tidak dapat menyerap semua cache miss, terapkan rate limit, bulkhead, dan kegagalan eksplisit daripada fallback tanpa batas.
Kerangka Jawaban 30 Detik
“Pertama-tama saya mendefinisikan kontrak berdasarkan tipe data. Data tampilan produk boleh usang paling lama 5 detik sejak commit database, sedangkan inventaris dan izin selalu menggunakan jalur otoritatif. Pembacaan memeriksa Redis, menggabungkan (coalesce) miss untuk kunci yang sama, mengambil baris berversi dari sumber yang memenuhi persyaratan kesegaran data, dan mengisi cache secara kondisional. Operasi write memperbarui baris bisnis dan menyisipkan catatan outbox yang berisi versi baris dalam satu transaksi PostgreSQL. Setelah commit, sistem mencoba penghapusan cepat pada cache, sementara jalur outbox atau CDC menyediakan perbaikan yang dapat dicoba ulang (retryable).
Saya melakukan commit database sebelum menghapus cache. Untuk menghentikan pembacaan lama melakukan fill setelah penghapusan tersebut, sebuah cache miss menangkap fill generation; invalidasi memajukan version fence dan menghapus nilai; proses fill hanya berhasil jika generation tidak berubah dan versi sumber memenuhi fence. Retry eventual tidak membuktikan batas 5 detik, sehingga cache memiliki hard TTL dalam batas anggaran, dan pembacaan melewati cache saat lag invalidasi mendekati batas tersebut. Fill setelah invalidasi menggunakan primary ketika lag replika tidak dapat memenuhi kontrak. Saya memverifikasi desain ini dengan pengujian kegagalan commit-then-crash, late-fill, event duplikat dan yang tertukar urutannya, serta lag replika, dengan menegaskan stale age, versi monotonik, dan beban database.”
Pembahasan Mendalam Langkah demi Langkah
Langkah 1: Ubah konsistensi menjadi kontrak hasil pembacaan (read-result contract)
Pisahkan data berdasarkan konsekuensi bisnis sebelum memilih pola cache:
| Jalur | Hasil yang sah | Jalur read yang direkomendasikan |
|---|---|---|
| Tampilan produk | Paling lama usang 5 detik sejak commit database | Redis cache-aside, hard TTL, dan invalidation-lag gate |
| Read berikutnya oleh writer | Setidaknya versi yang baru saja di-commit oleh caller tersebut | Bawa min_version; gunakan primary jika cache lebih lama |
| Inventaris, izin, saldo, checkout | Keputusan harus menggunakan state otoritatif saat ini | Lewati cache tampilan dan validasi di dalam transaksi atau layanan otoritatif |
Klasifikasi ini mendorong sisa desain berikutnya. Cache mempercepat salinan data yang dapat dibangun ulang. Cache tidak boleh mengotorisasi pengurangan inventaris menggunakan nilai yang dapat tertunda, ter-evict, atau hilang. Hit rate 99,9% tidak menjamin apakah 0,1% sisanya menyebabkan overselling atau akses yang tidak sah.
Setiap entri cache membutuhkan setidaknya versi sumber dan informasi waktu. Berikut ini adalah struktur semu (pseudostructure), bukan tipe data yang dapat dieksekusi dalam bahasa tertentu:
ProductCacheEntry {
value
source_version
source_committed_at
cached_at
}source_committed_at mengukur usia usang yang sebenarnya; cached_at hanya menunjukkan kapan Redis menerima nilai tersebut. Versi sumber dapat berupa versi baris yang meningkat secara monotonik, urutan commit, atau versi domain lain dengan urutan yang terdefinisi. Timestamp wall-clock saja merupakan bukti pengurutan yang buruk jika nilainya bisa sama persis atau jam sistem mengalami drift.
Langkah 2: Tetapkan jalur dasar cache-aside
Jalur read hanya mengembalikan cache hit jika memenuhi versi minimum pemanggil dan anggaran usia keusangan. Pada saat miss, gabungkan fill untuk product_id yang sama sehingga 20.000 read tidak mengakses database secara bersamaan. Ambil baris berversi, lalu coba penulisan kondisional ke Redis. Berikut ini adalah pseudocode alurnya:
read(product_id, min_version = none):
entry = cache.get(product_id)
if entry satisfies age_budget and min_version:
return entry.value
return singleflight(product_id):
recheck cache
row = read_authoritative_version(product_id)
conditional_fill(product_id, row)
return row.valueJalur write memperbarui baris bisnis dan menyisipkan catatan outbox dalam transaksi PostgreSQL yang sama. Hanya COMMIT yang berhasil yang membuat perubahan bisnis terlihat oleh transaksi lain dan bersifat durable. Aplikasi kemudian melakukan upaya invalidasi cepat. Relay atau konsumen CDC terpisah memproses event invalidasi yang durable tersebut untuk retry dan perbaikan. Pseudocode jalur write adalah:
transaction:
row = update product and increment source_version
insert outbox(product_id, source_version, committed_at)
commit
best_effort_invalidate(product_id, source_version)
return committed source_versionInvalidasi langsung mengurangi jendela jeda pada kasus umum (common-case). Outbox menutup celah hilangnya pesan saat proses mengalami crash setelah commit. Jika kedua jalur menangani perubahan yang sama, penghapusan harus bersifat idempoten: menerima versi sumber yang sama lagi tidak boleh menimbulkan efek samping bisnis yang baru.
Langkah 3: Analisis tiga jendela race condition secara eksplisit
Jendela 1: antara commit database dan penghapusan cache.
W: COMMIT version 42
R: read cached version 41
W: DEL cache keyReader melihat versi 41 secara singkat, yang mana hal ini sah di bawah bounded staleness. Jika tidak ada hasil usang yang dapat diterima, menunggu operasi cache selesai sebelum mengembalikan respons write tetap tidak menyelesaikan setiap ambiguitas jaringan. Kontrak yang lebih jelas adalah mengirimkan read kritis ke source of truth atau mengizinkan pemanggil mewajibkan min_version=42.
Jendela 2: database melakukan commit tetapi invalidasi tidak berjalan.
W: COMMIT version 42 plus outbox record
W: process crashes before DEL
R: cache still contains version 41
relay: retries invalidation for version 42Tugas in-memory yang dibuat setelah COMMIT dapat hilang. Transactional outbox melakukan commit pada perubahan bisnis dan kewajiban invalidasi secara bersamaan. Relay dapat mengirim ulang, sementara konsumen menghapus berdasarkan kunci dan mencatat versi yang telah ditangani. Jika relay macet, hard TTL atau freshness gate akan menegakkan batas 5 detik.
Jendela 3: read lama melakukan fill setelah invalidasi.
R: cache miss; captures generation 7
R: reads database version 41
W: commits version 42
W: advances generation to 8 and deletes cache value
R: tries to fill version 41 with generation 7; rejectedIni adalah race condition yang sering terlewatkan. Jika desain hanya menghapus nilai, maka version 41 >= no version lolos dan membangkitkan kembali data usang. Salah satu solusinya adalah memisahkan nilai dari fence-nya. Invalidasi memajukan kunci generation atau minimum-source-version yang berumur pendek. Script Redis secara atomik memeriksa bahwa generation saat fill masih cocok dengan generation yang ditangkap saat miss terjadi dan versi sumber memenuhi batas minimum sebelum menuliskan nilai. Di Redis Cluster, kunci nilai dan fence harus menggunakan hash slot yang sama agar script dapat mengakses keduanya secara atomik. Implementasi lain menerbitkan miss lease yang akan diinvalidasi oleh pembaruan database. Membaca ulang versi primary sebelum fill dapat membantu, tetapi menambah beban pembacaan database dan tetap memerlukan batas atomik yang terdefinisi antara pemeriksaan dan penulisan cache.
Pertahankan version fence cukup lama untuk mencakup durasi fill maksimum, retry, serta jeda proses atau jaringan. Menghapusnya terlalu cepat akan membuka kembali celah late-fill. Fence melindungi pengurutan penulisan cache; fence tidak menggantikan kontrol konkurensi database atau memberi tahu reader biasa bahwa database berisi versi yang lebih baru yang invalidasinya belum tiba.
Langkah 4: Buat invalidasi dapat dipulihkan tanpa mengklaim pengiriman yang berlebihan
Baris outbox dan pembaruan bisnis ditulis dalam satu transaksi. Sebuah relay mengirimkan invalidasi ke saluran yang durable. Konsumen dapat melacak versi tertinggi yang diamati untuk setiap product_id dan menerapkan aturan berikut:
- Event out-of-order di bawah versi tertinggi yang diamati di-acknowledge secara idempoten.
- Versi baru memajukan minimum-version fence dan menghapus nilai yang di-cache.
- Perintah cache yang gagal dicoba ulang; pekerjaan yang habis jatah percobaannya masuk ke antrean karantina yang terpantau.
- Pekerjaan rekonsiliasi membandingkan versi database, progres outbox, dan sampel versi cache.
Pengiriman at-least-once bekerja dengan baik dengan operasi penghapusan karena duplikasi DEL tidak memiliki arti bisnis tambahan. Jangan mengklaim bahwa retry menciptakan eksekusi exactly-once. Konsumen dapat menghapus di Redis, mengalami crash sebelum meng-acknowledge pesan, dan menghapus lagi. Pengulangan yang aman dan celah yang dapat dideteksi adalah properti yang berguna.
Amati interval dari commit database hingga invalidasi berhasil, bukan hanya usia antrean broker. Metrik yang berguna meliputi invalidation_lag_seconds, kegagalan dan retry invalidasi, usia karantina tertua, lag versi cache-ke-sumber, bypass cache akibat melebihi anggaran, QPS fill database, dan jumlah panggilan kunci serupa yang digabungkan melalui singleflight.
Langkah 5: Buktikan batas 5 detik dengan TTL dan gate
Event yang andal pada akhirnya akan tiba, tetapi "pada akhirnya" (eventually) tidak memiliki satuan waktu. Kontrak paling lama usang 5 detik sejak commit memerlukan pertahanan waktu berhingga (finite-time) yang independen:
- Atur TTL fisik cache di bawah 5 detik setelah mencadangkan margin untuk waktu jam sistem, penjadwalan, dan deteksi, lalu hitung usia usang dari waktu commit versi otoritatif.
- Atau, ketika lag invalidasi mendekati anggaran, hentikan kepercayaan pada cache secara global atau berdasarkan partisi yang terpengaruh dan baca sumber yang memenuhi persyaratan kesegaran.
- Jika keduanya tidak memungkinkan, kontrak tersebut adalah konsistensi eventual dan tidak boleh terus mengklaim batas 5 detik.
TTL adalah pelindung batas akhir (boundary backstop), bukan mekanisme invalidasi utama. Tambahkan jitter agar 2.000 kunci tidak kedaluwarsa secara bersamaan, dan gabungkan cache miss untuk setiap kunci. Jika semua 2.000 active key dibaca setidaknya sekali per interval 5 detik, masa kedaluwarsa yang terdistribusi secara seragam menghasilkan rata-rata sekitar 400 fill per detik. Estimasi tersebut bukan jaminan kapasitas. Ketimpangan popularitas data, kedaluwarsa batch, kegagalan Redis, dan query yang lambat dapat menciptakan lonjakan beban, jadi ujilah terhadap QPS database yang aman dan bulkhead konkurensi.
Jika TTL 5 detik melebihi kapasitas database, ada tiga pilihan jujur: tambah kapasitas fill yang aman, kurangi data aktif yang membutuhkan kontrak ini, atau longgarkan anggaran keusangan. Menaikkan TTL secara diam-diam melanggar ketentuan prompt.
Langkah 6: Tangani lag replika dan read-your-writes
Bahkan setelah penghapusan yang benar, replika yang tertinggal dapat mengisi ulang versi lama. Untuk cache miss dengan kontrak ketat 5 detik, gunakan urutan prioritas berikut:
- Baca primary untuk fill pertama setelah invalidasi.
- Gunakan replika hanya jika posisi replay yang dapat diamati telah mencapai posisi commit yang diperlukan.
- Kembalikan
source_versiondari operasi write dan biarkan read berikutnya membawamin_version; rute ke primary saat cache atau replika tertinggal. - Buka freshness circuit breaker saat lag replika atau invalidasi melebihi anggaran, dan hentikan pengembalian nilai cache yang tidak memenuhi syarat.
TTL 5 detik tidak membuktikan usia data 5 detik jika fill berasal dari replika yang sudah tertinggal 4 detik. Usia data usang dimulai sejak commit pada source of truth, sehingga penundaan replika, event-channel, dan cache semuanya diperhitungkan.
Langkah 7: Bandingkan alternatif dan batasannya
Memperbarui cache secara sinkron. Menulis ke Redis segera setelah database dapat meningkatkan read-your-writes, tetapi dua sistem independen tetap memiliki risiko kegagalan parsial. Database dapat berhasil commit sementara pembaruan cache gagal, dan penulisan database yang bersamaan dapat mencapai cache dengan urutan berbeda. Jalur ini tetap membutuhkan kondisi versi sumber dan perbaikan; menyebutnya sebagai write-through tidak membuat kedua penulisan tersebut menjadi transaksi atomik.
Hapus terlebih dahulu, lalu perbarui database. Cara ini sederhana tetapi memungkinkan reader mengisi ulang data lama sebelum commit database. Penghapusan kedua yang ditunda mengurangi probabilitas waktu tertentu, tetapi penundaan tetap (fixed delay) tidak dapat mencakup lag replika tanpa batas, jeda proses, atau gangguan jaringan. Proses juga dapat mengalami crash sebelum penghapusan kedua. Ini mungkin tindakan pembantu, tetapi tidak membuktikan bounded staleness dengan sendirinya.
Hanya gunakan TTL pendek. Ini bisa menjadi pilihan memadai yang paling sederhana ketika write jarang terjadi, anggaran keusangan longgar, dan database dapat menyerap fill. Konsekuensinya adalah setiap write dapat diikuti oleh pembacaan usang selama durasi penuh TTL, sementara masa kedaluwarsa yang tersinkronisasi dapat melipatgandakan beban database.
Baca semuanya dari database. Untuk data dengan throughput rendah atau yang kritis terhadap kebenaran, ini sering kali merupakan solusi paling jelas. Cache bersifat opsional. Ketika biaya koordinasi konsistensi lebih mahal daripada pembacaan database yang dihematnya, menghapus cache adalah langkah yang masuk akal.
Langkah 8: Uji invarian dan jalur kegagalan
Lakukan lebih dari sekadar me-refresh halaman secara manual setelah pembaruan. Gunakan barrier untuk mereproduksi late stale fill. Beri jeda pada reader setelah mendapatkan versi 41. Biarkan writer melakukan commit versi 42, memajukan fence, dan menghapus nilainya. Lepaskan reader lama dan pastikan bahwa conditional fill-nya gagal. Kemudian uji kasus-kasus berikut:
- matikan writer segera setelah PostgreSQL melakukan commit dan verifikasi bahwa outbox pada akhirnya melakukan invalidasi;
- duplikasi dan ubah urutan satu event invalidasi lalu verifikasi bahwa versi tertinggi tidak pernah mengalami regresi;
- gagalkan proses delete Redis dan verifikasi retry, karantina, serta backstop TTL;
- jeda replay replika dan verifikasi bahwa proses fill menggunakan primary atau menolak hasil yang tidak memenuhi syarat;
- tunda konsumsi invalidasi melebihi batas gate-nya dan verifikasi bahwa pembacaan melewati cache;
- buat 2.000 kunci mendekati masa kedaluwarsa secara bersamaan dan verifikasi jitter, singleflight, serta bulkhead database;
- buat Redis tidak tersedia dan verifikasi degradasi sesuai anggaran untuk pembacaan tampilan sementara keputusan otoritatif tetap menjaga jalur yang diwajibkan.
Invarian intinya adalah: versi cache yang dikembalikan tidak pernah di bawah min_version milik pemanggil; fill generation yang diinvalidasi tidak dapat menulis; tindakan bisnis otoritatif tidak pernah mempercayai cache tampilan; usia usang tidak melebihi 5 detik; dan fill database tetap berada dalam batas QPS serta konkurensi aman yang teruji.
Contoh Jawaban Berkualitas Tinggi
“Saya tidak akan memulai dengan menjanjikan bahwa PostgreSQL dan Redis cocok di setiap saat. Saya akan memisahkan kontrak pembacaan. Nama dan gambar produk boleh usang paling lama 5 detik sejak commit PostgreSQL. Pengurangan inventaris, izin, saldo, dan keputusan checkout tetap menggunakan state otoritatif. Ketika writer membutuhkan read-your-writes, API penulisan mengembalikan versi sumber. Pembacaan berikutnya menyertakan versi minimum tersebut dan menggunakan primary saat Redis lebih lama.
Pola dasarnya adalah cache-aside. Read hanya mengembalikan hit jika usia dan versinya memenuhi syarat. Miss menggunakan singleflight berdasarkan product_id, mengambil baris berversi monotonik, dan melakukan conditional fill. Satu transaksi PostgreSQL memperbarui produk, menaikkan versinya, dan menyisipkan baris outbox. Setelah commit, aplikasi mencoba menghapus Redis secara langsung. Relay atau konsumen CDC mencoba ulang dari event yang durable, sehingga crash setelah commit tidak menghilangkan kewajiban invalidasi secara permanen.
Urutannya adalah database terlebih dahulu, lalu hapus cache. Menghapus lebih dulu memungkinkan reader memulihkan data lama sebelum database melakukan commit. Bahkan dengan urutan yang benar, potensi late fill tetap ada: reader mendapatkan versi 41, writer melakukan commit 42 dan menghapus, kemudian reader lama menetapkan versi 41. Versi di dalam nilai ter-cache saja tidak cukup karena tidak ada versi 42 yang tersisa setelah penghapusan. Saya membuat miss menangkap generation. Invalidasi memajukan generation dan versi minimum sebelum menghapus nilainya. Script atomik menolak fill kecuali generation tidak berubah dan versi sumber memenuhi fence. Jika replika tidak dapat membuktikan telah me-replay commit yang diperlukan, fill setelah invalidasi akan membaca primary.
Outbox membuktikan invalidasi eventual yang dapat dipulihkan, bukan batas waktu mutlak 5 detik. Saya menetapkan hard TTL dalam batas anggaran, dengan margin operasional, dan mengukur lag commit-ke-invalidasi. Saat lag tersebut mendekati batas anggaran, pembacaan akan melewati Redis. Jika semua 2.000 active key diakses setiap 5 detik, fill yang seragam rata-rata mencapai 400 per detik, jadi saya juga menggunakan jitter TTL, penggabungan permintaan per kunci, dan bulkhead konkurensi database.
Untuk verifikasi, saya mengontrol urutan thread dan menyuntikkan pengujian kegagalan commit-then-crash, late-reader, event duplikat dan urutan tertukar, kegagalan delete Redis, serta lag replika. Penerimaan sistem berarti generation lama tidak dapat melakukan fill, versi sumber tidak mengalami kemunduran, usia usang tetap dalam 5 detik, tindakan otoritatif melewati cache tampilan, dan beban database tetap berada dalam anggaran terukur.”
Kesalahan Umum
- Mengklaim “konsistensi kuat antara database dan cache” → Jawaban tidak mendefinisikan pembacaan yang sah, batas waktu kegagalan, atau batasan transaksi di kedua sistem → Nyatakan kontrak terpisah untuk bounded staleness, read-your-writes, dan pembacaan otoritatif.
- Menghapus cache sebelum memperbarui database → Miss di antara operasi tersebut akan membaca dan memulihkan nilai database yang lama → Commit database terlebih dahulu, lalu lakukan invalidasi, dengan perbaikan yang durable.
- Hanya memublikasikan pesan in-memory setelah commit → Crash sebelum publikasi akan menghilangkan invalidasi secara permanen → Tulis outbox dalam transaksi yang sama atau tangkap perubahan log database.
- Mengasumsikan
DELmengakhiri setiap race condition → Pembacaan yang lebih lama dapat menyelesaikan penulisan nilainya setelah proses penghapusan → Tolak nilai usang yang terlambat dengan fill lease atau version fence yang bertahan setelah penghapusan nilai. - Meletakkan versi hanya di dalam nilai cache → Cache yang kosong tidak memiliki versi lebih baru untuk dibandingkan dan dapat menerima nilai lama → Simpan versi minimum atau generation dalam fence terpisah dan periksa fill secara atomik.
- Memperlakukan konsumsi duplikat sebagai error → Crash pada konsumen sebelum pengakuan (acknowledgement) secara alami menciptakan pengiriman ulang → Buat penghapusan dan pemajuan versi tertinggi bersifat idempoten di bawah pengiriman at-least-once.
- Menggunakan outbox untuk menjanjikan 5 detik → Kemampuan retry membuktikan penanganan eventual, bukan penundaan waktu terhingga → Tambahkan hard TTL atau over-budget bypass gate dan ukur lag end-to-end commit-ke-invalidasi.
- Melakukan fill dari read replica mana pun → Lag replika dapat mempopulasikan kembali data lama setelah invalidasi yang benar → Periksa posisi replay, gunakan primary, atau wajibkan versi sumber minimum.
- Menyimpan semua field dalam satu snapshot tampilan → Kelonggaran keusangan data tampilan merembes ke inventaris, izin, dan checkout → Pisahkan kontrak data dan baca ulang state otoritatif untuk tindakan yang tidak dapat dibatalkan.
- Mengirimkan semua traffic ke database saat Redis gagal → 20.000 read per detik dapat membebani source of truth → Lindungi dengan bulkhead, rate limit, degradasi eksplisit, dan pemulihan terkontrol.
- Menggunakan delayed double delete dengan waktu tetap → Waktu tunggu (sleep) tidak dapat mencakup lag tanpa batas dan penghapusan kedua juga bisa hilang → Perlakukan sebagai optimasi probabilitas sambil tetap mempertahankan invalidasi durable, fence, dan backstop waktu terhingga.
Pertanyaan Lanjutan dan Tanggapan
Lanjutan 1: Bagaimana jika harga produk juga membutuhkan read-your-writes?
Kembalikan source_version yang telah di-commit dari API penulisan. Pembacaan berikutnya dalam sesi tersebut mengirimkan min_version; jika Redis lebih lama, baca primary dan simpan hanya versi baru ke cache secara kondisional. Jika writer hanya perlu menampilkan hasilnya, kembalikan baris yang di-commit secara langsung dan lewati cache secara singkat. Checkout tetap harus memvalidasi ulang harga dalam transaksi otoritatif; read-your-writes bukanlah otorisasi pembayaran.
Lanjutan 2: Mengapa version fence tidak bisa berada di dalam nilai yang di-cache saja?
Invalidasi menghapus nilai tersebut, sehingga perbandingan biasa tidak dapat lagi melihat versi 42. Permintaan yang sebelumnya membaca versi 41 kemudian dapat memperlakukan versi 41 sebagai nilai yang valid untuk kunci yang kosong. Fence terpisah atau miss lease tetap bertahan setelah penghapusan nilai dan mencatat bahwa versi 42 ada atau izin fill lama telah dicabut.
Lanjutan 3: Bisakah transactional outbox mengirimkan duplikat?
Ya. Relay dapat mengalami crash setelah sistem downstream menerima event tetapi sebelum menandai baris outbox selesai. Konsumen menangani (product_id, source_version) secara idempoten: versi lama tidak memajukan state, versi baru memajukan nilai maksimum dan menghapus nilai cache, serta penghapusan duplikat aman dilakukan. Jaminan yang berguna adalah tidak adanya kehilangan invalidasi tanpa terdeteksi ditambah rekonsiliasi, bukan eksekusi exactly-once.
Lanjutan 4: Bisakah TTL menjadi 30 menit jika CDC menangani invalidasi?
Itu bisa menjadi kompromi (trade-off) yang valid jika bisnis hanya membutuhkan konsistensi eventual dan menerima gangguan invalidasi yang berkepanjangan. Prompt ini menjanjikan paling lama usang 5 detik. TTL 30 menit melanggar batas tersebut ketika CDC macet kecuali jika sistem secara otomatis melewati cache saat lag invalidasi mendekati 5 detik. Jika tidak, pertahankan batas waktu ketat dalam anggaran.
Lanjutan 5: Lag replika biasanya hanya puluhan milidetik. Mengapa menggunakan primary?
“Biasanya” bukanlah batas atas. Deployment, partisi jaringan, transaksi panjang, dan pemulihan dapat meningkatkan lag. Replika tetap dapat digunakan jika posisi replay-nya terbukti mencakup commit yang diperlukan atau pembacaan membawa versi minimum. Jika itu tidak dapat dibuktikan, arahkan cache miss yang kritis ke primary. Pilihan ini mengikuti kontrak 5 detik dan anggaran kapasitas database.
Lanjutan 6: Mengapa tidak memperbarui PostgreSQL dan Redis sambil menahan satu distributed lock?
Lock hanya membatasi partisipan yang mengikuti protokol tersebut. Lock tidak dapat membuat dua sistem melakukan commit secara atomik, atau menghilangkan risiko crash pemegang lock, habisnya masa lock, partisi jaringan, atau writer database eksternal. Transaksi database menetapkan faktanya, dan invalidasi yang durable memperbaiki sistem kedua. Lock mungkin mengurangi konkurensi pada kunci yang sama, tetapi bukan bukti konsistensi.
Lanjutan 7: Bagaimana cara mempertahankan batas 5 detik saat Redis sama sekali tidak tersedia?
Pembacaan tampilan melewati Redis, tetapi setiap pembacaan sumber melewati bulkhead dan rate limit database. Jika kapasitas tidak mencukupi, kembalikan hasil degradasi eksplisit atau kegagalan. Salinan lama lokal-proses dapat melayani secara singkat selama masih dalam anggaran 5 detik; setelah itu salinan tersebut didiskualifikasi. Pulihkan sistem dengan pemanasan cache (warming) yang dibatasi lajunya sambil menguras antrean invalidasi agar cache yang dingin tidak membebani database lagi.
Lanjutan 8: Bagaimana cara membuktikan lingkungan produksi tidak memuat nilai usang yang bertahan lama?
Untuk sampel kunci, baca versi sumber database dan versi cache secara bersamaan, lalu catat jarak versi serta usia sejak commit pada sumber data. Lakukan rekonsiliasi perubahan database, progres outbox, dan high-water mark konsumen. Berikan peringatan pada lag invalidasi end-to-end, pekerjaan karantina tertua, dan bypass yang melebihi anggaran. Secara berkala suntikkan kegagalan commit-then-crash, kegagalan perintah Redis, dan jeda replika untuk menunjukkan bahwa gate dan TTL tetap melindungi invarian di bawah kegagalan nyata.