İçeriğe geç
academia.sh

Ders 11 / 20

Kilitleme Davranışı

Paylaşımlı ve dışlayıcı kilitler, kilit ayrıntı düzeyi, iki bağlantıyla üretilen gerçek bir çakışma ve hata iletisi, yükseltme çakışması, kilitlenme döngüsü ve bekleme süresi sınırı.

İçindekiler

Yalıtım düzeyleri neyin görüneceğini söyler, bedelini söylemez. Bedel, aynı veriye dokunan işlemlerin birbirini beklemesidir. Beklemeyi yöneten mekanizma kilittir (lock): bir işlem bir kaynağa dokunmadan önce üzerinde hak alır, işi bitene kadar tutar, sonra bırakır.

Bu ders kilit türlerini tanımlar, iki bağlantıyla gerçek bir çakışma üretir ve motorun verdiği hatayı okur.

İki Kilit Türü

Temel model iki kilit türüne dayanır.

Paylaşımlı kilit (shared lock) okuma içindir. Aynı kaynak üzerinde birden çok paylaşımlı kilit birlikte bulunabilir; okuyucular birbirini engellemez.

Dışlayıcı kilit (exclusive lock) yazma içindir. Bir kaynakta dışlayıcı kilit varsa üzerine ne paylaşımlı ne başka bir dışlayıcı kilit konabilir.

Uyuşma kuralı tek cümleyle özetlenir: okuma okumayı engellemez, yazma her şeyi engeller. Çok sürümlü eşzamanlılık denetimi kullanan motorlar bu kuralı okuma tarafında gevşetir — okuyucular eski sürümü gördüğü için yazarı beklemez. Bu, İlişkisel Veritabanı Yönetimi kursunda ele alınan bir gerçekleştirim seçimidir.

Kilidin ne üzerine konduğu ayrı bir eksendir: satır, sayfa, tablo ya da veritabanının tamamı. İnce ayrıntı düzeyi eşzamanlılığı artırır ama kilit sayısını ve muhasebesini büyütür. Aşağıdaki gözlemin yapıldığı motor, yazma için veritabanının tamamını kilitler; satır kilidi kullanan bir motorda aynı senaryoda farklı satırlara yazan iki işlem birbirini engellemez. Bu, motora göre değişen bir davranıştır ve taşınabilir kod yazarken bilinmesi gerekir.

Gerçek Bir Çakışma

Aşağıdaki betik aynı dosyaya iki bağlantı açar. Birinci bağlantı bir yazma işlemi açar ve açık tutar; ikinci bağlantı yazmayı dener:

rm -f kilit.db kilit.db-wal kilit.db-shm
cat > kilit.mjs <<'JS'
import { DatabaseSync } from 'node:sqlite';

const a = new DatabaseSync('kilit.db');
const b = new DatabaseSync('kilit.db');
a.exec('PRAGMA journal_mode = WAL');
a.exec(`CREATE TABLE kitap(id INTEGER PRIMARY KEY, baslik TEXT, rafta INT);
        INSERT INTO kitap VALUES (1,'Kayip Zaman',1),(2,'Sessiz Ev',1);`);

a.exec('BEGIN IMMEDIATE');
a.exec('UPDATE kitap SET rafta = 0 WHERE id = 1');
console.log('A: yazma işlemi açık, 1 numaralı kitap güncellendi');

try {
  b.exec('BEGIN IMMEDIATE');
  b.exec('UPDATE kitap SET rafta = 0 WHERE id = 2');
  console.log('B: güncelledi');
} catch (hata) {
  console.log('B hata aldı:', hata.code, '-', hata.message);
}

console.log('B okuyabiliyor mu:',
            b.prepare('SELECT rafta FROM kitap WHERE id = 1').get().rafta);
a.exec('COMMIT');
console.log('A kesinleştirdi; B artık yazabilir');
b.exec('BEGIN IMMEDIATE');
b.exec('UPDATE kitap SET rafta = 0 WHERE id = 2');
b.exec('COMMIT');
console.log('son durum:', JSON.stringify(a.prepare(
  'SELECT id, rafta FROM kitap ORDER BY id').all()));
JS
node kilit.mjs
rm -f kilit.db kilit.db-wal kilit.db-shm
A: yazma işlemi açık, 1 numaralı kitap güncellendi
B hata aldı: ERR_SQLITE_ERROR - database is locked
B okuyabiliyor mu: 1
A kesinleştirdi; B artık yazabilir
son durum: [{"id":1,"rafta":0},{"id":2,"rafta":0}]

Üç gözlem var.

Birincisi, B ikinci kitaba yazmak istiyordu, A ise birinciye yazmıştı. Yine de engellendi: bu motorda yazma kilidi veritabanı düzeyindedir. Satır kilidi kullanan bir motorda bu deneme başarılı olurdu.

İkincisi, hata iletisi bekleme değil reddetme bildiriyor. Bekleme süresi sınırı sıfır olduğu için motor kuyruğa girmek yerine hemen döndü. İleti biçimi motora özgüdür; ortak olan, çakışmanın uygulamaya bir hata olarak ulaşmasıdır.

