Transaksi multidokumen memperluas jaminan atomicitas dari satu dokumen menjadi sekelompok operasi lintas dokumen yang berlaku menyeluruh atau tidak berlaku sama sekali. Topik ini membahas syarat server yang harus dipenuhi, cara membuka transaksi di dalam sesi (session), cara mengakhiri transaksi dengan commit maupun abort, serta kesalahan yang lazim terjadi saat menulisnya. Data contohnya berupa koleksi rekening pada basis data kas, dan proses yang dijadikan contoh adalah transfer dana antara dua rekening yang menuntut dua dokumen berubah sebagai satu kesatuan.
Syarat replica set untuk transaksi#
Transaksi multidokumen hanya dapat dijalankan pada replica set atau pada kluster ter-shard, karena implementasinya bertumpu pada oplog, yaitu log operasi milik replica set, dan koordinasi antaranggota replica set. Replica set adalah sekelompok server yang menyimpan salinan data yang sama, sedangkan kluster ter-shard membagi data ke beberapa server. Server mongod tunggal yang berdiri sendiri menolak perintah transaksi. Untuk lingkungan pengembangan, satu node saja sudah cukup selama node tersebut dijalankan sebagai anggota replica set. Caranya adalah memulai mongod dengan opsi --replSet yang menamai replica set, lalu menghubungkan mongosh dan menjalankan rs.initiate() satu kali.
Syarat tersebut lahir dari mekanisme transaksinya. Server mencatat perubahan transaksi pada oplog dan menandai keadaan commit-nya melalui commit point mayoritas, yaitu penanda tulisan yang sudah diakui mayoritas anggota. Dengan penanda itu, anggota lain mengetahui transaksi mana yang sudah sah dan transaksi mana yang harus dibuang ketika terjadi failover, yaitu peristiwa perpindahan peran ketika anggota utama replica set berhenti. Satu mongod yang berdiri sendiri tidak memiliki anggota lain untuk melakukan koordinasi tersebut, sehingga perintah transaksinya ditolak. Replica set satu node tetap memberikan seluruh jaminan transaksi pada satu server, tetapi tanpa ketersediaan tambahan dari anggota kedua.
# tidak-dijalankan
# memulai mongod sebagai replica set satu node bernama rs0
mongod --dbpath ~/mongodb-data --replSet rs0
# pada terminal lain, inisiasi replica set tersebut melalui mongosh
mongosh --quiet --eval "rs.initiate()"Blok di atas ditandai tidak-dijalankan karena server MongoDB pada lingkungan latihan ini sudah berjalan sebagai replica set satu node bernama rs0, sehingga langkah tersebut tidak perlu diulang. Periksa nama replica set yang sedang dipakai dengan db.hello(), lalu tanam data contoh berikut.
db.hello().setNamers0use kasswitched to db kasdb.rekening.insertMany([
{ nomor: "R-001", nama: "Ali Rahman", saldo: 250000, mutasi: [] },
{ nomor: "R-002", nama: "Budi Santoso", saldo: 175000, mutasi: [] }
]){
acknowledged: true,
insertedIds: {
'0': ObjectId('6ae89a29a73f4185438de666'),
'1': ObjectId('6ae89a29a73f4185438de667')
}
}Menjalankan transaksi dengan sesi#
Transaksi berjalan di dalam satu client session, yaitu sesi yang mengelompokkan operasi dari satu konteks eksekusi dan menyimpan keadaan transaksinya. Sesi dibuat dengan db.getMongo().startSession(), lalu transaksi dibuka dengan startTransaction(). Operasi yang ingin dimasukkan ke transaksi wajib ditulis melalui objek database yang diambil dari sesi tersebut dengan getDatabase(), karena operasi yang ditulis melalui objek db biasa berjalan di luar transaksi. Transaksi diakhiri dengan commitTransaction() agar seluruh perubahan tersimpan, atau dengan abortTransaction() agar seluruh perubahan dibuang.
Contoh berikut memindahkan 50000 dari R-001 ke R-002. Dua dokumen diperbarui, masing-masing dengan saldo yang berubah dan mutasi yang bertambah, lalu transaksi ditutup dengan commit.
const sesi = db.getMongo().startSession()
sesi.startTransaction()
sesi.getDatabase("kas").rekening.updateOne(
{ nomor: "R-001" },
{ $inc: { saldo: -50000 }, $push: { mutasi: { jenis: "transfer keluar", jumlah: -50000, tujuan: "R-002" } } }
)
sesi.getDatabase("kas").rekening.updateOne(
{ nomor: "R-002" },
{ $inc: { saldo: 50000 }, $push: { mutasi: { jenis: "transfer masuk", jumlah: 50000, sumber: "R-001" } } }
)
sesi.commitTransaction()
sesi.endSession(){
acknowledged: true,
insertedId: null,
matchedCount: 1,
modifiedCount: 1,
upsertedCount: 0
}
{
acknowledged: true,
insertedId: null,
matchedCount: 1,
modifiedCount: 1,
upsertedCount: 0
}Verifikasi hasil transfer dengan membaca kedua dokumen di luar transaksi. Kedua saldo sudah berubah bersamaan karena commit menyimpan seluruh perubahan sebagai satu kesatuan.
db.rekening.find({}, { _id: 0, nomor: 1, saldo: 1 })[
{ nomor: 'R-001', saldo: 200000 },
{ nomor: 'R-002', saldo: 225000 }
]Perubahan di dalam transaksi tidak terlihat oleh operasi lain sampai transaksi tersebut commit. Apabila commitTransaction() tidak pernah dipanggil dan sesi ditutup atau koneksi terputus, server membatalkan transaksi tersebut secara otomatis, sehingga tidak ada perubahan setengah jadi yang tertinggal. Pola ini yang membuat sekelompok operasi lintas dokumen memperoleh jaminan all-or-nothing yang sama dengan jaminan operasi satu dokumen.
Membatalkan transaksi dengan abortTransaction#
abortTransaction() membuang seluruh perubahan yang sudah dilakukan di dalam transaksi yang masih terbuka. Pembacaan di dalam transaksi dapat melihat tulisannya sendiri sebelum commit dilakukan, sedangkan pembacaan dari luar transaksi tetap melihat keadaan lama. Contoh berikut membuka transaksi, mengurangi saldo R-001 sebanyak 30000, lalu membandingkan pembacaan dari dalam dan dari luar transaksi sebelum akhirnya membatalkannya.
const sesi = db.getMongo().startSession()
sesi.startTransaction()
sesi.getDatabase("kas").rekening.updateOne({ nomor: "R-001" }, { $inc: { saldo: -30000 } })
sesi.getDatabase("kas").rekening.findOne({ nomor: "R-001" }, { _id: 0, saldo: 1 })
db.rekening.findOne({ nomor: "R-001" }, { _id: 0, saldo: 1 })
sesi.abortTransaction()
db.rekening.findOne({ nomor: "R-001" }, { _id: 0, saldo: 1 })
sesi.endSession(){
acknowledged: true,
insertedId: null,
matchedCount: 1,
modifiedCount: 1,
upsertedCount: 0
}
{ saldo: 170000 }
{ saldo: 200000 }
{ saldo: 200000 }Keluaran tersebut menunjukkan tiga keadaan yang berurutan. Pembacaan pertama dari dalam transaksi melihat saldo 170000, yaitu hasil tulisan yang belum disimpan oleh commit. Pembacaan kedua dari luar transaksi masih melihat saldo 200000 karena perubahan transaksi belum disimpan. Pembacaan ketiga setelah abortTransaction() tetap menampilkan 200000, yang membuktikan seluruh perubahan sudah dibuang tanpa sisa. Abort adalah mekanisme keselamatan ketika aplikasi mendeteksi kesalahan logika di tengah proses, misalnya saldo tidak cukup untuk melanjutkan transfer.
Sesi yang transaksinya dibatalkan tetap dapat dipakai untuk transaksi berikutnya, dan pembacaan sesudah abort kembali melihat keadaan data yang sudah commit. Pada kode aplikasi, abort juga menjadi pasangan resmi dari commit, yaitu blok operasi transaksi ditutup dengan commitTransaction() ketika seluruh operasinya berhasil dan dengan abortTransaction() ketika salah satu operasi gagal. Kebiasaan tersebut menjaga sesi tidak meninggalkan transaksi terbuka yang menahan sumber daya server.
Kesalahan umum#
Kesalahan pertama adalah memanggil startTransaction() ketika transaksi pada sesi yang sama masih terbuka. Server tidak mendukung transaksi bertumpuk, sehingga panggilan kedua langsung ditolak. Setiap startTransaction() harus diawali penutupan transaksi sebelumnya, baik melalui commit maupun abort.
const sesi = db.getMongo().startSession()
sesi.startTransaction()
sesi.startTransaction()
sesi.abortTransaction()
sesi.endSession()Uncaught
MongoTransactionError: Transaction already in progressKesalahan kedua lebih sulit dideteksi karena tidak menghasilkan pesan error apa pun. Operasi yang ditulis melalui objek db biasa padahal transaksi terbuka berjalan di luar transaksi tersebut. Contoh berikut mengulang pola abort, tetapi tulisannya sengaja ditulis melalui db.rekening dan bukan melalui sesi.getDatabase("kas").
const sesi = db.getMongo().startSession()
sesi.startTransaction()
db.rekening.updateOne({ nomor: "R-001" }, { $inc: { saldo: -30000 } })
sesi.abortTransaction()
db.rekening.findOne({ nomor: "R-001" }, { _id: 0, saldo: 1 })
sesi.endSession(){
acknowledged: true,
insertedId: null,
matchedCount: 1,
modifiedCount: 1,
upsertedCount: 0
}
{ saldo: 170000 }Tulisan itu lolos dari abort dan saldo tetap berubah menjadi 170000 meskipun transaksinya dibatalkan, karena operasi tersebut memang tidak pernah menjadi bagian dari transaksi. Perbaikannya adalah selalu mengambil objek database dari sesi.getDatabase("kas") saat menulis di dalam transaksi. Kembalikan saldo R-001 ke keadaan semula agar data contoh tetap konsisten.
db.rekening.updateOne({ nomor: "R-001" }, { $inc: { saldo: 30000 } }){
acknowledged: true,
insertedId: null,
matchedCount: 1,
modifiedCount: 1,
upsertedCount: 0
}db.rekening.find({}, { _id: 0, nomor: 1, saldo: 1 })[
{ nomor: 'R-001', saldo: 200000 },
{ nomor: 'R-002', saldo: 225000 }
]Kesalahan ketiga berkaitan dengan usia transaksi. Server membatalkan transaksi yang berlangsung lebih lama dari batas waktunya. Batas tersebut bernilai bawaan 60 detik dan dapat diatur melalui parameter transactionLifetimeLimitSeconds. Transaksi sebaiknya dibuka ketika seluruh operasinya sudah diketahui dan ditutup segera, bukan dipakai sebagai wadah logika aplikasi yang panjang.
Ringkasan#
- Transaksi multidokumen menuntut replica set atau kluster ter-shard, dan replica set satu node sudah cukup untuk pengembangan.
- Sesi dibuat dengan
startSession(), transaksi dibuka denganstartTransaction(), dan setiap operasi ditulis melaluisesi.getDatabase(). - Perubahan transaksi terlihat oleh pembacaan di dalamnya sebelum commit, tidak terlihat dari luar, dan dibuang seluruhnya oleh
abortTransaction(). - Operasi yang ditulis melalui objek
dbbiasa berjalan di luar transaksi dan tidak ikut dibatalkan, sehingga objek sesi harus dipakai secara konsisten.
Referensi#
- Transactions — MongoDB Manual — Dokumentasi utama transaksi multidokumen, termasuk atomicitas dan perilaku isolation-nya.
- Transactions in Applications — MongoDB Manual — Pola pemakaian transaksi di kode aplikasi, termasuk penanganan ulang saat transient error.
- Mongo.startSession — MongoDB Manual — Dokumentasi pembuatan client session untuk transaksi dan konsistensi kausal.
- rs.initiate — MongoDB Manual — Dokumentasi inisiasi replica set yang menjadi syarat transaksi.
- MongoDB Limits and Thresholds — MongoDB Manual
— Batas transaksi, termasuk nilai bawaan
transactionLifetimeLimitSeconds.