Firebird'da İşlemler
Firebird’de İşlemler: ACID, İzolasyon Seviyeleri, Kilitlenmeler ve Güncelleme Çakışmalarının Çözümü
Alexey Kovyazin, Vlad Khorsun ve Dmitry Kuzmenko’nun katkılarıyla, 08-NİS-2019
İçindekiler:
- İşlemler hakkında nasıl konuşacağız
- Şemalarla ACID
- Firebird’de işlem izolasyon seviyeleri
- Güncelleme çakışmalarının çözümü: Wait seçeneği
- Gerçek kilitlenme
İşlemlerin nasıl çalıştığını bilmek gerekli mi?
Muhtemelen evet, çünkü işlem kavramı basittir ve birçok geliştirici Firebird’de işlemleri doğru kullanmanın önemini hafife alır. Ancak, işlemlerin nasıl çalıştığını derinlemesine anladıktan sonra, performansla ilgili birçok gizemli şeyi kavrayabilirsiniz; örneğin, ani veritabanı yavaşlamaları (kötü işlem yönetimi nedeniyle ortaya çıkan aşırı kayıt sürümlerinin süpürülmesiyle bağlantılı).
Genel olarak, işlem kavramı bir durumdan diğerine geçen herhangi bir dinamik sisteme uygulanır. Örneğin, klasik bir işlem örneği, bir hesaptan diğerine para transferidir. Genellikle şöyle görünür:
Başla --- hesap 1'den hesap 2'ye para transferi
--hesap 1'i azalt
--hesap 2'yi artır
Bitir - işlemi onayla
Örnek, paranın hesap 1’den kaybolması ve hesap 2’de aynı anda görünmesi gerektiği gerçeğine dayanır; aksi takdirde, sistemde bir süre fazla para veya açıklanamayan para eksikliği olacaktır.
Veritabanları açısından, bir işlem genellikle diğer işlemlerden bağımsız olarak görülen bir veritabanında gerçekleştirilen bir grup işlem olarak tanımlanır. Benim açımdan, bu tanım diğer tanımlardan ne daha iyi ne de daha kötüdür, ancak her tanım gibi, bir DBMS’nin gerçek iç işleyişini ve mantığını bilmeden pek bir anlam ifade etmez.
Bir veritabanındaki işlemin sözde ACID gereksinimlerini karşılaması gerektiğine inanılır:
A - Atomiklik (Atomicity)
C - Tutarlılık (Consistency)
I - İzolasyon (Isolation)
D - Kalıcılık (Durability)
Veritabanı uygulamalarının birçok geliştiricisi bu kısaltmadan o kadar ilham alır ki, farklı DBMS’leri karşılaştırırken sık sık “ACID’de D’niz yok” gibi argümanlar kullanırlar (genellikle hemen ardından “ne düşündüğün umurumda değil” gelir).
Aslında her şey oldukça basittir - ACID, belirli bir DBMS’de bir işlemin uygulanmasına ilişkin bir dizi gereksinimdir; bazıları çok katıdır (örneğin, D - elbette kalıcılık önemlidir!) bazıları ise daha az katıdır - işlem izolasyon seviyelerine baktığımızda izolasyonun değişebileceğini göreceğiz.
Bu yüzden bu kısaltmanın gerçek anlamını hemen anlamaya çalışmak yerine, DBMS’lerin (ve özellikle işlemlerin) nasıl çalıştığının mantığını inceleyeceğiz ve ACID’i “ne anlama geldiği” yerine “nasıl yapıldığı” açısından ele alacağız.
İşlem yönleri karmaşık olduğundan, işlemlerin çalışmasını ve etkileşimini göstermek için grafiksel bir temsile - bir tür şemalara - ihtiyacımız var. Bu şemaları kullanarak mantıksal bir anlatı oluşturabilecek ve işlemlerin nasıl çalıştığının ayrıntılarına inebileceğiz.
Öncelikle bir zaman çizelgesi tanıtacağız çünkü işlemler zaman içinde gelişir. Zaman çizelgesi ihtiyacımız olan şekilde işaretlenecektir - ne saniyelere ne de dakikalara ihtiyacımız var, ancak işlemler arasındaki etkileşimin kilit adımlarına ihtiyacımız var:

Sonra bu zaman çizelgesine bir işlem ekleyeceğiz - onu, kenarları işlemin başlangıcına ve bitişine karşılık gelecek bir dikdörtgen şeklinde çizelim. Firebird’deki tüm işlemler numaralandırıldığından, işlem numarasını da belirteceğiz.

Böylece, şema t3 zamanında başlayan ve t10 zamanında biten 11 numaralı işlemi gösterir. Bir işlemin sona ermesinin iki yolu vardır - COMMIT, yani işlem içinde yapılan tüm değişiklikleri uygulamak ve ROLLBACK, yani işlem içinde yapılan tüm değişiklikleri iptal etmek. Bir işlemin nasıl bittiğini şu şekilde göstereceğiz:

Devam edebilmek için, bu şemalarda işlemlerin çeşitli parametrelerini belirtmemiz gerekecek ve bunları ilgili işlemi temsil eden dikdörtgenin sol alt köşesinde belirteceğiz - bu örnek, 11 numaralı işlemin snapshot izolasyon seviyesine sahip olduğunu gösterir.

“X işlemi veri ekler” veya “Y işlemi şu verileri okur” dediğimizde - bu biçimsel olarak yanlıştır, çünkü “X işlemi içinde değişiklikler yapıldı” demeliyiz. Yalnızca SQL ifadeleri veri okuyabilir veya ekleyebilir; bu nedenle, anlatım için önemliyse, bu ifadeleri işlem dikdörtgeninin içinde göstereceğiz:

Bu örnekte, T1 tablosu, i1 alanı, 100 değeri için INSERT işlemi var - bu işlem 11 numaralı işlem içinde gerçekleştirilir ve onaylanır.
Ayrıca, bazen bir işlemin sonucunu göstermemiz gerekecek; örneğin, aşağıdaki örnekte:

Bu örnek şunları gösterir:
- İzolasyon seviyesi parametresi snapshot olarak ayarlanmış 11 numaralı işlem (izolasyon seviyeleri daha sonra tartışılacaktır, burada sadece tam resmi çizmek için gösterilmiştir) t3 anında başlar
- T1 tablosunun i1 alanına 100 değerini ekleyen INSERT INTO T1(i1) values (100) işlemi t5 anında başlar VE t7 anında biter
- T1’den i1 değerini 100’e eşit döndüren SELECT i1 from T1 işlemi t8 anında başlar
- 11 numaralı işlem COMMIT ifadesiyle biter, yani 11 numaralı işlem tarafından yapılan değişiklikler veritabanına onaylanır
Böylece, işlem şemalarının yardımıyla veritabanında neler olduğunu ayrıntılı olarak tanımlayabilir ve işlemlerin nasıl çalıştığını öğrenebiliriz.
Artık işlem şemalarımız olduğuna göre, ACID kısaltmasının gerçekte ne anlama geldiğini görelim.
Atomiklik
Atomiklik, bir işlemi oluşturan tüm işlemlerin ya gerçekleştirildiği ya da hiçbirinin gerçekleştirilmediği anlamına gelir: “ya hep ya hiç”. Kulağa çok kolay geliyor, ancak sonra ayrıntılar ortaya çıkıyor.
İlk olarak, DBMS (yalnızca Firebird değil, neredeyse hepsi) 2 tür atomikliğe sahiptir: ifade düzeyinde atomiklik ve bir işlem içindeki ifade grubu düzeyinde atomiklik.
İfade düzeyinde atomiklik, UPDATET1 SETX=1 WHEREY=2 ifadesinin her zaman ya başarıyla yürütüldüğü ya da yürütülmediği anlamına gelir.
İfade grubu düzeyinde atomiklik farklı çalışır (işlemin ne zaman başlatıldığını ve onaylandığını işaretlemek için burada sözde kod kullanacağız):
İşlem 11'i başlat
INSERT ..100
INSERT ..200
INSERT ..300
11'i onayla
Şemada yaklaşık olarak şöyle görünecektir:

Bu, üç INSERT ifadesinin de başarıyla yürütüldüğü ve bunlar tarafından yapılan değişikliklerin 11 numaralı işlem onaylandığı anda onaylandığı anlamına gelir.
İşlemler söz konusu olduğunda atölye çalışmalarında sık sık sorduğum soru - INSERT INTO..300 bir istisna oluşturursa, 11 numaralı işlem için COMMIT ifadesi başarıyla yürütülecek mi:

