Entity Framework Core’da İzolasyon Seviyeleri ve Veri Güvenliği

Transactions specify an isolation level that defines how one transaction is isolated from other transactions.

Cihat Solak
Intertech
9 min readJun 20, 2023

--

Understanding Isolation Levels in a Database Transaction

ISOLATION SEVİYESİ ARTTIKÇA TUTARLILIK ARTAR. EĞER Kİ VERİ TABANINDA TUTARLILIK ÖNEM ARZ EDİYORSA ISOLATIN SEVİYESİNİ ARTTIRMALISINIZ. EŞ ZAMANLI OKUMA SEVİYESİ ÖNEM ARZ EDİYORSA ISOLATION SEVİYESİZİ AZALTMALISINIZ.

Herhangi bir işlemde isolation seviyesi belirtmezseniz, varsayılan isolation seviyesi READ COMMITTED’dir. 👌 Bir transaction başlattığını düşünürsek default isolation seviyesi read committed olduğu için anlık olarak farklı transactionlar tarafından commit edilmeyen (veri tabanına yansıtılmamış) veriyi SQL tarafında görüntüleyemezsiniz. 👁️‍🗨️

using var transaction = _context.Database.BeginTransaction(IsolationLevel.ReadCommitted);

User user01 = new()
{
Name = "Deneme 1"
};

_context.Users.Add(user01);
_context.SaveChanges(); // 1. SaveChanges

User user02 = new()
{
Name = "Deneme 2"
};

_context.Users.Add(user02);
_context.SaveChanges(); // 2. SaveChanges

transaction.Commit(); // Transaction Commit !

Yukarıdaki örneği ele aldığımızda, transaction.Commit() edilmediği sürece veri tabanında yapacağınız SELECT * FROM USERsorgusunda “Deneme 1” ve “Deneme 2” adlı kullanıcıları görüntüleyemezsiniz. Çünkü default isolation seviyesi READ COMMITTED’dir.

Isolation, birden fazla transaction işleminin birbiriyle olan ilişkisini tanımlayan ve birbirleriyle olan izolasyon (etkilenme) seviyesini belirlememize imkan veren olgudur. EF Core’a özgü bir durum değildir!!! 💥 Veri tabanıyla ilgili konudur.

Isolation Nedir? Neyi İfade Eder? Hangi Sorulara Yanıttır?

❓ Bir transaction bir satırı güncellerken, farklı transaction aynı satırı okuyabilecek mi?

❓ Bir transaction bir satırı güncelledi fakat commit etmedi. Dolayısıyla farklı bir transaction, commit edilmemiş olan veriyi mi okuyacak? Yoksa commit edilmesi mi gerekli?

❓ Bir transaction insert yaptığında, diğer transaction insert yapılan veri’yi görebilecek mi? göremeyecek mi?

Isolation konusu önemi, kullanıcı 👥 sayısıyla da doğru orantılıdır diyebiliriz. Kullanıcı sayısı arttıkça bir çok yönden önemli hale gelecektir.

Isolation, genellikle RDBMS (İlişkisel veri tabanı) türünde çok daha yaygındır.

Isolation Seviyeleri (Microsoft SQL Server)

MSSQL toplamda 5 adet isolation seviyesine sahiptir. En güçlü isolation seviyesi SERIALIZABLE’dır.

Yukarıdan (Read uncommitted) aşağıya (Serializable) doğru gittikçe;

  • Tutarlılık artar. Dolayısıyla en tutarlı seviye serializable, en tutarsız seviye read uncommitted’dır. ✔️
  • Bir transaction’ın diğer transactionları bloklaması artar. ✔️
  • Kaynak tüketimi artar. ✔️
  • Eş zamanlı okuma sayısı azalır. Yani aynı anda yapılan işlemlerin (kullanıcıların veri okuması, insert etmesi vb.) performansında azalma yaşanır. ✔️
  • Concurrency effect (Eş zamanlılık) azalır. ✔️

💎💎 Sadece “Read Uncommitted” seviyesi commit edilmemiş verileri okur. Diğer tüm seviyeler commit edilmiş verileri okuyacaktır.

Concurrency Effects (Eşzamanlılık Etkileri)

1- Lost Updated (Kayıp Güncellendi — Son Kayıp)

İki farklı transaction aynı anda başlarsa (eş zamanlı) SQL Server transactionlardan birini kabul eder işler sonra diğerini işleme alır.

// 1. Transaction
var user = _context.Users.Find(1);
user.Age = 15;

_context.SaveChanges();

// 2. Transaction
var user = _context.Users.Find(1);
user.Age = 25;

_context.SaveChanges();

