MongoDB transaction (çok belgeli işlem), birden fazla belgeye, koleksiyona hatta veritabanına dokunan bir grup okuma ve yazmayı tek bir bütün olarak çalıştırır: ya hepsi kalıcı olur ya da hiçbiri görünmez. Bunun için bir session açarsınız, startTransaction() ile işlemi başlatırsınız, işlemlerinizi o session üzerinden yaparsınız ve sonunda commitTransaction() ya da abortTransaction() çağırırsınız. mongosh içinde daha kısa yol Session.withTransaction() yöntemidir; commit ve yeniden deneme işini sizin yerinize üstlenir.
Bu yazıda transaction için gereken altyapıyı, mongosh ile iki farklı yazım biçimini, read concern ve write concern seçimini, transaction içinde yapılamayan işlemleri, süre ve kilit sınırlarını ve üretimde sık görülen hataları anlatıyoruz. Bilmeniz gereken ilk nokta şu: MongoDB tek başına çalışan (standalone) bir sunucuda transaction desteklemez; en az birden fazla üyeli bir replica set gerekir. Kurulum için MongoDB replica set kurulumu yazısına bakabilirsiniz.
Önce şunu sorun: Transaction gerçekten gerekli mi?

MongoDB'de tek bir belge üzerindeki işlem zaten atomiktir. Bir updateOne aynı belgedeki birkaç alanı, gömülü alt belgeleri ve dizi elemanlarını tek seferde değiştirebilir; hata olursa belge yarım kalmaz. İlişkisel bir veritabanında ayrı tablolara bölünen sipariş ve sipariş satırı gibi verileri MongoDB'de tek belgeye gömebildiğiniz için, birçok uygulamada çok belgeli transaction ihtiyacı hiç doğmaz.
Resmî belgeler bu konuda açık bir uyarı yapar: dağıtık bir transaction çoğu durumda tek belge yazımından daha yüksek bir performans maliyeti taşır ve iyi bir şema tasarımının yerine geçmemelidir. Önce veriyi doğru modelleyin, transaction'ı gerçekten birden fazla belgenin birlikte değişmesi gereken yerlere saklayın. Gömme ve referans kararını MongoDB veri modelleme yazısında ayrıntılı ele alıyoruz.
Transaction'ın doğal olarak gerektiği tipik durumlar şunlardır: iki hesap arasında para transferi (iki ayrı belge), bir siparişi kaydederken stok belgesini düşürmek (iki ayrı koleksiyon) ya da bir kaydı bir koleksiyondan silip arşiv koleksiyonuna eklemek. Ortak nokta, ara durumun hiçbir okuyucu tarafından görülmemesi gerekmesidir.
Gereksinimler: replica set ve FCV
Transaction kullanabilmek için dağıtımınız bir replica set ya da sharded cluster olmalıdır. Tek sunuculu kurulumda transaction açılamaz. Ayrıca dağıtımdaki tüm üyelerin featureCompatibilityVersion (FCV) değeri replica set için en az 4.0, sharded cluster için en az 4.2 olmalıdır. Güncel bir MongoDB 9.0 kurulumunda bu koşul zaten sağlanır, ama eski sürümden yükseltilmiş sunucularda FCV eski değerde kalmış olabilir. Değeri bir üyeye bağlanıp şu komutla görebilirsiniz:
db.adminCommand( { getParameter: 1, featureCompatibilityVersion: 1 } )Erişim denetimi açık bir sunucuda, transaction içindeki her işlem için gerekli yetkilere sahip olmanız gerekir; transaction ayrıca bir yetki istemez ama içindeki her okuma ve yazma normal yetki kontrolünden geçer. Kullanıcı ve rol tanımlamak için MongoDB kullanıcı ve rol yönetimi yazısına bakın.
Session ve transaction ilişkisi
Her transaction bir session'a bağlıdır. Bir session üzerinde aynı anda yalnızca bir açık transaction olabilir. Transaction açıkken session sona ererse (örneğin endSession() çağrılırsa ya da bağlantı kapanırsa) transaction iptal edilir. Sürücü kullanan uygulamalarda en sık yapılan hata, transaction içindeki işlemlerden birine session'ı geçirmeyi unutmaktır; o işlem transaction dışında, kendi başına çalışır ve atomiklik bozulur. mongosh'ta bu sorunu, koleksiyon nesnesini doğrudan session üzerinden almakla çözersiniz: session.getDatabase("magaza").siparisler gibi.
mongosh ile çekirdek API: startTransaction, commit, abort
Aşağıdaki örnekte bir mağaza veritabanında sipariş kaydı ile stok düşümünü aynı transaction içinde yapıyoruz. Önce koleksiyonları transaction dışında oluşturmak iyi bir alışkanlıktır; belgeler, transaction'dan hemen önce koleksiyon oluşturup silerken "majority" write concern kullanmanızı önerir ki transaction gerekli kilitleri alabilsin.
// Koleksiyonlari transaction disinda hazirlayin
db.getSiblingDB("magaza").stok.insertOne(
{ _id: "kalem-01", adet: 40 },
{ writeConcern: { w: "majority" } }
)
db.getSiblingDB("magaza").createCollection("siparisler", { writeConcern: { w: "majority" } })
// Session acin ve koleksiyonlari session uzerinden alin
const session = db.getMongo().startSession( { readPreference: { mode: "primary" } } )
const stok = session.getDatabase("magaza").stok
const siparisler = session.getDatabase("magaza").siparisler
session.startTransaction( { readConcern: { level: "snapshot" }, writeConcern: { w: "majority" } } )
try {
stok.updateOne( { _id: "kalem-01", adet: { $gte: 3 } }, { $inc: { adet: -3 } } )
siparisler.insertOne( { urun: "kalem-01", adet: 3, durum: "alindi" } )
} catch (hata) {
session.abortTransaction()
throw hata
}
session.commitTransaction()
session.endSession()Burada dikkat edilecek üç ayrıntı var. Birincisi, read concern ve write concern değerleri transaction başlatılırken verilir; içerideki updateOne ve insertOne çağrılarına ayrıca writeConcern yazmazsınız. Belgelere göre transaction içindeki tek tek yazma işlemlerine write concern vermek hata döndürür. İkincisi, commit edilene kadar bu değişiklikler transaction dışından görünmez. Üçüncüsü, bu örnek bilerek basit tutulmuştur: geçici hatalarda yeniden deneme yapmaz. Üretimde ya aşağıdaki callback biçimini kullanın ya da hata etiketlerine göre yeniden deneme mantığı ekleyin.
Bir mantık hatasına da dikkat edin: stok yetersizse updateOne hata vermez, yalnızca hiçbir belge eşleşmez ve sipariş yine eklenir. Gerçek bir uygulamada updateOne sonucundaki eşleşen belge sayısını kontrol edip sıfırsa transaction'ı iptal etmelisiniz. Güncelleme sonucunun alanlarını MongoDB updateOne ve updateMany yazısında anlatıyoruz.
Callback API: Session.withTransaction()