İzleyicinin önemli bir kısmı her zaman COMMIT ifadesinin başarıyla yürütülmeyeceğini söyler! (İlginçtir ki, bu diğer bazı DBMS’lerde işlemin geri alınmasına neden olur!)
Ancak bu doğru değildir - sadece isql’i çalıştırın ve herhangi bir veritabanıyla bir deney yapın (isql, kullanıcı için “tahmin etmeden” basit ve doğrudan bir işlem uygulamasına sahiptir).
Mesele şu ki, ifade grubu düzeyinde atomiklik, işlemlerin onaylanmasıyla sağlanan, bir iş mantığı meselesidir. Uygulamanın geliştiricisi, üçüncü INSERT ifadesinde bir istisna durumunda işlemin onaylanıp onaylanmayacağına karar vermek zorundadır. İş mantığı sonucun onaylanmasını mümkün kılıyorsa, COMMIT ifadesi kolayca yürütülebilir.
Bu nedenle, ACID’deki atomiklik gereksinimi, DBMS’nin bir işlem içinde yürütülen bir ifade grubunun sonuçlarını onaylayabilmesi veya geri alabilmesi gerekliliğidir. Onaylama veya geri alma kararı, uygulamanız gereken iş mantığına bağlıdır.
Ve bir kez daha vurgulayalım - bir işlem için bir ifade grubunun atomikliği, sonuçlardan bağımsız olarak tüm grubun onaylanması veya geri alınması olasılığı anlamına gelse de (ve seçim iş mantığına bağlıdır), tek bir ifadenin atomikliği DBMS’nin uygulanması tarafından garanti edilir, yani tek bir ifadeyi (örneğin, UPDATE) “eksik” (atomik olmayan) yürütmek imkansızdır.
Tutarlılık
Tutarlılık, veritabanındaki verilerin hiçbir çelişki sunmadığı anlamına gelir. Elbette, burada spekülasyon için geniş bir alan görebiliriz çünkü “hiçbir çelişki sunmamak” ne anlama gelir?
Genellikle iki tutarlılık seviyesi ayırt edilir:
- Veritabanı seviyesi - tutarlılığın, verilerin Primary, Unique ve Foreign anahtarlar, Checks gibi veritabanı kısıtlamalarına uygunluğu anlamına geldiği seviye. Bu tutarlılık seviyesi, veritabanı kısıtlamalarının kısıtlamalara uymayan verilerin eklenmesine izin vermeyeceği gerçeğiyle sağlanır: örn. CHECK(x>0), ilgili alana negatif bir sayının eklenmesine izin vermez.
- İş mantığı seviyesi - tutarlılığın, uygulamanın geliştiricisi tarafından DBMS tarafından sunulan işlemler gibi araçların yardımıyla sağlandığı seviye.
İşlemler iş mantığı seviyesinde tutarlılığın sağlanmasına nasıl yardımcı olur? Oldukça basit - para transferi örneğini alırsak, geliştirici bir istisna durumunda tüm değişikliklerin geri alındığından emin olmalıdır ve bir işlem kullanmak ona bu konuda yardımcı olur.
İşlemi başlat
Hesap 1'deki para miktarını azalt…. Başarılı
Hesap 2'de artır… Başarısız
Geri al ---- bir istisna durumunda!
Başka bir deyişle, geliştirici, bir istisna durumunda verilerin geri alınacağı ve böylece veri tutarlılığının iş mantığı açısından korunacağı şekilde kod yazmalıdır.
Bu şekilde, ACID kısaltmasındaki tutarlılık gereksinimi, DBMS’nin işlem mekanizmasının yardımıyla veri tutarlılığını sürdürme olasılığına sahip olmasının gerekli olduğu anlamına gelir.
İzolasyon
İşlem izolasyonu gereksinimi, bir dizi işlemin sonucunun, hangi sırayla gerçekleştirildiklerinden bağımsız olarak garanti edilmesi ihtiyacından kaynaklanır.
Basitçe söylemek gerekirse, her işlem, eşzamanlı olarak aktif işlemlerden bağımsız olarak aynı sonuçla yürütülmelidir.
İşlem mekanizmasının iş mantığı seviyesinde tutarlılığı sağlaması gerektiği varsayılır, ancak aynı zamanda işlemleri, eşzamanlı işlemlerin yürütülmesi sürecinde ortaya çıkabilecek geçici onaylanmamış verilere karşı koruması da gerekir.
Pratikte şöyle görünür:

t2 anında başlayan 11 numaralı işlemi görüyoruz; bu işlem içinde t3-t5 anlarında tabloya bir ekleme yapılır. 11 numaralı işlem eklemeden hemen sonra onaylanmaz, t8 anına kadar aktif kalır.
Eşzamanlı olarak, 12 numaralı işlem başlatılır ve 11 numaralı işlemin kayıtlarının eklendiği tablo için SELECT ifadesini yürütür. İlk SELECT ifadesi t6 anında yürütülür; bu, ekleme işleminin zaten bittiği zamandır, ancak bu ifade boş bir sonuç döndürür çünkü 12 numaralı işlem diğer işlemlerden onaylanmamış verileri göremez.
11 numaralı işlem t8 anında onaylanır ve 12 numaralı işlem içindeki SELECT ifadesi t9 anında yürütülür. 100’e eşit bir sonuç döndürür çünkü 11 numaralı işlem içinde oluşturulan veriler artık onaylanmıştır (ve 12 numaralı işlemin izolasyon seviyesi read committed olduğu için, ancak bunu daha sonra konuşacağız).
Bu örnek, izolasyon gereksinimini açıklamak için oldukça yeterlidir - atomiklik ve tutarlılığın aksine, izolasyon, izolasyon seviyeleri adı verilen katı kurallar olarak uygulanır ve her işlemin, çalıştığı izolasyon seviyesini belirleyen bir parametresi olmalıdır.
Kalıcılık
Kalıcılık kavramı, geliştiricinin, commit edilmiş bir işlem içinde oluşturulan verilerin anında veritabanında görüneceğine ve (tabii ki onu silen veya değiştiren açık ifadeler olmadıkça) ne olursa olsun veritabanından kaybolmayacağına tamamen güvenmesini sağlar.
Gördüğünüz gibi, kalıcılık gereksinimi sadece sağduyudur - verileri aniden kaybolabilen bir sistemi kullanmayı kimse kabul etmez.
ACID: özet
ACID, işlemlerin nasıl çalışması gerektiğine dair gereksinimler anlamına gelir:
- Atomiklik
- İfadeler her zaman atomiktir
- İfade grupları, işlemler yardımıyla atomik hale getirilebilir
- Tutarlılık
- İki tutarlılık seviyesi: veritabanı kısıtlamaları ve iş mantığı
- İzolasyon
- İşlemler mekanizması tarafından, kendileri için belirlenen izolasyon seviyeleri ile sağlanır
- Kalıcılık
- Commit edilen tüm veriler kalıcı hale gelir
Gördüğünüz gibi, her şey oldukça mantıklı. Pratikte, ana zorluk izolasyon seviyelerinden kaynaklanır, bu yüzden nasıl çalıştıklarını ayrıntılı olarak görelim.
Bir işlemin izolasyon seviyesi, bu işlemin hangi commit edilmiş verileri görebileceğini tanımlar.
Geleneksel olarak standart olarak adlandırılan izolasyon seviyeleri vardır. Bunlar ANSI SQL standardında (çeşitli revizyonlarda) açıklanmıştır. Bildiğim kadarıyla, bunların standartta açıklandığı şekilde tam olarak uygulandığı tek bir DBMS yoktur, ancak kimse bu konuda endişelenmez çünkü belirli DBMS’lerdeki gerçek işlem mekanizmaları, iş mantığını uygulamak için gerekli tüm seçeneklere sahiptir.
İzolasyon seviyelerinin klasik tanımını “A Critique of ANSI SQL Isolation Levels” adresinde bulabilirsiniz.
Bu makaleyi okuyanlar için, klasik izolasyon seviyelerini Firebird’deki benzerleriyle karşılaştıran tablo aşağıdadır. Elbette, karşılık doğrudan değildir çünkü Firebird’deki izolasyon seviyeleri, diğer DBMS’lerde olduğu gibi, ANSI SQL tanımlarına %100 uymaz, ancak onlara çok benzer.
| ANSI İzolasyon Seviyeleri | Firebird’de İzolasyon Seviyesi |
| Read Uncommitted | yok |
| Read Committed | Read Committed |
| Repeatable Read | Snapshot |
| Serializable | Snapshot table stability |
Diğer DBMS’ler gibi Firebird’ün de izolasyon uygulamasında kendine özgü özellikleri vardır. Şimdi, standartla ne kadar uyumlu olduklarına değil, Firebird’de izolasyon seviyelerinin nasıl çalıştığına odaklanacağız.
Snapshot izolasyon seviyesi
Snapshot izolasyon seviyesi, InterBase’ün orijinal kodundaki ilk seviyeydi ve Firebird çekirdek API’si ve yardımcı programları (örneğin, isql.exe) için varsayılan olmaya devam etmektedir. Bu, anlaşılması en kolay olanı olmasının nedeni olabilir.
Snapshot, işlemi başladığı andan itibaren yapılan tüm değişikliklerden izole eder.
Aşağıdaki işlem şemasına bakalım: snapshot izolasyon seviyesiyle başlatılan işlem #10’u gösterir. Bu işlem içinde, bu örnekte kaydı olmayan T1 tablosu için birkaç SELECT ifadesi yapılır.

İşlem #10’un başlangıcından sonra başlayan eşzamanlı işlem #15, T1 tablosuna veri ekler ve bu işlem t9 anında COMMIT ifadesiyle sona erer, yani veriler bu anda veritabanına commit edilir ve diğer işlemlerin ifadeleri için kullanılabilir.
Ancak, işlem #10’da t10 anında (yani işlem #15 commit edildikten sonra) yürütülen ifade, eklenen verileri görmez çünkü snapshot izolasyon seviyesi, yalnızca işlem #10’un BAŞLANGICINDAN ÖNCE eklenen veya değiştirilen commit edilmiş verileri görmesine izin verir.
Böylece, Snapshot izolasyon seviyesi, veritabanıyla işlemin başladığı anda donmuş gibi çalışmanıza olanak tanır. Genellikle hızla değişen verilere dayalı karmaşık raporlar oluşturmak gerekir: raporun ilk bölümünün bazı verilere ve son bölümünün farklı verilere dayanması durumundan kaçınmak için snapshot kullanılır.
Ancak, bu harika özelliğin bir bedeli vardır - daha sonra izolasyonun Firebird’de nasıl uygulandığını incelediğimizde, snapshot izolasyon seviyesiyle çok uzun işlemler başlatmanın aşırı kayıt sürümlerine ve daha düşük performansa yol açtığını göreceksiniz.
Read Committed izolasyon seviyesi
Read committed izolasyon seviyesine sahip bir işlem, aktifken commit edilen diğer işlemlerin commit edilmiş verilerini görebilir (snapshot seviyesinde olduğu gibi yalnızca işlemin başladığı andan önce commit edilen verileri görebildiğiniz durumun aksine).
Read committed izolasyon seviyesinin nasıl çalıştığını aşağıdaki şema ile gösterelim:

Bu, öncekine neredeyse aynı bir örneği gösterir: biri düzenli olarak T1 tablosundan veri okuyan, diğeri veri ekleyen ve commit eden iki eşzamanlı işlem.
Snapshot izolasyon seviyesindeki durumun aksine, bu örnekte işlem #10, işlem #15 tarafından eklenen ve commit edilen verileri görür.
Bu örnek bize read committed izolasyon seviyesinin etkisi hakkında bir fikir verir: bu izolasyon seviyesine sahip bir işlem içindeki ifadeler, ilgili ifadenin yürütüldüğü andan önce commit edilen verileri görebilir.
Sonraki şema, verileri değiştiren iki eşzamanlı işlem #11 ve #18’in olduğu bir örneği gösterir.
İşlem #11’in, veri okuyan işlem #14’ün başlangıcından önce başladığını, işlem #18’in ise ondan sonra başladığını, ancak bunun sonucu etkilemediğini unutmayın: veriler commit edilmişse, read committed izolasyon seviyesine sahip eşzamanlı işlem tarafından görülebilir.

Bu olasılık, read committed izolasyon seviyesini, en son veritabanı durumunu göstermek için düzenli olarak yürütülen SQL ifadeleri (örneğin, en son siparişleri göstermek için) için doğal bir seçim haline getirir.
Çöp toplama bölümü, Firebird’ün 4. sürümüne kadar read-only değiştiricili read committed işlemlerin, önceden commit edilmiş olarak başlatıldıkları için “sonsuz” okuma işlemleri için en iyi seçim olduğunu gösterecektir.
Snapshot table stability izolasyon seviyesi
Standart Serializable izolasyon modunun karşılığı olan snapshot table stability izolasyon modu hakkındaki hikayeyi ya çok kısa ya da oldukça uzun ve ayrıntılı yapmak mümkündür.
Hikayenin kısa versiyonu şudur: bu seviye, tabloyu (tablo, işlem parametrelerinde açıkça belirtilmelidir) yazma ve okuma için kilitlemekle birlikte snapshot seviyesine tamamen benzer. Bu, belirtilen tabloyu tamamen işgal edecek bir işlem başlatmanın mümkün olduğu ve diğer işlemlerin erişim hataları alacağı anlamına gelir.
Başka bir deyişle, snapshot table stability izolasyon seviyesine sahip bir işlem, belirtilen tabloya yapılan tüm sorguları aslında bir kuyruğa koyacaktır. Aslında, yalnızca normal işlemlerdeki okumalar sırası dışında (her zamanki gibi) yapılacak, diğer tüm modlar bir kuyruk oluşturacaktır (etkileşime bağlıdır, elbette).
Dikkatli uygulanmazsa, kilitlere ve veritabanıyla çalışmanın imkansızlığına neden olabilir, bu yüzden Firebird veritabanı uygulama geliştiricileri bu izolasyon seviyesini kullanmaktan korkabilir.
Ancak, doğru uygulanırsa, Serializable izolasyon seviyesi, kuyrukları kolayca oluşturmayı ve veritabanı kayıtlarında sıralı değişiklikler yapmayı mümkün kılar; bu, sayaçlar, sıralı belge numaraları ve benzeri nesneleri uygulamak için çok yararlı olabilir.
Snapshot table stability izolasyon seviyesine sahip bir işlem yardımıyla bir kuyruğun nasıl oluşturulacağını doğru bir şekilde açıklamak için, bir işlem parametresine daha bakmamız gerekecek: wait/nowait - ve sonra kuyruk örneğine geri döneceğiz.
Daha önce, verilerin bir işlem içinde değiştirildiği ve başka bir işlem içinde okunduğu böyle bir etkileşim yolunu inceledik.
Ancak, pratikte farklı işlemlerin aynı verileri değiştirmeye çalıştığı durumlar sıklıkla olur ve veritabanında yalnızca bir sonuç kaydedildiğinden, eşzamanlı işlem bir çakışma mesajı alacaktır - aslında, zaten değiştirilmiş verileri değiştirmeye çalışan belirli ifadenin yürütülmesini kesintiye uğratan (ve iptal eden) bir istisna.
Wait seçeneği, bir işlemin güncelleme çakışmasına nasıl tepki vermesi gerektiğini tanımlar. Bu seçeneği yapılandırmanın üç yolu vardır:
- Wait (parametresiz) = eşzamanlı işlem bitene kadar bekle
- Wait Timeout N sec = eşzamanlı işlem bitene kadar bekle, ancak N saniyeden fazla değil
- Nowait - eşzamanlı işlemin bitmesini bekleme
Wait seçeneğinin burada sözde kodda belirtildiğini, API’de ve belirli bileşenlerde adların farklı olabileceğini, ancak anlamın aynı kaldığını unutmayın.
Wait seçeneğinin çeşitli varyantlarıyla güncelleme çakışmaları durumunda ne olduğunu ayrıntılı olarak görelim.
Wait
Şimdi, aynı T1 tablosundaki aynı kaydı değiştirmesi gereken UPDATE ifadesinin yürütüldüğü iki eşzamanlı aktif işlemi (#11 ve #14) hayal edelim.
İşlem #14, wait seçeneğiyle çalışır (örnekleri yeniden oluşturmak için isql kullanıyorsanız, wait varsayılan olarak ayarlanır).
İşlem #11’deki UPDATE ifadesi t3 anında başlar ve t5 anında biter, ancak işlem henüz commit edilmemiştir - yani COMMIT ifadesi t6 anına kadar yoktur.
Aşağıdaki şema bu durumu gösterir:

UPDATE ifadesi ayrıca işlem #14’te de yürütülür ve aynı tablodaki aynı kaydı güncellemeye çalışır, ancak daha sonra başlar - yaklaşık t4 anında.
İşlem #11’deki güncellemeyle bir güncelleme çakışması olduğundan ve işlem #14’te wait belirtildiğinden, UPDATE ifadesi çakışan işlem #11 bitene kadar bekleyecektir.
İşlem #11 yeterince uzun sürerse, işlem #14’teki UPDATE ifadesi, bu ifadenin yürütülmesini izleyen kullanıcının bakış açısından donmuş gibi görünecektir.
Bu durumu iki isql.exe yardımıyla yeniden oluşturursanız, sonraki resim ikinci işlemin (daha doğrusu eşzamanlı UPDATE ifadesinin daha sonra başladığı işlemin - örneğimizde işlem #14) ilk işlem bitene kadar beklediği anı gösterir (örneğimizde işlem #11).

İşlem #11’de COMMIT ifadesi yürütüldükten sonra, onu bekleyen işlem #14 anında bilgilendirilecek ve çakışan güncelleme bir istisna ile sona erecektir.
Aşağıda böyle bir hata mesajının bir örneğini görebilirsiniz (eşzamanlı işlemin numarası örneğimizle çakışmaz çünkü işlem numaraları her veritabanında baştan başlar ve yalnızca yedekleme/geri yüklemeden sonra sıfırlanarak artar):
SQL> update T1 set i1 = 2 where i1=1;
Statement failed, SQLSTATE = 40001
deadlock
-update conflicts with concurrent update
-concurrent transaction number is 19
SQL>
Hata mesajındaki “deadlock” kelimesine dikkat edin - şimdi aslında klasik tanımına göre bir deadlock yoktur. Bunun yerine, bir güncelleme çakışması vardır, ancak Firebird geliştiricileri hata mesajını değiştirmez çünkü 35 yıldan fazla bir süredir kullanılmaktadır. Gerçek “klasik” deadlock ile daha sonra ilgileneceğiz.
Böylece, eşzamanlı UPDATE ifadesine sahip işlemin COMMIT ifadesiyle sona erdiği durumu inceledik. Şimdi benzer bir durumu ele alalım, ancak bu sefer geri alındığında - aşağıdaki şemada görebilirsiniz:

Durum öncekiyle tamamen benzer - iki UPDATE ifadesi aynı kaydı güncellemeye çalışır, ancak eşzamanlı işlem #20 bu sefer geri alınır ve sonuç olarak işlem #15 içindeki değişiklikler hatasız olarak veritabanına kaydedilir.
Böylece, bekleme seçeneği, güncelleme çakışmalarını, çakışan işlemlerin, kendileriyle çakışan işlemin ROLLBACK ifadesiyle sona ermesini son ana kadar umarak bir kuyrukta sonsuza dek bekleyeceği şekilde düzenlemeyi mümkün kılar.
Bu taktik her zaman mantıklı mıdır? Elbette, iş mantığının uygulanmasına bağlıdır, ancak Firebird, bekleme seçeneği yardımıyla güncelleme çakışmalarını çözmek için başka seçenekler de sunar.
Zaman Aşımlı Bekleme
Her şeyden önce, bekleme süresini sınırlamak iyi bir fikir olabilir - bir çakışma durumunda sonsuza dek beklemek yerine, bekleme seçeneği için bir zaman aşımı belirterek bekleme süresini sınırlayabilirsiniz.
isql.exe içinde böyle bir parametre aşağıdaki ifade yardımıyla belirtilir:
SET TRANSACTION WAIT LOCK TIMEOUT N;
Burada N, eşzamanlı işlemin çakışmanın çözülmesi için bekleyeceği süredir (saniye cinsinden).
İşlem kontrol ifadeleri hakkında daha fazla ayrıntıyı Firebird Language Reference’da bulabilirsiniz. Belirli sürücülerde veya erişim bileşenlerinde zaman aşımını belirtmenin farklı yolları olabileceğini unutmayın (genellikle API parametresi yardımıyla).
Aşağıdaki resimde isql içinde bir örnek görebilirsiniz:

İşlem grafikleri yardımıyla, bekleme seçeneği için zaman aşımı belirtirseniz işlemlerin nasıl etkileşime girdiğini inceleyelim.
Yani durum aynı - T1 tablosundaki aynı kaydı güncellemeye çalışan UPDATE ifadesinin yürütüldüğü iki eşzamanlı işlem #11 ve #14.

Ancak bu durumda, #14 işlemi içindeki ifade ya #11 işlemi sona erene kadar ya da belirtilen zaman aşımı (3 saniye) dolana kadar bekler - hangisi önce olursa.
Bu örnekte zaman aşımı daha erken dolar, ifade bir istisna ile sona erer:
SQL> update T1 set i1=6 where i1=1;
Statement failed, SQLSTATE = 40001
lock time-out on wait transaction
-deadlock
-update conflicts with concurrent update
-concurrent transaction number is 40
Hata mesajında yine “deadlock” olduğuna dikkat edin, ancak bu yine de “gerçek” bir deadlock değildir.
Buna karşılık, durum bekleme seçeneğindekine benzer, ancak zaman aşımı ile sınırlıdır - belirtilen zaman aşımı, eşzamanlı işlemin sona ermesinden daha erken dolarsa.
Zaman aşımlı bekleme seçeneğini belirtmek, tüm yazma işlemlerinin oldukça kısa olduğundan eminseniz (örneğin, 1-2 saniyeden uzun olmadığını) iş mantığını uygulamak için iyi bir çözüm olabilir.
Nowait
Nowait’in resmi bakış açısından ne olduğunu açıklamak çok kolaydır - sıfır zaman aşımlı beklemektir. İşlemlerde nowait belirtirseniz, çakışan güncellemeler anında bir istisna oluşturur.

Bu durumda, yine eşzamanlı UPDATE ifadelerinin yürütüldüğü eşzamanlı işlemler #11 ve #14 (nowait) var. Nowait seçeneğine sahip işlem içindeki ifade, eşzamanlı bir güncelleme gördüğünde beklemez, ancak güncelleme anında aşağıdaki istisnayı hemen oluşturur (yalnızca işlem numarası farklıdır):
SQL> update T1 set i1=5 where i1=1;
Statement failed, SQLSTATE = 40001
lock conflict on no wait transaction
-deadlock
-update conflicts with concurrent update
-concurrent transaction number is 50
SQL>
İki isql aracıyla bir örnekte şöyle görünür:

Nowait işleminin, eşzamanlı UPDATE ifadesine sahip işlemin ne zaman ve nasıl sona erdiğini umursamadığını unutmayın - COMMIT veya ROLLBACK ifadesi olup olmayacağı, istisna yine de oluşturulur.
İş mantığı açısından, eşzamanlı güncellemenin mevcut ifadenin eylemlerini kesinlikle iptal etmesi gerektiğinden eminseniz nowait işlemi uygun olabilir.
Birçok Firebird sürücüsü varsayılan değer olarak nowait seçeneğini kullanır ve birçok geliştirici, güncelleme çakışmalarını çözmek için daha az katı bir düzey ayarlamanın mümkün olduğunu bilmediği sürece (örneğin, wait lock timeout 1), uygulamaları (ve bazen kullanıcıları da) çakışmalardan kaynaklanan gereksiz hatalardan muzdariptir.
Güncelleme çakışmalarıyla ilgili her istisnada “deadlock” anahtar sözcüğü bulunduğundan, birçok uygulama geliştiricisi bunun gerçek bir deadlock olduğundan emindir (hatta bazıları bu hatada belirli bir Dead’in rolü olduğunu düşünür).
Aynı zamanda, firebird.conf yapılandırma dosyasına bakarsak, orada DeadlockTimeout parametresini görürüz (varsayılan olarak 10 saniyedir) ve fb_lock_print yardımcı programının çıktı başlığına bakarsak, “Deadlock scans” parametresini de görürüz.
Mesele şu ki, Firebird’de “gerçek deadlock” mümkündür ve güncelleme çakışmalarıyla ilgili tüm istisnalarda görünen “deadlock” anahtar sözcüğünün bununla doğrudan bir bağlantısı yoktur. Neyse ki, gerçek deadlock oldukça nadiren meydana gelir.
Bu “gerçek deadlock"ın ne olduğunu görelim. Bunu yapmak için aşağıdaki işlem etkileşim grafiğine bakalım:

UPDATE ifadesinin yürütüldüğü bekleme seçeneğine sahip iki eşzamanlı işlemimiz var. Basit bir güncelleme çakışmasının aksine, burada birbirine bağımlı bir güncelleme çakışması görebiliriz:
- İşlem #11, anahtar = 20 olan kaydı günceller ve işlem #12, anahtar = 10 olan kaydı günceller;
- Bundan sonra, işlem #11, anahtar = 10 olan kaydı günceller ve işlem #12, anahtar = 20 olan kaydı günceller;
Sonuç olarak, her işlemin diğerinin sona ermesini beklemek zorunda olduğu ve her ikisinin de sonsuza dek bekleyebileceği bir durumla karşı karşıyayız çünkü her ikisi için de bekleme seçeneği belirtilmiştir. Elbette, sunucu bunun olmasına izin veremez, bu nedenle işlemlerden biri, varsayılan olarak 10 saniye değerine ayarlanmış DeadlockTimeout parametresinde belirtilen zaman aşımından sonra geri alınmaya zorlanacaktır.
Bu durumu iki isql yardımıyla yeniden üretebiliriz:

İkinci işlem başlatıldıktan sonra, gerçek deadlock durumu ortaya çıkar. Bundan emin olmak için sunucu, Deadlock scan adı verilen bir prosedür başlatır - varsayılan olarak 10 saniyeye eşit olan DeadlockTimeout’a eşit aralıklarla başlatılır.
İstemcinin (bu durumda isql) normal bir güncelleme çakışması mesajı aldığını, ancak işlem bekleme seçeneğiyle başlatılmış olsa bile bunun 10 saniye içinde başlatıldığını unutmayın.
Sunucu, iki işlemin birbirine bağımlı kilidini algıladıktan sonra, dahili deadlock sayacını da artıracaktır (bunu fb_lock_print çıktısında görebilirsiniz).
Snapshot Table Stability’nin Pratik Kullanımı
Artık çakışan UPDATE ifadeleriyle işlemlerin nasıl çalıştığını bildiğimize göre, Snapshot Table Stability yalıtım düzeyine geri dönebilir ve bunun için pratik bir kullanım bulabiliriz.
Yani, bu yalıtım düzeyi belirtildiğinde, tablo yazma ve hatta okuma için kilitlenir.
İşlem parametrelerinde tablo açıkça belirtilmezse, bu işlem içindeki ifadelerin eriştiği tüm tabloların kilitlendiğini ve bunun bir tabloya ilk erişim sırasında gerçekleştiğini unutmayın. Görünüşe göre, bu yalıtım düzeyi dikkatli kullanılmazsa, çok sayıda güncelleme çakışmasına kolayca yol açacaktır.
Reserving TableNN yan tümcesi, işlemin başlangıcında belirli bir tablonun (veya birkaç tablonun) kilitlenmesini belirtmenize olanak tanır (ayrıca rezervasyon modunu da belirtmek mümkündür).
Bu harika özellik, bekleme seçeneğiyle birlikte, belirli bir tabloyu değiştirmek için çok etkili bir sıralı kuyruk uygulamanıza olanak tanır.
Pratikte şöyle görünür - belirli bir tabloya kuyruk oluşturması gereken istemciler, bu tabloyu belirten SNAPSHOT TABLE STABILITY işlemini başlatır ve ardından bu işlem içinde bir işlem gerçekleştirmeye çalışır ve hemen sona erdirir.
Örneğin, CREATE TABLE Table1(i1 integer not null) türünde yalnızca bir kaydı olan bir tabloda sıralı olarak artan bir sayaç oluşturmak istiyoruz, ancak bir nedenden dolayı bir jeneratör kullanamıyoruz.
Sözde kod yaklaşık olarak şöyle görünür:
set transaction snapshot table stability reserving TABLE1 for protected write
UPDATE Table1 Set i1 = i1+1;
SELECT i1 from Table1;
COMMIT;
Bu kodu Snapshot Table Stability (Table1) yalıtım düzeyiyle değil de daha düşük bir yalıtım düzeyiyle çalıştırırsak, işlemin başlangıcı ile UPDATE ifadesinden önce eşzamanlı bir UPDATE ifadesinin araya girmesi mümkün olacaktır. Sonuç olarak, ya hemen bir güncelleme istisnası (nowait) alırız ya da ifade eşzamanlı olanın sonuna kadar donar (wait) ya da zaman aşımına kadar (wait interval) - başka bir deyişle, çakışma bir şekilde ifade düzeyinde çözülür.
Snapshot table stability yalıtım düzeyiyle, tablo işlemin başlangıcında ayrıldığı için buna karşı korunuruz - ya tamamen bizimdir ya da tamamen bizim değildir. Çakışmaları çözmek için bekleme seçeneğini belirtirsek, paralel bağlantılar herhangi bir hatayı işlemeden otomatik olarak bir kuyruk oluşturur.

Elbette, bu yaklaşım yalnızca kısa işlemlere uygulanabilir (örneğimizde olduğu gibi).
Pratikte, Snapshot table stability yalıtım düzeyi, özel modda kuyruklar oluşturmak ve karmaşık mantığı yeniden hesaplamak için kullanılır (nispeten küçük tablolarda veya başka kullanıcı olmadığında).
Motorun içinde Firebird, dizinler oluşturmak için Snapshot Table Stability yalıtım düzeyini kullanır - yani ALTER INDEX indexname ACTIVE; ifadesini yürüttüğünüzde, Firebird dizinin oluşturulduğu tabloyu tamamen işgal eder.
Sırada Ne Var?
Bu makale yalnızca Firebird işlemleri kavramlarına bir giriş sağlar. Firebird’de işlemlerin nasıl çalıştığını tam olarak anlamak için, çok kuşaklı mimariyi (kayıt sürümleri ve çöp toplama kavramları) dikkate almak, işlem işaretleyicilerini (Oldest Interesting, Oldest Active, Oldest Snapshot, next) ve diğer şeyleri dikkate almak gerekir.
Makale, ilk olarak 2013 yılında Firebird Tour seminerleri sırasında tanıtılan “All About Transactions” seminer/atölye çalışmasının materyallerine ve IBSurgeon’un " Firebird Transaction in details” eğitimine dayanmaktadır.
İletişim
[email protected] Herhangi bir soru veya öneriniz için lütfen bizimle iletişime geçmekten çekinmeyin: [email protected]