Yukarıdaki örneği göz önünde bulundurduğumuzda SQL server 2. transaction’i seçtiğini düşürsek, ilgili kullanıcının yaşı ilk etapda 25 olarak güncellenecektir. Daha sonra 1. transaction devreye girerek kullanıcının yaşını 15 olarak güncelleyecektir. İşte bu durumu Lost Updated olarak adlandırmaktayız. Son yapılan transaction ilk yapılan transaction’ı eziyor. Fakat aynı anda başlarsa hangisinin diğerini ezdiğini bilemeyiz. 😔

2- Dirty Read (Kirli Okuma)

Isolation seviyeleri yardımıyla dirty read okumalarının önüne geçilebilir.

// 1. Transaction
using var transaction = _context.Database.BeginTransaction();

var user = _context.Users.Find(1);
user.Age = 15;

_context.SaveChanges();

transaction.Commit();

// 2. Transaction
var users = _context.Users.ToList();

Birinci transaction kullanıcının yaşını 15 yapıp, _context.SaveChanges() dediği sırada ikinci transaction kullanıcı listesini alırsa buna Dirty Read🗑️ denir. Çünkü birinci transaction ilgili güncellemeyi daha commit transaction.Commit() etmemişti. Dolayısıyla daha commit edilmemiş olan veriyi okunursa, kirli okuma dediğimiz dirty read meydana gelmektedir. Commit edilmemiş veri başarılı bir şekilde commit edilecek mi? bilemeyiz.

3. Nonrepetable Reads (Tekrarlanamaz Okumalar — Güncelleme Olayı Var)

Isolation seviyeleri yardımıyla nonrepetable reads okumalarının önüne geçilebilir.

// Transaction 1
var users = _context.Users.ToList();

// Transaction 2
var user = _context.Users.Find(1);
user.Age = 15;

_context.SaveChanges();

// Transaction 1
var users = _context.Users.ToList();

Burada sıralı bir işlem bulunmaktadır. Şöyle ki,

  • 1. transaction tüm kullanıcıları listeledi.
  • 2. transaction ilgili kullanıcının yaşını 15 olarak güncelledi.
  • 1. transaction tekrar kullanıcıları listediğinde primary key değeri 1 olan kullanıcının yaşı 15 olarak listelenecektir.

Dolayısıyla buna nonrepetable reads olayı denmektedir.

— — Tekrar edelim mi? 🙄

Örneğin transaction 1 kullanıcıları listeledi. Bu sırada araya transaction 2 girerek, ilgili kullanıcıyı güncellendiğinde, transaction 1 tekrar kullanıcıları listelemek istediğinde, güncellenmiş (bir öncekinden farklı) kullanıcı listesine sahip olacaktır. Çünkü transaction 1 işlem yaparken bir yandan transaction 2 de güncelleme işlemi yapıyordu gibi düşünebiliriz.

Bu tür problemi yaşamak istemiyorsak, bir transaction içerisinde başta listelediğim kullanıcı verisiyle, yine aynı transaction içerisinde farklı bir T anında listeğim kullanıcı listesi aynı olması gerekmektedir. Neden? Çünkü TUTARLILIK 💕.

4. Phantom Reads (Hayalet Okumalar — Ekleme Olayı Var)

Nonrepetable reads ile benzerdir.

// 1. Transaction
var users = _context.Users.ToList();

// 2. Transaction
var user = _context.Users.Add(new User
{
Name = "Cihat",
Age = 1
});
_context.SaveChanges();

// 3. Transaction
var users = _context.Users.ToList();

Birinci ve üçüncü listeleme işleminin arasına yeni bir kayıt (insert) girdiğinde phantom reads olarak adlandırılmaktadır. Örnekte olduğu gibi

  • 1. transaction listeleme yaptı.
  • 2. transaction yeni bir kullanıcı ekledi.
  • 3. transaction tekrar bir okuma işlemi yaptığında +1 fazla veri olacak.

Bu durum phantom reads 👽 olarak adlandırılmaktadır.

Phantom reads’de ekleme olayı, Nonrepetable reads’de güncelleme olayı bulunmaktadır. İkisinin arasındaki fark budur. ✍️

Isolation Levels

1- Read Uncommitted

Değişlik yapılmış (SaveChanges) fakat (Commit) edilmemiş veriler okunur. Dolayısıyla bu isolation seviyesinde Dirty Read meydana gelebilir. Okunan veri üzerinde farklı bir transaction Insert, Update, Delete işlemi yapabilir.

Bir transaction içerisinde okuma yaparken aynı zamanda güncelleme/silme işlemi yapılabilir. Fakat bir transaction Read Uncommitted’dayken dahii, güncelleme yaparken başka bir transaction aynı satır için tekrar güncelleme/silme işlemi yapamaz.

