Pola penjaga sejauh ini — operator atomic, filter versi — bekerja dalam lingkup satu document. Tetapi invariant paling khas dari dompet digital justru melintasi dua document: ketika Budi transfer ke Siti, saldo Budi berkurang tepat sebanyak saldo Siti bertambah. Tidak boleh ada keadaan di mana uang sudah keluar tapi tidak sampai. MongoDB menyediakan multi-document transactions untuk janji seperti itu — tersedia untuk replica set sejak versi 4.0 dan sharded cluster sejak 4.2. Sebelum menyentuh API-nya, chapter ini memetakan kapan transaksi benar-benar dibutuhkan, memperlihatkan apa yang terjadi tanpanya, dan menimbang harganya.
Kapan Transaksi Dibutuhkan #
Transaksi layak dipertimbangkan ketika ada invariant lintas document — sekumpulan perubahan yang harus terjadi semuanya atau tidak sama sekali. Contoh nyatanya:
- Transfer saldo: dua document
dompetharus berubah bersamaan. - Pesanan dan stok:
pesananbaru dibuat hanya bilastokbuku berkurang. - Pemindahan data referensial: menghapus
pelangganharus sekaligus menyingkirkan atau memindahkanpesananmiliknya.
Kebalikannya sama penting: banyak operasi yang terasa butuh transaksi ternyata tidak. Mengubah tiga field satu document? Sudah atomic. Menambah item ke keranjang yang embedded? Atomic. Menghitung laporan dari beberapa collection? Itu pekerjaan $lookup, bukan transaksi. Diagram keputusan berikut meringkasnya:
Jawaban “gunakan transaksi” pun bukan akhir cerita — alternatif yang lebih murah dibahas pada bagian akhir chapter ini.
Tanpa Transaksi, Invariant Bisa Patah #
Kita bangun skenario yang meniru aplikasi nyata: transfer 25000 dari Budi ke Siti lengkap dengan pencatatan mutasi. Collection mutasi diberi unique index pada kode — dan sebuah mutasi ber-kode sama sudah ada dari transfer kemarin:
use latihan_relasi
db.dompet.drop()
db.mutasi.drop()
db.dompet.insertMany([
{ pengguna: "Budi", saldo: 100000 },
{ pengguna: "Siti", saldo: 50000 }
])
db.mutasi.createIndex({ kode: 1 }, { unique: true })
db.mutasi.insertOne({ kode: "TRF-001", keterangan: "transfer kemarin", nominal: 10000 })Output yang Dihasilkan:
switched to db latihan_relasi
true
true
{
acknowledged: true,
insertedIds: {
'0': ObjectId('6ad39bf3ea3fd50c424ad3ce'),
'1': ObjectId('6ad39bf3ea3fd50c424ad3cf')
}
}
kode_1
{
acknowledged: true,
insertedId: ObjectId('6ad39bf3ea3fd50c424ad3d0')
}{
acknowledged: true,
insertedId: null,
matchedCount: 1,
modifiedCount: 1,
upsertedCount: 0
}
{
acknowledged: true,
insertedId: null,
matchedCount: 1,
modifiedCount: 1,
upsertedCount: 0
}
Uncaught
MongoServerError: E11000 duplicate key error collection: latihan_relasi.mutasi index: kode_1 dup key: { kode: "TRF-001" }[
{ pengguna: 'Budi', saldo: 75000 },
{ pengguna: 'Siti', saldo: 75000 }
]Saldo Budi sudah terpotong menjadi 75000 dan saldo Siti sudah bertambah menjadi 75000 — aplikasi menerima galat, tidak tahu harus bagaimana, dan kedua document dibiarkan berubah. Pada sistem produksi, penyebab gagal di tengah beragam: index bentrok seperti di atas, koneksi terputus, server aplikasi berhenti tepat di antara dua perintah. Tanpa mekanisme pemulihan, setiap kegagalan semacam ini meninggalkan data yang harus direkonsiliasi manual.
Biaya dan Alternatif #
Transaksi bukan fitur gratis. Server harus menjaga snapshot data, menahan lock pada document yang disentuh sampai commit, menulis koordinasi ekstra ke oplog, dan menangani konflik dengan transaksi lain. Pada beban tinggi, transaksi yang panjang saling menunggu dan menekan cache WiredTiger — dokumentasi resmi menyebut transaksi berjalan lama sebagai sumber tekanan cache. Karena itu urutan pertimbangannya selalu:
- Redesain jadi embedded — invariant yang hidup dalam satu document mendapat atomicity gratis (lihat Chapter Atomicity Per Document).
- Operator dan filter penjaga —
$inc, compare-and-set, dan version field menutup banyak kasus tanpa transaksi sama sekali. - Pola compensating — terapkan perubahan satu per satu, lalu bila ada yang gagal, jalankan operasi kebalikannya (potong 25000 yang keliru tadi dengan menambahkannya kembali). Umum di sistem pesan antar-layanan.
- Antrian pekerjaan — kumpulkan perubahan di antrian, satu worker memprosesnya berurutan; kecepatan tetap, kompleksitas konkurensi hilang.
- Transaksi — bila invariant mutlak dan opsi di atas tidak memadai.
Dari sisi API, seluruh mekanisme transaksi hidup di objek session: db.getMongo().startSession() membuat sesi, dan seluruh operasi di dalam transaksi wajib berjalan lewat handle collection milik sesi itu. Ada dua gaya — core API (startTransaction → operasi → commitTransaction/abortTransaction) dan callback API (withTransaction yang membungkus semuanya termasuk logika coba-ulang). Keduanya berlaku sama di mongosh maupun driver; kita praktikkan keduanya di chapter berikutnya, langsung di replica set rs0 kita yang mendukung transaksi.
Rangkuman #
- Transaksi menjawab invariant lintas document: semua perubahan terjadi atau tidak sama sekali.
- Banyak kasus tidak butuh transaksi: atomic per document,
$lookup, operator penjaga, atau version field sudah cukup. - Tanpa transaksi, kegagalan di tengah rangkaian perintah meninggalkan perubahan setengah jalan — didemokan lewat bentrok unique index pada pencatatan mutasi.
- Urutan pertimbangan: redesain embedded → operator/filter penjaga → compensating → antrian → transaksi.
- API transaksi hidup pada session; tersedia sejak 4.0 (replica set) dan 4.2 (sharded cluster).
Peta sudah di tangan. Chapter Multi-Document Transactions membawa kita menjalankan transfer sungguhan di dalam transaksi — commit, abort, dan tingkah error label-nya.