Üçüncüsü, B yazamazken okuyabildi ve A’nın kesinleştirmediği değeri değil, eski değeri gördü. Okuyucunun yazardan bağımsız ilerlemesi, önceki dersteki anlık görüntü davranışının kilit tarafındaki karşılığıdır.

A kesinleştirdikten sonra B’nin aynı yazması sorunsuz geçti. Çakışma kalıcı bir engel değil, zamanlamaya bağlı bir yarıştır.

Yükseltme Çakışması

Daha ince bir durum, iki işlemin de önce okuyup sonra yazmasıdır. İkisi de okuma hakkıyla başlar, sonra yazma hakkına yükselmek ister. Biri yükselirse diğerinin isteği karşılanamaz:

rm -f kilit2.db kilit2.db-wal kilit2.db-shm
cat > kilitlenme.mjs <<'JS'
import { DatabaseSync } from 'node:sqlite';

const a = new DatabaseSync('kilit2.db');
const b = new DatabaseSync('kilit2.db');
a.exec('PRAGMA journal_mode = WAL');
a.exec(`CREATE TABLE kitap(id INTEGER PRIMARY KEY, baslik TEXT, rafta INT);
        INSERT INTO kitap VALUES (1,'Kayip Zaman',1),(2,'Sessiz Ev',1);`);

a.exec('BEGIN');
b.exec('BEGIN');
console.log('A okudu:', a.prepare('SELECT rafta FROM kitap WHERE id = 1').get().rafta);
console.log('B okudu:', b.prepare('SELECT rafta FROM kitap WHERE id = 1').get().rafta);

a.exec('UPDATE kitap SET rafta = 0 WHERE id = 1');
console.log('A yazdı');
try {
  b.exec('UPDATE kitap SET rafta = 0 WHERE id = 1');
  console.log('B yazdı');
} catch (hata) {
  console.log('B hata aldı:', hata.code, '-', hata.message);
}
b.exec('ROLLBACK');
a.exec('COMMIT');
console.log('son durum:', JSON.stringify(a.prepare(
  'SELECT id, rafta FROM kitap ORDER BY id').all()));
JS
node kilitlenme.mjs
rm -f kilit2.db kilit2.db-wal kilit2.db-shm
A okudu: 1
B okudu: 1
A yazdı
B hata aldı: ERR_SQLITE_ERROR - database is locked
son durum: [{"id":1,"rafta":0},{"id":2,"rafta":1}]

İki işlem de kitabı rafta gördü; ikisi de ödünç vermek istedi. Yalıtım olmasaydı ikisi de başarılı olur, kitap iki üyeye birden verilmiş görünürdü — önceki dersteki kayıp güncelleme. Kilit bunu engelledi: biri kazandı, diğeri hata aldı.

Hata alan taraf için doğru davranış, işlemi geri alıp yeniden denemektir. Yeniden denemede okuma güncel değeri getirir ve kitabın artık rafta olmadığı görülür. Çakışma çözümü, uygulamanın bu yeniden deneme döngüsünü yazmasıyla tamamlanır; motorun tek başına yaptığı, çakışmayı sessiz bırakmamaktır.

Bu deseni hatasız kurmanın standart yolu, okumayı baştan yazma niyetiyle yapmaktır. Standart SQL bunun için okunan satırları kilitleyen bir yazım tanımlar:

SELECT rafta FROM kitap WHERE id = 1 FOR UPDATE;

Bu yazım, satırı okurken üzerine dışlayıcı hak alır; ikinci işlem okuma aşamasında beklemeye girer ve yükseltme çakışması hiç oluşmaz. Yukarıdaki gözlemin yapıldığı motor bu yan tümceyi tanımaz; onun karşılığı, işlemi BEGIN IMMEDIATE ile yazma niyetiyle açmaktır. Deyimin kendisi burada çalıştırılmamıştır, biçimi gösterilmiştir.

Kilitlenme

İki işlem birbirinin tuttuğu kaynağı beklerse hiçbiri ilerleyemez. Bu duruma kilitlenme (deadlock) denir ve en yalın hâli iki kaynakla oluşur:

Adım İşlem A İşlem B
1 kitap satırını kilitler
2 uye satırını kilitler
3 uye satırını ister, bekler
4 kitap satırını ister, bekler

Dördüncü adımdan sonra bekleme çizgesinde bir çevrim vardır ve hiçbir bekleme kendiliğinden sona ermez. Motorlar bunu iki yoldan çözer. Kilitlenme algılayan motorlar bekleme çizgesini tarar, çevrim bulunca bir işlemi kurban seçip geri alır; kurbana bir kilitlenme hatası döner. Algılama yapmayan motorlar bir bekleme süresi sınırı koyar; süre dolunca bekleyen işlem hata alır. İkinci yolda gerçek bir kilitlenme ile yalnızca yavaş bir işlem birbirinden ayırt edilemez.