2- Read Committed

SQL Server’ın varsayılan isolation seviyesidir ve sadece commit edilen verileri okur. Dolayısıyla commit edilmeyen verileri okumayacağı için dirty read meydana gelmez. İşlem gören veri üzerinde başka bir transaction insert/update/delete işlemi yapabilir.

3- Repeatable Read (İnsert OK! Update, Delete No!)

Read Committed ile aynı prensibte olup commit edilmiş verileri okur. Aralarındaki fark ise şudur: farklı bir transaction insert işlemi yapabilir, ancak UPDATE/DELETE İŞLEMİ YAPAMAZ.

Örneğin, transaction 1 ile insert/update/delete yapıldığı ancak commit edilmediği esnada transaction 2 okuma işlemi yaparsa orjinal veriyi okur. Yalnız 1–10 arasındaki id (identifier) sahip kullanıcıları transaction içerisinde okurken farklı bir transaction bu kullanıcılar üzerinde update/delete işlemi yapamaz.

Dolayısıyla Read Committed seviyesine ek olarak bu seviyede update/delete işlemi yapılamaması anlamına gelmektedir.

4-Serializable

Sadece commit edilmiş veriyi okur. Repeatable Read ile benzerdir fakat bir farkı bulunmaktadır. Repeatable read’de insert işlemi var, update/delete işlemi yoktu. Serializable’da ise insert/update/delete işlemi yoktur.

UPDATE işlemi olmadığından doılayı Unrepeatable problemi oluşmuyor, INSERT olmadığından dolayı phantom problemi oluşmuyor. Dolayısıyla çok tutarlı bir isolation seviyesine ulaşıyoruz.

Phantom (Hayalet) Problemi: Transactionlar arasında insert işleminin gelmesidir.

5- Snapshot

Transaction boyunca veriyi tutarlı bir seviyede tutar. Örneğin transaction başladıktan sonra farklı bir transaction insert/update/delete işlemi yaparsa güncel transaction içerisinde bu değişiklikler görünmez.

buraya foto gelecek..

Snapshot isolation seviyeside bir veri okuduğunuzda arkasından bir kez daha veri okuması yaptığınızı düşünürsek hep aynı sonucu alırsınız. Aslında bu durum size şöyle bir artı sağlar. Bir transaction başladıktan sonra başka bir transaction insert/update/delete yapabilir ama diğer isolation seviyelerinde yapamıyordu. Ancak güncel transaction içerisinde bu değişikleri o an için siz göremezsiniz (snapshot seviyesinde).

Toparlayalım. Snapshot seviyesinde verileri listelediniz. Farklı bir transaction sizin listemiş olduğunuz veriler üzerinde insert/update/delete işlemi yapabilir. Ancak siz bunu mevcut transaction içerisinde göremezsiniz. İşte buna snapshot isolation deniyor.

Amaç?

Performansı arttırmak, locklamayı azaltmak için yapılır. Sonuç olarak Serializable seviyesi insert/update/delete işlemi için lock koyuyordu. Ayrıca transaction içerisinde bir veri tutarlılığı söz konusudur. Repeatable read ve Phantom etkisi burada yaşanmaz. Oluşturmuş olduğunuz transaction içerisinde veri tutarlılığı sağlanır.

Concurrency (Eş Zamanlılık)

Birden fazla kullanıcı aynı zamanda aynı veriyi güncellemeye çalışırsa ne olur? 🤔

Eşzamanlılık Türleri

  • Pessimistic (Kötümser) Concurrency Control (xlock)
  • Optimistic (İyimser) Concurrency Control
https://cult.honeypot.io/reads/optimistic-vs-pessimistic-concurrency

Concurrency, birden fazla kullanıcın aynı anda aynı verinin güncellemesiyle meydana gelir. EF Core varsayılan olarak optimistic concurrency control’e sahiptir.

Pessimistic concurrency control’ü nasıl sağlarım? diye bir soru akıllara gelebilir. Herhangi bir sorguya xlock ekleyerek bunu uygulayabilirsiniz. Çünkü xlock yazdığınız zaman ilgili sorgu kilitlenir quearyable lock. Başka transaction’lar okuma dahi yapamaz. Bu işlem gerçekten kritik ve performansı yüksek seviyede düşüren bir tercihtir. Dolayısıyla çok tercih edilen bir yöntem değildir.

Optimistic concurrency nasıl meydana gelir?

Bu ekstra kodlama gerektirmeyen, varsayılan davranıştır. Son yapılan transaction ilk yapılan transaction’ı ezebilir. Örneğin iki kişi aynı kullanıcıyı güncellemeye çalıştığında bu iki transaction sırasıyla işlenir. Dolayısıyla biri diğerini ezecektir.