mongosh 1.1.0 ile gelen Session.withTransaction(), verdiğiniz fonksiyonu bir transaction içinde çalıştırır. Fonksiyon normal biçimde dönerse transaction commit edilir; fonksiyon içinde bir hata fırlatılırsa transaction sonlandırılır ve commit edilmemiş değişiklikler geri alınır. Commit aşamasında hata olursa commit'i, hata türü izin veriyorsa transaction'ın tamamını yeniden dener. Dönüş değeri, fonksiyonunuzun ürettiği değerdir.
// Bakiyeleri kurus cinsinden tamsayi olarak tutuyoruz
use banka
db.hesaplar.insertMany( [
{ _id: "ayse", bakiye: 15000 },
{ _id: "mert", bakiye: 2000 }
] )
const tutar = 2500
const session = db.getMongo().startSession( { readPreference: { mode: "primary" } } )
session.withTransaction( () => {
const hesaplar = session.getDatabase("banka").getCollection("hesaplar")
const gonderen = hesaplar.findOne( { _id: "ayse", bakiye: { $gte: tutar } } )
if (gonderen === null) {
throw new Error("Gonderen hesapta yeterli bakiye yok")
}
if (hesaplar.findOne( { _id: "mert" } ) === null) {
throw new Error("Alici hesap bulunamadi")
}
hesaplar.updateOne( { _id: "ayse" }, { $inc: { bakiye: -tutar } } )
hesaplar.updateOne( { _id: "mert" }, { $inc: { bakiye: tutar } } )
} )
session.endSession()Kontroller fonksiyonun içinde, transaction'ın gördüğü veri üzerinde yapılır. Bakiye yetersizse fırlatılan hata transaction'ı geri alır ve iki hesap da değişmeden kalır. Belgelerdeki örnek async bir fonksiyon kullanır; mongosh sürümü Node.js sürücüsündeki callback API'den farklı olarak Promise döndürmenizi zorunlu tutmaz. Para tutarlarını tamsayı kuruş olarak tutmak yerine ondalık tip kullanmak isterseniz MongoDB BSON veri tipleri yazısındaki Decimal128 bölümüne bakın.
| Özellik | Çekirdek API | Callback API (withTransaction) |
|---|---|---|
| Başlatma | startTransaction() ile elle | Yöntem kendisi başlatır |
| Commit | commitTransaction() ile elle | Fonksiyon dönünce otomatik |
| Geçici hatada yeniden deneme | Sizin yazmanız gerekir | Yerleşik |
| Commit sonucu belirsizse | Sizin yazmanız gerekir | Commit yeniden denenir |
| Uygun olduğu yer | Özel hata akışı gereken durumlar | Çoğu uygulama |
Hata etiketleri ve yeniden deneme mantığı
Transaction hatalarını anlamak için iki etiketi bilmeniz yeterlidir. TransientTransactionError etiketi, örneğin primary üyenin görevi bırakması gibi geçici bir sorunu işaret eder; bu durumda transaction'ın tamamı baştan denenebilir. Önemli bir ayrıntı: transaction içindeki tek tek yazma işlemleri, retryWrites ayarı ne olursa olsun kendi başına yeniden denenmez; yeniden denenen şey transaction'ın bütünüdür.
UnknownTransactionCommitResult etiketi ise commit isteğinin sonucunun bilinmediği anlamına gelir; commit tekrar gönderilebilir. Commit işlemi yeniden denenebilir bir yazma sayılır ve sürücüler commit'i retryWrites değerinden bağımsız olarak yeniden dener. Belgelere göre sürücü, transaction için hangi write concern'ü seçmiş olursanız olun, commit'i yeniden denerken w: "majority" uygular.
Üçüncü bir hata türü yeniden denemeye uygun değildir: TransactionTooLargeForCache. MongoDB 6.2 ile eklenen bu hata, transaction'ın WiredTiger önbelleğine sığmayacak kadar büyük olduğunu söyler. transactionTooLargeForCacheThreshold parametresinin varsayılan değeri 0.75'tir; transaction önbelleğin yüzde 75'inden fazlasını kullanırsa sunucu yeniden denemek yerine bu hatayı döndürür, çünkü tekrar denemenin de başarısız olması beklenir. Çözüm, transaction'ı küçük parçalara bölmektir.
Read concern, write concern ve read preference
Transaction içindeki işlemler, koleksiyon veya veritabanı düzeyinde tanımladığınız read concern'ü yok sayar ve transaction düzeyindeki değeri kullanır. Transaction düzeyinde değer vermezseniz session düzeyindeki, o da yoksa istemci düzeyindeki değer geçerli olur. İstemci düzeyinde varsayılan read concern primary okumaları için "local", MongoDB 5.0 ve sonrasında varsayılan write concern ise w: "majority" değeridir (arbiter içeren dağıtımlarda farklılıklar vardır).
| Read concern | Ne sağlar | Not |
|---|---|---|
| local | Üyedeki en güncel veri | Geri alınabilir; sharded cluster'da shard'lar arası aynı anlık görüntüyü garanti etmez |
| majority | Çoğunluk tarafından onaylanmış veri | Yalnızca commit w: majority ile yapılırsa garanti verir |
| snapshot | Çoğunlukça onaylanmış verinin anlık görüntüsü | Commit w: majority ile yapılırsa; sharded cluster'da shard'lar arasında eşitlenir |
Pratik sonuç şudur: tutarlı bir anlık görüntü istiyorsanız transaction'ı readConcern: { level: "snapshot" } ve writeConcern: { w: "majority" } ile başlatın. w: 1 ile commit ederseniz yanıt primary uygular uygulamaz gelir ama failover durumunda transaction geri alınabilir ve snapshot ya da majority okuma garantileri geçerliliğini yitirir. Okuma içeren transaction'lar primary read preference kullanmak zorundadır ve tüm işlemler aynı üyeye yönlenmelidir. Bu kavramların ayrıntısı MongoDB write concern ve read concern yazısında.
Transaction içinde yapamayacaklarınız
Transaction içinde CRUD işlemlerinin büyük kısmını kullanabilirsiniz ve koleksiyonlar farklı veritabanlarında olabilir. Ancak belgeler bir dizi kısıt listeler:
- Capped koleksiyonlara yazamazsınız; capped koleksiyondan
"snapshot"read concern ile okuyamazsınız. config,adminvelocalveritabanlarındaki koleksiyonları okuyup yazamazsınız;system.*koleksiyonlarına yazamazsınız.- İşlemin sorgu planını
explainile alamazsınız. Planı transaction dışında inceleyin; bkz. MongoDB explain listCollections,listIndexes,createUser,getParametervecountkomutları ile yardımcı yöntemleri kullanılamaz.- Paralel işlemler desteklenmez; birden fazla namespace'i aynı anda güncellemek için belgeler
bulkWritekomutunu önerir. - Transaction dışında açılmış bir cursor için transaction içinde
getMoreçağıramazsınız; tersi de geçerlidir. hello,buildInfo,connectionStatusgibi bilgi komutlarına izin verilir ama transaction'ın ilk işlemi olamazlar.killCursorsda ilk işlem olamaz.- Time series koleksiyonlarına transaction içinde yazamazsınız; okumak serbesttir. Ayrıntı MongoDB time series koleksiyonları yazısında.
Sayma için count yerine db.collection.countDocuments() ya da $count / $group aşamaları kullanılır; countDocuments() arka planda zaten $group ve $sum ile çalışır. Sharded bir koleksiyonda distinct kullanamazsınız; aynı sonucu $group ile $addToSet kullanarak alırsınız. Bu aşamaları MongoDB $group ve accumulator yazısında anlatıyoruz.
Transaction içinde koleksiyon ve index oluşturmak
Shard'lar arası bir yazma transaction'ı değilse, transaction içinde koleksiyon oluşturabilirsiniz: var olmayan bir koleksiyona insert yapmak ya da upsert: true ile güncellemek koleksiyonu örtük olarak oluşturur; db.createCollection() ile açıkça da oluşturabilirsiniz. Index oluşturmak ise yalnızca var olmayan bir koleksiyonda ya da aynı transaction içinde az önce oluşturulmuş boş bir koleksiyonda mümkündür. Açık oluşturma için transaction'ın read concern değeri "local" olmalıdır.
Süre, kilit ve önbellek sınırları

Varsayılan olarak bir transaction bir dakikadan kısa sürmelidir. Bu sınır transactionLifetimeLimitSeconds parametresiyle belirlenir (varsayılan 60); süreyi aşan transaction'lar süresi dolmuş sayılır ve periyodik bir temizlik süreci tarafından iptal edilir. mongosh'ta elle deneme yaparken komutlar arasında oyalanırsanız bu sınıra takılabilirsiniz. Değeri çalışma anında değiştirmek mümkündür; sharded cluster'da tüm shard üyelerinde ayarlanmalıdır:
db.adminCommand( { setParameter: 1, transactionLifetimeLimitSeconds: 30 } )Kilitler için de bir süre vardır: transaction, gereken kilitleri almak için varsayılan olarak 5 milisaniye bekler; alamazsa iptal edilir. Bu süre maxTransactionLockRequestTimeoutMillis ile ayarlanır. Değeri artırmak kısa süreli kilit çakışmalarında gereksiz iptalleri azaltabilir, ama kilitlenmiş (deadlock) transaction'ların iptalini de geciktirebilir. Transaction commit ya da abort olduğunda tüm kilitler bırakılır.
Önbellek tarafında belgelerin önerisi nettir: vazgeçtiğiniz bir transaction'ı açık bırakmayın, abort edin; bir işlem hata verirse transaction'ı iptal edip yeniden deneyin. Commit edilmemiş bir transaction WiredTiger önbelleğinde aşırı baskı yaratırsa write conflict hatasıyla iptal edilir. Toplam boyut konusunda ise eski 16 MB sınırı kalkmıştır: MongoDB bir transaction'daki yazmalar için gerektiği kadar oplog girdisi oluşturur, yalnızca her bir oplog girdisi 16 MB BSON sınırına uymak zorundadır.
Write conflict ve eski okuma (stale read)
Transaction sürerken dışarıdan bir yazma, transaction'ın daha sonra değiştirmeye çalışacağı bir belgeyi değiştirirse transaction write conflict nedeniyle iptal edilir. Tersine, transaction bir belgeyi değiştirmek için kilit aldıysa dışarıdan gelen yazma transaction bitene kadar bekler. Ayrıca transaction içindeki okumalar, başka yerde commit edilmiş daha yeni yazmaları görmeyebilir; buna stale read denir. Tek bir belge için güncel okumayı garantilemek isterseniz belgeler, transaction içinde findOneAndUpdate() ile belgede küçük bir alan değiştirip yazma kilidi almayı önerir:
const kayit = session.getDatabase("ik").calisanlar.findOneAndUpdate(
{ _id: 7, durum: "aktif" },
{ $set: { kilitId: ObjectId() } },
{ returnNewDocument: true }
)Bu güncelleme belgeyi kilitler; commit edilene kadar dışarıdan bu belgeyi değiştirmeye çalışan işlem write conflict hatası alır.
Bekleyen DDL işlemleri
Bir transaction açıkken aynı koleksiyonu etkileyen createIndex() gibi bir DDL işlemi gelirse, DDL transaction'ın bitmesini bekler. Bu bekleme sürerken aynı koleksiyona dokunmak isteyen yeni transaction'lar da kilit alamaz ve maxTransactionLockRequestTimeoutMillis süresi sonunda iptal edilir. Yoğun saatlerde index oluşturmanın transaction ağırlıklı uygulamaları neden aniden hata yağmuruna soktuğunu bu mekanizma açıklar. Index işlemlerini sakin zamanlara planlayın; bkz. MongoDB index oluşturma.
MongoDB 9.0 ile eşzamanlı transaction sınırı
MongoDB 9.0 ile sunucu, dış istemcilerin aynı anda açık tutabileceği çok belgeli transaction sayısını sınırlar. Sınır maxConcurrentMultiDocumentTransactions parametresiyle belirlenir ve varsayılan değeri 10000'dir. Sınıra ulaşıldığında sunucu yeni transaction'ları reddeder ve TooManyOpenTransactions hatası döndürür. Sayım sunucu genelindedir; sunucunun kendi iç transaction'ları reddedilmez ama sayıma dahildir. Değeri 0 yapmak sınırı kapatır. Bu değişikliğin amacı, boşta bekleyen çok sayıda transaction'ın sistemi yavaşlatmasını önlemektir. Uygulamanız bu hatayı alıyorsa sorun büyük olasılıkla commit ya da abort edilmeden bırakılan session'lardır. 9.0'daki diğer değişiklikler için MongoDB 9.0 yenilikleri yazısına bakın.
db.adminCommand( { setParameter: 1, maxConcurrentMultiDocumentTransactions: 5000 } )Transaction'ları izlemek
Transaction durumunu ve metriklerini görmek için birkaç kaynak vardır. db.serverStatus() çıktısındaki transactions bölümü genel metrikleri verir. $currentOp aşaması ve db.currentOp(), bir işlem transaction'a aitse transaction bilgisini, kilit tutan boştaki session'ları ve birden çok shard'a yazan transaction'lar için iki aşamalı commit koordinatörü bilgisini gösterir. Ayrıca operationProfiling.slowOpThresholdMs eşiğini aşan yavaş transaction'lar, mongod ve mongos loglarında TXN bileşeni altında yazılır. Eşiği ayarlamayı MongoDB yavaş sorgu ve profiler yazısında anlatıyoruz.
Sık yapılan hatalar
- Standalone sunucuda denemek: Tek sunuculu kurulum transaction desteklemez; geliştirme ortamında bile tek üyeli bir replica set yerine, belgelerin dediği gibi çok üyeli bir replica set kullanın.
- Session'ı işleme geçirmemek: Koleksiyonu
dbüzerinden alırsanız işlem transaction dışında çalışır. - İçerideki işlemlere writeConcern yazmak: Hata döndürür; write concern yalnızca transaction başlatılırken verilir.
- Abort etmeden bırakmak: Açık kalan transaction önbelleği meşgul eder, 9.0'da ayrıca eşzamanlı sınıra sayılır.
- Uzun süren işleri transaction içine koymak: 60 saniye sınırı ve kilit beklemeleri nedeniyle büyük toplu işleri parçalara bölün.
- Yeniden denemeyi unutmak: Çekirdek API kullanıyorsanız TransientTransactionError ve UnknownTransactionCommitResult için döngü yazmanız gerekir.
- Her şeyi transaction ile çözmek: Tek belgeye sığan değişikliklerde transaction gereksiz maliyettir.
Sık Sorulan Sorular
Transaction olmadan tek belgede atomik güncelleme yapabilir miyim?
Evet. Tek bir belgeye yapılan her yazma atomiktir. Birden çok alanı, alt belgeyi veya dizi elemanını aynı updateOne içinde değiştirdiğinizde ya hepsi uygulanır ya da hiçbiri. Dizi güncellemeleri için MongoDB dizi güncelleme yazısına bakabilirsiniz.
Transaction içinde aggregation kullanabilir miyim?
Evet, okuma işlemleri arasında aggregation da yer alır. Ancak transaction içinde $count veya $group ile sayım yapabilirsiniz, count komutunu kullanamazsınız. Sharded koleksiyonlarda distinct yerine $group kullanmanız gerekir.
PostgreSQL'deki transaction ile aynı şey mi?
Kavram olarak benzerdir: ACID garantisi verir ve commit ile rollback mantığı aynıdır. Fark, MongoDB'de transaction'ın bir session'a bağlı olması, varsayılan 60 saniyelik süre sınırı ve replica set gerektirmesidir. İlişkisel tarafı merak ediyorsanız PostgreSQL nedir yazısına göz atabilirsiniz.
mongosh'ta transaction neden kendiliğinden iptal oldu?
En olası neden süre sınırıdır. Komutları elle yazarken bir dakikayı aşarsanız temizlik süreci transaction'ı iptal eder. Deneme ortamında transactionLifetimeLimitSeconds değerini geçici olarak artırabilir ya da komutları tek bir blok halinde çalıştırabilirsiniz.
Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.
İlgili içerikler: MongoDB öğrenme rehberi · Replica set kurulumu · Write concern ve read concern · Veri modelleme · MongoDB 9.0 yenilikleri