Yukarıdaki gözlemde çevrim oluşmadı, çünkü o motor aynı anda tek yazara izin verir; ikinci yazar beklemeye bile geçmeden reddedilir. Kilitlenme, ince ayrıntı düzeyinde kilit tutan motorların sorunudur — daha çok eşzamanlılığın bedeli.

Önleme, çevrimin oluşmasını engellemekle olur:

  • Kaynaklara her zaman aynı sırayla dokunmak. Her iki işlem de önce kitap sonra uye kilitlerse üçüncü ve dördüncü adım aynı yönde olur, çevrim kapanmaz.
  • İşlemi kısa tutmak. Kilit tutma süresi kısaldıkça çakışma olasılığı düşer.
  • Gerekiyorsa tüm kilitleri baştan almak; ilerledikçe hak yükseltmemek.

Bu üçü, çakışmayı yok etmez, olasılığını düşürür. Hata alma ihtimali kaldığı için, çakışabilecek her işlemin yeniden denenebilir yazılması gerekir: aynı işlemi ikinci kez çalıştırmanın ek bir yan etki üretmemesi, yani yinelemeye dayanıklı olması.

Bekleme Süresi Sınırı

Reddetme ile bekleme arasındaki seçim bir ayardır. Bekleme süresi sınırı verilirse, çakışan işlem hemen hata almak yerine belirtilen süre boyunca kilidin serbest kalmasını bekler:

rm -f kilit3.db kilit3.db-wal kilit3.db-shm
cat > bekleme.mjs <<'JS'
import { DatabaseSync } from 'node:sqlite';

const a = new DatabaseSync('kilit3.db');
const b = new DatabaseSync('kilit3.db', { timeout: 250 });
a.exec('PRAGMA journal_mode = WAL');
a.exec(`CREATE TABLE kitap(id INTEGER PRIMARY KEY, baslik TEXT, rafta INT);
        INSERT INTO kitap VALUES (1,'Kayip Zaman',1);`);

a.exec('BEGIN IMMEDIATE');
a.exec('UPDATE kitap SET rafta = 0 WHERE id = 1');

const basla = process.hrtime.bigint();
try {
  b.exec('BEGIN IMMEDIATE');
} catch (hata) {
  const gecen = Number(process.hrtime.bigint() - basla) / 1e6;
  console.log('B hata aldı:', hata.code);
  console.log('beklediği süre 250 ms sınırının üstünde mi:', gecen >= 250);
}
a.exec('COMMIT');
JS
node bekleme.mjs
rm -f kilit3.db kilit3.db-wal kilit3.db-shm
B hata aldı: ERR_SQLITE_ERROR
beklediği süre 250 ms sınırının üstünde mi: true

Ölçülen sürenin kendisi makineye ve yüke bağlı olduğu için yazdırılmadı; yazdırılan, sınırın aşılıp aşılmadığıdır. Sonuç, ilk gözlemdekiyle aynı hatadır — fark, hatanın hemen değil, belirtilen süre boyunca beklendikten sonra gelmesidir.

Sınırın seçimi bir ödünleşimdir. Kısa sınır, çakışmayı hızlı bildirir ve uygulamanın yeniden denemesine izin verir; uzun sınır, geçici çakışmaları uygulamaya hiç göstermez ama gerçek bir kilitlenmede kaynak tutmayı uzatır. İkisi de işlemlerin kısa tutulması varsayımı üzerine kuruludur.

Özet

  • Paylaşımlı kilit okuma, dışlayıcı kilit yazma içindir; okuma okumayı engellemez, yazma her şeyi engeller.
  • Kilit ayrıntı düzeyi satırdan veritabanına kadar değişir; ince düzey eşzamanlılığı artırır, muhasebeyi büyütür.
  • Gözlemde ikinci yazar database is locked hatası aldı ama okumayı sürdürebildi; çakışma kalıcı engel değil, zamanlamaya bağlı bir yarıştır.
  • Okuyup sonra yazan iki işlem yükseltme çakışması üretir; standart çözüm okumayı yazma niyetiyle kilitlemek, uygulama çözümü işlemi geri alıp yeniden denemektir.
  • Kilitlenme, bekleme çizgesinde çevrim oluşmasıdır; kaynaklara aynı sırayla dokunmak, işlemi kısa tutmak ve yeniden denenebilir yazmak önleyici alışkanlıklardır.

Sonraki Adım

Bu derse kadar işlemin sınırını, hata durumunda ne yapılacağını ve yeniden denemeyi hep uygulama kodu belirledi; veritabanı yalnız deyimleri aldı. Oysa bu mantığın bir kısmı veritabanının içinde de tanımlanabilir: ödünç verme kuralı bir yordama konabilir, uygulama o yordamı tek çağrıyla tetikleyebilir. Sonraki ders, sunucu tarafında çalışan yordam ve işlevlerin standart biçimini, sağladıklarını ve getirdiği ödünleşimleri ele alacak.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat