PostgreSQL'da Transaction'lar
Geçen gün girdiğim mülakatta PostgreSQL veritabanlarındaki transaction isolation seviyeleriyle ilgili bir soru soruldu. Oradan esinlenerek, bu konuda bildiklerimi paylaşmak istedim.
Transaction kavramı, veritabanlarında okuma/yazma gibi işlemlerin gruplandırılıp sanki tek bir işlemmiş gibi ya hep ya hiç mantığıyla gerçekleştirilmesidir. Bu garanti sayesinde biz uygulama kodu yazarken işlemler yarım kaldı mı kalmadı mı dert etmemiş oluyoruz.
Transaction’lar ömürlerini “commit” olarak ya da “rollback” edilerek tamamlarlar. Commit, işlemler veritabanına kaydedildi ve başarıyla bitti anlamına gelir. Rollback’te ise transaction içinde bulunan işlemlerin etkisi geri alınır, yani iptal edilir diye düşünebiliriz.
Okuma/yazma işlemlerine verilen genel ad “query”. Select, insert, update ve delete olarak dört temel query var. Select okuma, diğerleri ise yazma.
PostgreSQL’da Transactionlar Nasıl Çalışıyor
Örnek olarak “hesaplar” adında bir tablo kullanacağız. Hesabın adı ve bir de bakiyesi var. İsterseniz siz de PostgreSQL indirip bilgisayarınıza kurabilirsiniz. Böylece makaleyi okurken bir taraftan da komutları ve davranışları kendiniz deneyip pekiştirebilirsiniz. PostgreSQL güncel versiyonu 18, bendeki versiyon ise 17.
Tabloyu oluşturmak için şu komutu çalıştırın.
CREATE TABLE hesaplar (
isim text PRIMARY KEY,
bakiye numeric NOT NULL
);Transaction başlatmak için iki yöntem var. “begin transaction” komutuyla başlatabilirsiniz. Veya herhangi bir query (select, insert, update, delete) çalıştırdığınızda, halihazırda bir transaction içinde değilseniz, postgresql sizin için otomatik olarak bir transaction başlatacak ve komutu o transaction içinde gerçekleştirip daha sonra da “commit”leyecektir. Teknik olarak şu ikisi aynı şey
begin transaction;
insert into hesaplar values ('selcuk', 30000);
commit;-- bu komut başlarken otomatik bir transaction açılıyor
insert into hesaplar values ('selcuk', 30000);
-- komut bittiğinde ise transaction yine otomatik olarak commitleniyorBir başka deyişle, PostgreSQL’da her komut bir transaction içinde çalışıyor. Transaction’ı siz başlatmazsanız, veritabanı kendisi başlatıp bitiyor.
Transaction Isolation Seviyeleri
Veritabanları aynı anda yüzlerce farklı istemciden (client, uygulama) gelen istekleri (query) işleyebiliyor. Banka hesabına para transferi yapan tek kişi siz değilsiniz, uygulamayı aynı anda binlerce insan kullanıyor olabilir.
Veritabanında her query bir oturum (session) içinde çalıştırılır ve aynı anda yüzlerce oturum aktif olabilir. Peki sistem farklı istemcilerden gelen ve her biri farklı operasyonlar içeren, farklı farklı zamanlarda başlayan ve biten transaction’ların birbirleriyle olan ilişkilerini nasıl yönetiyor? Transaction’lar aynı anda aynı veriye erişmeye çalıştıklarında ne ile karşılaşıyorlar? Bir transaction, aynı anda devam eden başka transactionlardaki değişikliklerin ne kadarını görmeli? Birlikte nasıl davranmalılar?
SQL standardı dört çeşit davranış tanımlıyor. Bunları, transaction’da bize sağlanan esnekliğin kapsamı gibi düşünebiliriz. En laçkadan en sıkıya doğru sıralarsak:
Read Uncommitted
Read Committed
Repeatable Read
Serializable
Hangi seviyeyi istediğinizi transaction başladıktan sonra (ama herhangi bir query çalıştırmadan önce) belirtebiliyorsunuz.
begin transaction;
-- 4 seviyeden birini isteyebilirsiniz
set transaction isolation level read uncommitted;
set transaction isolation level read committed;
set transaction isolation level repeatable read;
set transaction isolation level serializable;PostgreSQL veritabanı, standartta belirtilen dört davranışın üçünü destekliyor. Gerçek anlamda “read uncommitted” desteği yok. Arkada onu da “read committed” gibi yapıyor. Bu ne demek? Standartta tanımlanan garantiden biraz daha fazlasını, bir sonraki seviyedeki garantiyi veriyor.
Read Uncommitted
Bitmemiş transaction’lardaki değişiklikleri görebileyim.
“Read uncommitted” seviyesinde, A transaction’ı B' transaction’ında gerçekleşen ama henüz commitlenmeyen işlemin etkisini görebiliyor. Bu duruma “dirty read” deniyor, yani henüz nihai duruma ulaşmamış bir transaction’daki geçici etkiyi görmek gibi. PostgreSQL’da bu mümkün değil. Size her zaman şu garantiyi veriyor: okuduğun veri, devam eden başka bir transaction’ın yarım kalmış işlemlerinden etkilenmeyecektir.
Read Committed
Commit’leneni okuyabileyim.
Varsayılan seviye bu, eğer “set transaction isolation level…” diyip bir seviye seçmezseniz, transaction’larınız bu seviyede başlıyor. A transaction’ı B’deki değişiklikleri B commit’lendiği an görebiliyor.
-- T1 anında A kullanıcısı akiyeyi 30.000 görüyor
begin transaction;
set transaction isolation level read committed;
select * from hesaplar; isim | bakiye
--------+--------
selcuk | 30000-- T2 anında B kullanıcısı (başka bir transaction) güncelleme yapıyor
update hesaplar set bakiye=40000;
-- burada transaction otomatik açıldı ve commitlendi-- T3 anında A kullanıcısı (devam eden transaction'ında) güncellemeden haberdar oluyor
select * from hesaplar;
isim | bakiye
--------+--------
selcuk | 40000Eğer B commit’lemeseydi, o değişiklik A’da görünmeyecekti, A eski değeri okumaya devam edecekti. Şu şekilde deneyip görebilirsiniz:
-- T1 anında A kullanıcısı bakiyeyi 40.000 görüyor
begin transaction;
set transaction isolation level read committed;
select * from hesaplar;
isim | bakiye
--------+--------
selcuk | 40000-- T2 anında B kullanıcısı (başka bir transaction) güncelleme yapıyor
begin transaction;
update hesaplar set bakiye=50000;
-- güncelleme yapıldı fakat henüz commit'lenmedi-- T3 anında A kullanıcısı (devam eden transaction’ında)
-- güncellemeden haberdar değil çünkü commit'lenmemişti
select * from hesaplar;
isim | bakiye
--------+--------
selcuk | 40000Repeatable Read
Başta gördüğüm değerler değişmesin.
Transaction başladığı anda veritabanı ne haldeyse (bu transaction’da yapılan değişiklikler haricinde) o halde kalsın. Diğer transaction’lar değişiklikler yapabilir fakat bu değişiklikler görünmeyecek. Örnek vermek gerekirse:
-- T1 anında A kullanıcısı
-- bakiyeyi 50.000 görüyor
begin transaction;
set transaction isolation level repeatable read;
select * from hesaplar;
isim | bakiye
--------+--------
selcuk | 50000-- T2 anında B kullanıcısı (başka bir transaction)
-- güncelleme yapıyor
update hesaplar set bakiye=60000;
-- burada transaction otomatik açıldı ve commitlendi-- T3 anında A kullanıcısı (devam eden transaction’ında)
-- commit'lenen güncellemeden haberdar değil
select * from hesaplar;
isim | bakiye
--------+--------
selcuk | 50000Repeatable read seviyesindeki transaction’lar başlangıçta veritabanının bir “snapshot”ını alıp, commit’leninceye kadar aynı snapshot’a bakıyorlar gibi düşünebiliriz. Veritabanı bu seviyedeki garantiyi “snapshot isolation” metoduyla veriyor. Her transaction, veritabanının kendi başladığı zamandaki şeklini görüyor.
Bu seviyeyi kullanıyorsanız, uygulama katmanında transaction’ları tekrar denemeniz gerekebilir. Farz edin ki repeatable read olarak başlattığınız transaction bakiyeyi okudu ve 60000 olarak gördü. Bu esnada başka bir transaction bakiyeyi güncelledi ve commit’lendi. Repeatable read olan transaction bu aşamada gidip o güncellenen değeri tekrar güncellemek isterse veritabanı hata verecek ve transaction iptal edilecektir. Çünkü değişiklik yapmaya karar verdiğinizdeki şartlar artık değişti.
-- T1 anında A kullanıcısı
-- bakiyeyi 60.000 görüyor
begin transaction;
set transaction isolation level repeatable read;
select * from hesaplar; isim | bakiye
--------+--------
selcuk | 60000-- T2 anında B kullanıcısı (başka bir transaction)
-- güncelleme yapıyor
update hesaplar set bakiye=70000;
-- burada transaction otomatik açıldı ve commitlendi-- T3 anında A kullanıcısı (devam eden transaction’ında)
-- bakiyeyi güncelleyemiyor çünkü başka transaction'da değer çoktan değiştirildi
update hesaplar set bakiye=80000;
ERROR: could not serialize access due to concurrent updateBu hatayı alan uygulama, gerek görürse transaction’ı tekrar deneyebilir. Şartlar değiştiği için başa dönmek ve yeni bir snapshot okumak zorunda.
Serializable
Eş zamanlı çalışan transaction’lar tutarsızlık oluşturamasın.
En güçlü garanti serializable seviyesinde mevcut. Dolayısıyla en hantal olan da bu, çünkü veritabanı sizin adınıza ekstra kontroller yapıyor transaction’ların birbirini etkileyip etkilemediğini tespit edebilmek için. Bu tespit edildiğinde de, transaction’lardan bazılarını şu mesajla iptal ediyor
ERROR: could not serialize access due to read/write dependencies among transactions
DETAIL: Reason code: Canceled on identification as a pivot, during commit attempt.
HINT: The transaction might succeed if retried.Örnek olarak nöbetçiler tablosunu ele alalım ve tutarsız bir durum oluşturmaya çalışalım.
CREATE TABLE nobetciler (
isim text PRIMARY KEY,
aktif boolean NOT NULL
);
INSERT INTO nobetciler VALUES
('ahmet', true),
('mehmet', true);Bu tablodan aktif nöbetçilere bakıp, bazı nöbetçileri pasife çeken bir uygulama yapmış olsak ve bu uygulamadan veritabanına eş zamanlı transaction’lar açsak. Tutarsızlığımız şu olsun: uygulamada tek aktif nöbetçi istiyoruz, ve iki transaction da farklı nöbetçileri pasife çekiyor ama birbirlerinden haberleri yok.
begin transaction;
-- Bu transaction'a A diyelim
set transaction isolation level serializable;
SELECT count(*) FROM nobetciler WHERE aktif = true;
count
-------
2
-- hmm, 2 nöbetçi varmış, birini pasife çekeyim
UPDATE nobetciler SET aktif = false WHERE isim = 'ahmet';
COMMIT;begin transaction;
-- Bu transaction'a B diyelim
set transaction isolation level serializable;
SELECT count(*) FROM nobetciler WHERE aktif = true;
count
-------
2
-- hmm, 2 nöbetçi varmış, birini pasife çekeyim
UPDATE nobetciler SET aktif = false WHERE isim = 'mehmet';
COMMIT;İki transaction’ın aynı anda başladığını düşünelim. Birinin okuduğu veriyi (select) diğeri değiştirdiği için (hata mesajında read/write dependency olarak geçen durum), serializable modunda PostgreSQL buna izin vermeyecek. Bu iki transaction’dan hangisi önce commit’lerse o başarıyla tamamlanacak, sona kalan dona kalacak ve hata alacak.
Eğer serializable modunda olmasaydık, ikisi de commit’leyebilecekti ve hiç aktif nöbetçi kalmamış olacaktı.
Özet
Transaction isolation seviyelerini öğrendik. Mutlaka kendiniz deneyip görün, o şekilde daha iyi pekişir. Eğer anlaşılmayan bir yer varsa da yorumlarda belirtebilirsiniz. Son olarak, PostgreSQL resmî dokümanları çok teferruatlı ve maalesef biraz robotik olmakla birlikte, transaction isolation konusundaki şu tablo anlattıklarımın güzel bir özetini veriyor.