Optimistic Concurrency Senaryolar 🤼

1- Client Wins Or Last in Wins Scenario (İstemci Kazanır veya Kazanır Senaryosunda Sonuncu Olur)

EF core tarafından otomatik olarak gerçekleştirilen senaryodur. Birden fazla kişi aynı anda aynı entity için güncelleme işlemi gerçekleştirdiğinde (milisaniyeler dahil aynı zamanda) EF core burda kendisi sıralama yaparak ilgili transactionları veri tabanına yansıtmaktadır.

2- Store Wins (Mağaza Kazandı)

Buradaki mevcut durumu ele almak (handle) etmek isteyebiliriz. Şöyle ki, birden fazla kullanıcı aynı anda aynı entity için güncelleme talebinde bulunduğunda ilgili entity’nin daha önce güncellendiğini anlarsa bunu diğer kullanıcılara bildirebiliriz. Nasıl Yani?

Örneğin User1 banka tablosundaki X bankasını güncellendi. Ardından User2 de X bankasını güncellemek istediğinde “bu banka bilgileri daha önce user1 tarafından güncellendi. emin misin? bilgileri ezecek misin? yoksa güncel verileri görmek ister misin? vb.” gibi benzer bir uyarı mesajıyla karşılayabiliriz. Dolayısıyla kullanıcının seçimine göre işlem yaparız.

— Store Wins Senaryosunu Nasıl Uygulanır?

İki anahtar kelime var. Bunlar, DbConcurrencyException ve Row version .

EF Core optimistic concurrency de store wins durumunu ele almak amacıyla bize yardımcı oluyor. Store wins 2 aşamada kontrol edilebilir. Birincisi ilgili tabloda row version isminde bir sütun oluşturmak. Bu row version sütunu artan bir değer veya guid olabilir. Dolayısıyla row version için ilgili satır her güncellendiğinde değeri artacak ya da guid gibi random bir değer üretilecektir.

Güncelleme esnasında EF Core veri tabanındaki Row version ile istekden gelen row version’ı karşılaştırarak işlemi kontrol ediyor. Eğer bu row version değerleri eşit değilse, DbConcurrencyException hatasını fırlatıyor. Anlaşılan o ki her bir güncellemede ilgili row version değeri random bir değer ya da birer birer artar değer olarak güncelleniyor.

Ekstra bir kodlamaya ihtiyacımız olmadığını belirtmiştik. Çünkü EF Core her güncellemede random bir değer üretebiliyor. Yapılması gereken DbConcurrencyException hatasını kontrol edebilmek ve ilgili tabloya Row version sütununu eklemek. Başlayalım!

public class Shoe
{
public string Color { get; set; }
public int Size { get; set; }

//[TimeStamp] --> Data Annotations
public byte[] RowVersion { get; set; }
}

Fluent API --> modelBuilder.Entity<Shoe>().Property(p => p.RowVersion).IsRowVersion();

Row version tipinin kesinlikle byte[] olması gerekmektedir.

Best practices açısından Fluent API kullanımının amacı nokta işareti ile arka arkaya tüm metotlarla beraber ilgili property’i set edebilmemizden kaynaklıdır.

EF Core, entity üzerinde değişiklik yaptığımızda kendisi row version üzerinde güncelleme yapıyor. Başka bir kullanıcı güncelleme yapmaya çalıştığı anda veri tabanındaki row version ile kullanıcıdan gelen row version ile kıyaslayıp 🆚 işlem yapıyor.

[HttpPost]
public IActionResult Update(Shoe shoe)
{
try
{
_appDbContext.Shoes.Update(shoe);
_appDbContext.SaveChanges();
}
catch (DbUpdateConcurrencyException dbUpdateConcurrencyException)
{
var currentProduct = dbUpdateConcurrencyException.Entries[0].Entity as Shoe; //Kullanıcının güncellemek istediği data
var databasePropertyValues = dbUpdateConcurrencyException.Entries[0].GetDatabaseValues(); //Veri tabanından entity'nin değerini almak

if (databasePropertyValues is null) //veri tabanında değeri yoksa entity daha önce silinmiştir.
{
ModelState.AddModelError(string.Empty, "Bu ayakkabı başka bir kullanıcı tarafından silindi.");
return View(shoe);
}

var databaseProductEntity = databasePropertyValues.ToObject() as Shoe; //veri tabanından değeri alınan entity'i ayakkabı sınıfına dönüştürme

ModelState.AddModelError(string.Empty, "Bu ayakkabı başka bir kullanıcı tarafından güncellendi.");
}

return View(shoe);
}

--

--