Bir e-ticaret sitesinde müşteri "Siparişi Tamamla" düğmesine bastığında arka planda en az üç şey olur: stok azalır, sipariş kaydı oluşur, ödeme kaydı yazılır. Bu üç işlemden ikincisi tamamlanıp üçüncüsü yarıda kalırsa ortaya ödemesi olmayan bir sipariş çıkar; birincisi tamamlanıp diğerleri kalırsa stoktan düşmüş ama satılmamış bir ürün. ACID ve BASE modelleri, tam olarak bu "ya hep ya hiç" sorusuna iki farklı cevap veren yaklaşımlardır ve hangisini seçtiğiniz uygulamanızın hata anında nasıl davranacağını belirler.
Bu rehberde ACID'in dört harfinin her birinin ne garanti ettiğini, izolasyon seviyelerinin hangi gerçek anomalileri engellediğini, dayanıklılık ayarlarının performansla nasıl takas edildiğini, BASE modelinin neyi vaat edip neyi vaat etmediğini ve modern sistemlerde bu ikisinin aynı uygulama içinde nasıl bir arada kullanıldığını anlatacağım. Sonunda "hangisi daha iyi" sorusunun yerini "hangi işlem hangi garantiyi hak ediyor" sorusunun alması gerektiğini göreceksiniz.
ACID: Dört Harfin Her Biri Neyi Garanti Eder#
Atomicity (Atomiklik), bir transaction içindeki işlemlerin ya tamamının uygulanmasını ya da hiçbirinin uygulanmamasını garanti eder. Yarım kalmış durum diye bir şey yoktur. Sunucu tam ortada elektrik kesintisiyle kapansa bile, açılışta veritabanı yarım kalan transaction'ı geri alır.
Consistency (Tutarlılık), transaction'ın veritabanını geçerli bir durumdan başka bir geçerli duruma taşımasıdır. Burada "geçerli" demek, tanımladığınız tüm kısıtların (yabancı anahtar, benzersizlik, CHECK kuralları) sağlanması demektir. Dikkat: bu, CAP teoremindeki tutarlılıkla aynı şey değildir; oradaki C kopyalar arası güncelliği anlatır.
Isolation (İzolasyon), eşzamanlı çalışan transaction'ların birbirinin yarım işini görmemesini sağlar. Bu, dört harf içinde ayarlanabilir olanıdır ve pratikte en çok sürprize yol açanıdır.
Durability (Dayanıklılık), COMMIT cevabı döndükten sonra verinin kalıcı olmasıdır — sunucu o an kapansa bile. Bunun bedeli, her commit'te diske gerçek bir yazma (fsync) yapılmasıdır.
-- Klasik ACID transaction: üçü de olur ya da hiçbiri olmaz
START TRANSACTION;
UPDATE stok
SET adet = adet - 1
WHERE urun_id = 4210 AND adet >= 1;
-- Etkilenen satır 0 ise stok yetersizdir; uygulama burada ROLLBACK yapmalı
INSERT INTO siparisler (musteri_id, urun_id, adet, durum)
VALUES (1042, 4210, 1, 'bekliyor');
INSERT INTO odemeler (siparis_id, tutar, yontem)
VALUES (LAST_INSERT_ID(), 899.90, 'kart');
COMMIT;
Bu blokta COMMIT başarıyla dönerse üç değişikliğin üçü de kalıcıdır. Herhangi bir adımda hata olursa ROLLBACK ile hiçbiri uygulanmaz. Uygulama katmanında elle telafi kodu yazmanız gerekmez — ACID'in en büyük pratik faydası budur.
İzolasyon Seviyeleri ve Gerçek Anomaliler#
İzolasyon, performansla doğrudan takas edilen bir garantidir. Ne kadar sıkı izolasyon isterseniz, veritabanı o kadar çok kilit tutar ve eşzamanlılık o kadar düşer. SQL standardı dört seviye tanımlar ve her seviye belirli anomalilere izin verir:
| Seviye | Dirty read | Non-repeatable read | Phantom read | Tipik kullanım |
|---|---|---|---|---|
| READ UNCOMMITTED | İzin verir | İzin verir | İzin verir | Neredeyse hiç |
| READ COMMITTED | Engeller | İzin verir | İzin verir | PostgreSQL varsayılanı |
| REPEATABLE READ | Engeller | Engeller | Motora göre | MySQL InnoDB varsayılanı |
| SERIALIZABLE | Engeller | Engeller | Engeller | Kritik finansal işlem |
Bu anomalilerin ne demek olduğunu somutlaştıralım. Dirty read, henüz commit edilmemiş bir değişikliği okumaktır; o transaction geri alınırsa siz var olmayan bir veriyi okumuş olursunuz. Non-repeatable read, aynı transaction içinde aynı satırı iki kez okuduğunuzda farklı değer görmenizdir. Phantom read, aynı sorguyu iki kez çalıştırdığınızda ikinci seferde yeni satırların belirmesidir.
Pratikte en tehlikeli olan ve tabloda görünmeyen anomali ise lost update (kaybolan güncelleme)dir. İki kullanıcı aynı satırı okuyup kendi hesabını yapıp geri yazarsa, ikincisinin yazması birincisininkini sessizce yok eder:
-- YANLIŞ: oku, hesapla, yaz. Araya başka transaction girerse güncelleme kaybolur.
SELECT adet FROM stok WHERE urun_id = 4210; -- 10 döndü
-- uygulama 10 - 1 = 9 hesapladı
UPDATE stok SET adet = 9 WHERE urun_id = 4210;
-- DOĞRU: hesabı veritabanına yaptırın, koşulu da sorguya koyun
UPDATE stok
SET adet = adet - 1
WHERE urun_id = 4210 AND adet >= 1;
Alternatif olarak satırı okurken kilitleyebilirsiniz:
START TRANSACTION;
-- Satırı transaction sonuna kadar kilitle; diğerleri bekler
SELECT adet FROM stok WHERE urun_id = 4210 FOR UPDATE;
UPDATE stok SET adet = adet - 1 WHERE urun_id = 4210;
COMMIT;
İzolasyon seviyesini yükseltmek her sorunu çözmez ve kilit çakışmalarını artırır. Bunun yerine, güncellemeyi tek bir atomik UPDATE ifadesine indirgemek çoğu durumda hem daha hızlı hem daha güvenlidir.
Dayanıklılık: Commit Ne Kadar Kalıcı#
Dayanıklılık, dört harf içinde performans faturası en yüksek olanıdır. Her COMMIT'te veritabanının değişiklik günlüğünü fiziksel diske yazması (fsync) gerekir; bu işlem, RAM'e yazmaktan kat kat yavaştır. Bu yüzden hem MySQL hem PostgreSQL, dayanıklılığı gevşetmenize izin veren ayarlar sunar — ve bu ayarlar üretimde en sık yanlış anlaşılanlardandır.
# MySQL / MariaDB — tam dayanıklılık (varsayılan ve önerilen)
innodb_flush_log_at_trx_commit = 1 ; her commit diske yazılır ve fsync edilir
sync_binlog = 1 ; binlog da her commit'te senkronize edilir
# Değer 2 yapılırsa: commit OS önbelleğine yazılır, saniyede bir fsync edilir.
# MySQL çökerse veri kaybolmaz; SUNUCU çökerse son ~1 saniye kaybolur.
# PostgreSQL — dayanıklılık ayarları
fsync = on # asla kapatmayın, çökme sonrası veri bozulur
synchronous_commit = on # commit WAL diske yazılana kadar bekler
wal_sync_method = fdatasync
Buradaki en tehlikeli ayar fsync = off'tur. Bu ayar test ortamında toplu veri yüklemesini hızlandırmak için kullanılır; üretimde açıkta bırakılırsa bir güç kesintisi veritabanını onarılamaz biçimde bozabilir. synchronous_commit = off ise daha ılımlıdır: veritabanı bozulmaz, yalnızca son birkaç yüz milisaniyelik commit'ler kaybolabilir. Log toplama gibi kayıp toleransı olan tablolarda bu, işlem başına ölçülebilir bir hız kazancı sağlar.
MySQL tarafında performans ayarlarının bütününü görmek isterseniz MySQL ve MariaDB performans optimizasyonu yazısı bu parametreleri bağlam içinde ele alıyor.
BASE: Gevşek Ama Ayakta#
BASE, ACID'in katılığının ölçeklenmeyi zorlaştırdığı dağıtık sistemler için geliştirilmiş alternatif bir yaklaşımdır ve üç parçadan oluşur:
Basically Available (Temelde erişilebilir): Sistem her zaman bir cevap verir. Cevap en güncel veri olmayabilir, hatta kısmi olabilir, ama "şu anda hizmet veremiyorum" demez.
Soft state (Yumuşak durum): Sistemin durumu, dışarıdan yeni bir girdi olmasa bile zamanla değişebilir. Kopyalar arka planda senkronize olurken veri "yerine oturur".
Eventually consistent (Nihai tutarlı): Yeni yazma gelmediği sürece, tüm kopyalar sonunda aynı değere ulaşır. "Sonunda" genellikle milisaniyelerle ölçülür ama garanti edilen bir üst sınır yoktur.
BASE'in altında yatan mantık şudur: dağıtık bir sistemde her yazmayı tüm kopyaların onaylamasını beklemek, sistemi en yavaş bağlantı kadar yavaş ve en kırılgan düğüm kadar kırılgan yapar. Bu bedeli ödemeye değmeyen veri tipleri için gevşek model daha akılcıdır. Sepet içeriği, görüntülenme sayacı, öneri listesi, bildirim akışı bu kategoriye girer.
BASE'in vaat etmediği şeyi de netleştirelim: veri kaybını göze almak değildir. Nihai tutarlılık, yazmanın kaybolacağı anlamına gelmez; kopyaların bir süre farklı görünebileceği anlamına gelir. Dayanıklılık ayrı bir eksendir ve BASE sistemlerde de replikasyon faktörü ile sağlanır. Bu ayrımın dağıtık taraftaki karşılığı için Apache Cassandra nedir yazısındaki tutarlılık seviyeleri bölümüne bakabilirsiniz.
| Boyut | ACID | BASE |
|---|---|---|
| Öncelik | Doğruluk | Erişilebilirlik |
| Yazma onayı | Tüm kısıtlar doğrulanır | Yeterli sayıda kopya yeter |
| Hata anında | İşlem geri alınır | İşlem kabul edilir, sonra uzlaşılır |
| Ölçeklenme | Genellikle dikey | Doğal olarak yatay |
| Karmaşıklık nerede | Veritabanında | Uygulama katmanında |
| Tipik veri | Fatura, bakiye, stok | Sayaç, akış, sepet, log |
Tablodaki "karmaşıklık nerede" satırı en önemlisidir. BASE seçtiğinizde karmaşıklık ortadan kalkmaz; veritabanından uygulama koduna taşınır. Yarım kalan işlemleri temizlemek, çakışmaları uzlaştırmak ve mükerrer işlemleri engellemek artık sizin sorumluluğunuzdur.
Aynı Uygulamada İkisini Birlikte Kullanmak#
Gerçek sistemlerde ACID ve BASE bir "ya o ya bu" seçimi değildir. Tipik bir e-ticaret uygulaması şöyle kurulur: para ve stok gibi çekirdek veriler ilişkisel bir veritabanında tam ACID ile; sepet, oturum, sayaç ve öneri verileri ise gevşek modelde tutulur.
Sorun, bu iki dünya arasında bir işlem yayıldığında başlar. Sipariş veritabanına yazılıp ardından bir arama indeksine ve bir bildirim kuyruğuna gönderilmesi gerekiyorsa, bunların hepsini tek bir transaction'a alamazsınız. Burada iki yerleşik desen devreye girer.
Outbox deseni, dışarıya gidecek mesajı aynı veritabanı transaction'ı içinde bir tabloya yazmanızı, ayrı bir işçinin de bu tabloyu okuyup mesajı göndermesini önerir. Böylece "sipariş yazıldı ama mesaj gitmedi" durumu ortadan kalkar:
START TRANSACTION;
INSERT INTO siparisler (musteri_id, tutar, durum)
VALUES (1042, 899.90, 'onaylandi');
-- Mesaj da aynı transaction içinde yazılır: ikisi birlikte kalıcı olur
INSERT INTO giden_kutusu (olay_tipi, gonderim_durumu, veri)
VALUES ('siparis_olusturuldu', 'bekliyor',
JSON_OBJECT('siparis_id', LAST_INSERT_ID(), 'musteri_id', 1042));
COMMIT;
Saga deseni ise birden fazla servise yayılan uzun işlemleri, her adımın kendi telafi (compensating) işlemine sahip olduğu bir zincire dönüştürür. Ödeme başarısız olursa stok rezervasyonu geri alınır, sipariş iptal edilir. Bu deseni kullanırken iki kural hayatidir: her adım idempotent olmalıdır (aynı mesaj iki kez gelirse ikinci kez etki etmemeli) ve her adımın telafisi önceden yazılmış olmalıdır.
-- İdempotency: aynı işlem anahtarıyla ikinci kez gelen istek yeni kayıt açmaz
INSERT INTO odemeler (islem_anahtari, siparis_id, tutar, durum)
VALUES ('idem-8f2c41', 77120, 899.90, 'basarili')
ON DUPLICATE KEY UPDATE durum = durum; -- ikinci çağrıda hiçbir şey değişmez
islem_anahtari kolonunda benzersiz indeks olması bu desenin çalışmasının tek şartıdır. Ağ zaman aşımı sonrası yeniden denemelerde mükerrer tahsilatı engelleyen şey tam olarak budur.
Sık Yapılan Hatalar#
Otomatik commit ile transaction sandığınız kodu çalıştırmak en yaygın hatadır. Çoğu istemci varsayılan olarak autocommit açık gelir; START TRANSACTION yazmadan art arda gönderdiğiniz üç UPDATE, üç ayrı transaction'dır ve aradaki bir hata yarım durum bırakır. Transaction sınırlarını uygulama kodunda açıkça belirtin.
Transaction'ı ağ çağrısıyla uzatmak ikinci hatadır. Bir transaction açıkken harici bir API'ye istek atmak, kilitleri o isteğin süresi kadar tutar. Uzak servis yavaşladığında veritabanınızda kilit yığılması ve bağlantı tükenmesi başlar. Bu belirti çoğu zaman kendini bağlantı hatası olarak gösterir; nedenlerine MySQL Too Many Connections hatası yazısında ayrıntılı değinilmiş.
Kilit sırasını standartlaştırmamak üçüncü hatadır. İki farklı kod yolu aynı iki tabloyu ters sırada kilitlerse deadlock kaçınılmazdır. Kural basittir: tüm kod yolları kaynakları hep aynı sırayla kilitlesin. Ayrıca deadlock tamamen önlenemez, bu yüzden uygulama tarafında yeniden deneme mantığı bulundurun.
BASE'i "tutarlılığı hiç düşünmemek" sanmak dördüncü hatadır. Gevşek modelde çalışan bir sistemde bile hangi verinin otorite kaynağı olduğunu, çakışmaların nasıl çözüleceğini ve mükerrer mesajların nasıl engelleneceğini yazılı olarak kararlaştırmanız gerekir. Bu kararları vermezseniz, varsayılan davranışa ve rastlantıya teslim olursunuz.
Sıkça Sorulan Sorular#
ACID mi BASE mi daha iyi#
Hiçbiri diğerinden mutlak olarak daha iyi değildir; ikisi farklı problemleri çözer. Para, stok, fatura ve kimlik gibi hata toleransı sıfır olan veriler için ACID zorunludur, çünkü telafi kodunu elle yazmak hem hataya açık hem pahalıdır. Sayaçlar, akışlar, öneriler ve loglar gibi anlık güncelliği kritik olmayan veriler için BASE, sistemin ayakta kalmasını ve ucuza ölçeklenmesini sağlar. Olgun uygulamalar ikisini aynı anda, farklı veri tipleri için kullanır.
MySQL varsayılan izolasyon seviyesi nedir#
MySQL'in InnoDB motoru varsayılan olarak REPEATABLE READ seviyesinde çalışır; PostgreSQL ise READ COMMITTED kullanır. Bu fark, iki motor arasında taşıma yaparken sürpriz davranışlara yol açabilir. Mevcut seviyeyi SELECT @@transaction_isolation; komutuyla görebilir, oturum bazında SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; ile değiştirebilirsiniz. Seviyeyi yükseltmeden önce, sorunu atomik bir UPDATE ifadesiyle çözüp çözemeyeceğinizi kontrol edin.
NoSQL veritabanları transaction desteklemiyor mu#
Birçoğu destekliyor, ama kapsamı ve maliyeti farklı. MongoDB çok belgeli ACID transaction sunar; Cassandra hafif ağırlıklı transaction (LWT) ile koşullu yazma yapabilir; Redis komut gruplarını atomik çalıştırabilir. Ortak nokta şudur: bu özellikler dağıtık koordinasyon gerektirdiği için sıradan yazmalardan belirgin biçimde pahalıdır. Uygulamanız sürekli çok kayıtlı transaction'a ihtiyaç duyuyorsa, bu genellikle verinin ilişkisel olduğunun ve ilişkisel bir motorda durması gerektiğinin işaretidir.
Eventual consistency veri kaybı anlamına mı geliyor#
Hayır. Nihai tutarlılık, kopyaların bir süre farklı değerler gösterebileceğini söyler; yazmanın kaybolacağını değil. Yazmanın kalıcı olması dayanıklılık ile ilgilidir ve replikasyon faktörü, yazma onay seviyesi ve disk senkronizasyon ayarlarıyla sağlanır. Veri kaybı riski, tutarlılık modelinden değil, tek kopyaya yazıp onaylamak ya da disk senkronizasyonunu kapatmak gibi tercihlerden doğar.
Transaction'ı ne kadar uzun tutabilirim#
Mümkün olduğunca kısa. Uzun transaction'lar kilitleri uzun süre tutar, eşzamanlılığı düşürür ve MVCC kullanan motorlarda eski satır sürümlerinin temizlenmesini engelleyerek tablo şişmesine yol açar. Pratik kural şudur: transaction yalnızca veritabanı işlemlerini kapsasın; kullanıcı girdisi beklemek, dosya yüklemek ya da harici API çağırmak transaction dışında yapılsın. Toplu güncellemeleri de tek dev bir transaction yerine binlik parçalara bölmek çok daha güvenlidir.
Deadlock hatası alıyorum, ne yapmalıyım#
Deadlock, iki transaction'ın birbirinin tuttuğu kilidi beklemesidir ve veritabanı bunu algılayıp birini otomatik olarak geri alır. Tamamen önlemek mümkün değildir, bu yüzden ilk yapmanız gereken uygulama tarafında kısa bir bekleme sonrası yeniden deneme mantığı eklemektir. Sıklığını azaltmak içinse tüm kod yollarının kaynakları aynı sırayla kilitlemesini sağlayın, transaction'ları kısaltın ve gereksiz SELECT ... FOR UPDATE kullanımından kaçının. MySQL'de SHOW ENGINE INNODB STATUS çıktısı son deadlock'un hangi iki sorgu arasında olduğunu ayrıntısıyla gösterir.
Kapanış#
ACID ve BASE, birbirinin rakibi değil birbirinin tamamlayıcısıdır. Aklınızda kalması gereken dört alışkanlık şunlar: transaction sınırlarını kodda açıkça belirtin ve içine ağ çağrısı koymayın; "oku–hesapla–yaz" desenini atomik bir UPDATE ya da satır kilidiyle değiştirin; dayanıklılık ayarlarını performans için gevşetecekseniz hangi veride ne kadar kayıp göze aldığınızı yazılı olarak kararlaştırın; ve servisler arası işlemlerde outbox ya da saga deseniyle idempotency'yi baştan kurun.
Veritabanınızın dayanıklılık ayarlarını tam açık tutup performansı da korumak istiyorsanız, işin büyük kısmını disk hızı belirler. Hızlı NVMe depolamalı VDS ve bulut sunucu paketleri commit gecikmesini belirgin biçimde düşürür; yüksek işlem hacmi için dedicated sunucu seçenekleri daha öngörülebilir sonuç verir. Ayar, izleme ve yedek doğrulamasını devretmek isterseniz sunucu yönetimi hizmetimiz bu işi üstlenebilir.