Aslında çok basit bir konu olduğunu düşünebilirsiniz. Elimizde bir ön ek var. Sonuna belli bir sayaç ekleyip sıralı bir şekilde kodlar üretmek istiyoruz. Konu basit ama yapı büyüdükçe aynı kodun tekrarlı üretilmesi problemi ile karşılaşabiliyoruz.
Öncelikle üretmek istediğiniz kodun belli bir formatı yoksa, sadece eşsiz bir veri olmasını istiyorsanız, uzunluk sorununuz da yoksa, bir de tipi numerik olması gerekmiyorsa (ne kadar çok kısıt varmış 😊) GUID değer üretmek de bir çözüm olarak değerlendirilebilir.
Belli bir düzende üretilmesi istenen kodlarda genellikle sıralı bir şekilde artan bir sayaç kullanırız. Belirli bir ön ekin (örneğin “INFO”) sonuna istenilen uzunluğa uyacak şekilde 1’er 1’er artan bir değer ekleyip kod üretebiliriz. Bunun için en genel haliyle şöyle bir tablo yeterli olur.
Bu tablodaki tanıma göre yeni bir kod üretebilmek için tablodaki son sayaç (Counter) değerini okuyup, 1 arttırıp tekrar tabloyu bu yeni değer ile güncelleriz.
Ama bu işlemi yaparken aynı sayacı kullanan bir başka işlemin olmaması gerekir. Aksi halde tablodaki sayacı ilk okuyan işlem güncelleyene kadar ikinci işlem de yine aynı sayaç değerini okur. Bunun sonucunda da iki işlem de aynı değeri üretmiş olur. İşte sorun tam da bu noktada ortaya çıkıyor. Bu sorunu çözmek için farklı yöntemler uygulanabilir. Çalıştığım projelerde bu sorun için deneyimlediğim çözümleri bu yazıda toparlamak istedim.
Lock (Object)
Aynı kodun üretilmesini engellemek için öncelikle bu işlemi gerçekleştiren fonksiyonun kilitlenmesi düşünülebiliriz. Bunun için de Lock anahtar sözcüğünü kullanılabiliriz. Aşağıdaki ekran görüntüsündeki gibi kod üreten blok Lock içerisine alınır. Böylece aynı anda 2 farklı işlemin bu bloğu çalıştırması engellenir.
Bu çözüm tek sunucu ile çalışılan bir ortamda başarılı sonuç üretir. Geliştirme ortamlarında genelde tek sunucu ile ilerlendiği için aynı kodun üretilmesi sorunu ile muhtemelen karşılaşmazsınız. Ancak birden fazla sunucu ile devreye girdiğinde bu kez farklı sunucuların aynı değeri üretme sorunu karşımıza çıkacaktır.
With (Updlock)
Sunucuların ayrışması ile kod üreten bloğun tekilleşmesi sorun olduğuna göre, sunucuların ortak kullandığı başka bir yerde bu sorunun çözülmesi gerekecektir. Burada ilk olarak aklımıza büyük ihtimalle veri tabanı geliyordur.
Veri tabanından güncellenmek üzere sayaç bilgisinin çekilmesi esnasında bu kez de veri tabanı üzerinde bir kilit mekanizması kullanabiliriz. Ancak bilgi getirme işlemlerinde veri tabanı motorları ilgili kayıt üzerine kilit koymadığından bunun manuel yapılması gerekmektedir. MSSQL için UPDLOCK anahtar sözcüğü kullanılabilir.
Bu şekilde çekilen kayıtlara kilit konulduğu için farklı sunuculardan aynı kayda atılan istekler, kilit kaldırılana kadar bekletilir. Böylece aynı kodun üretilmesi engellenmiş olur. Bu en yaygın kullanılan çözümlerden biridir. Ancak işlemlerin birbirlerini engellemesi sürelerin artmasına ve bir yük oluşmasına sebep olacaktır. Çünkü bir veri tabanı işlemi (transaction) içerisinde konulan kilit, işlem tamamlanana kadar kaldırılamıyor. Bu da işlem bitene kadar o kaydın kilitli kalmasına sebep oluyor.
Identity Specification
Peki, yine sunucular arası ortak bir alan olan veri tabanını kullansak, her seferinde sayacı 1 attırsak ve işlemlerin de birbirlerini beklememelerini sağlasak nasıl olur?
Aslında farklı bir işlem uygulamıyoruz. Yine veri tabanında bir kayıt tutuyoruz ve her seferinde bir arttırıyoruz. Ama bunu biz değil veri tabanı motoru Identity Specification ile yapıyor. Evet aslında bildiğimiz Primary Key’in sayaç olarak kullanılmasından bahsediyorum. Değeri veri tabanı yönetiyor, tekrarlı değer üretmiyor, üstelik veri tabanı yönettiği için UPDLOCK gibi bir yapı kullanmaya da gerek kalmıyor ve böylelikle işlemlerin birbirlerini engellemeleri sorunu da ortadan kalkıyor.
Ancak bunun için tablo yapısında bir düzenleme gerekiyor. Sürekli güncelleme alan tablomuzun artık kayıt ekleme yapısına geçirilmesi gerekiyor.
Bu yapı sayesinde yeni bir kod üretmek istediğimizde tabloya ilgili ön ek için bir kayıt ekleyip, eklenilen kaydın Id’si sayaç olarak kullanıyoruz. Evet, tablo gereksiz yere şişiyor, çünkü amacımız sadece bir sayacı yönetmek. Ama bunun için de tablonun düzenli olarak temizlenmesi planlanabilir, çünkü veri tabanı her zaman en son kaydedilen Id üzerinden yeni kayıtları üretir. O yüzden bir ön ek için son üretilen kayıt dışındaki kayıtların hiçbir değeri yoktur.
En sorunsuz çözüm bu gibi duruyor. Veri tabanına kayıt at, eşsiz değerini üret, işlemleri engelleme, herkes mutlu. Peki farklı ön ekler için kod üretilmek istendiğinde ne yapacağız? Id kolonumuz tek ama ön ekler farklı. Bu durumda kodlarımızda boşluklar oluşacaktır. Yani siz aşağıdaki gibi kodların üretilmesini beklerken;
- “INFO” ön eki için : INFO001, INFO002, INFO003
- “WARN” ön eki için : WARN001, WARN002
- “ERR” ön eki için : ERR001, ERR002
şu şekilde kodlar ile karşılacaksınız;
- “INFO” ön eki için : INFO001, INFO003, INFO 007
- “WARN” ön eki için : WARN002, WARN004
- “ERR” ön eki için : ERR005, ERR006
Bu eğer sizin için problem değilse bu yöntem gayet kullanışlı olacaktır. Ama bizim için bu bir problem . O yüzden alternatif son çözüme başvurduk.
REDIS.StringIncrement()
Sunucular arası ortak kullanılan parçalardan bir diğeri de CACHE sunucusudur. Bizim çözümümüz için REDIS uygulamasının kullanıldığı bir cache sunucusundan faydalanacağız ama alternatiflerinin de benzer kabiliyetleri olabilir.
Biz burada REDIS’in özel bir fonksiyonunu olan StringIncrement fonksiyonunu kullanarak bir çözüm uygulayacağız. Birden fazla sunucunun erişim sağladığı REDIS sunucusu, bu fonksiyon ile tekil bir değer üretmeyi sağlıyor. Farklı sunuculardan aynı anda gelen talepleri dahi bir sıraya sokup tekrarlı değer üretilmesini engelliyor. Biz de sayaç değerini REDIS’e, uygulama ayağa kalkarken yazıp, bu fonksiyon ile hep bir sonraki değeri üretiyor, eşsiz üretilen kodları uygulamanın kullanımına sunuyoruz. Bunun için de ufak bir iki düzenleme yapmak yeterli oluyor.
İlk düzenleme veri tabanında. Sayaç arttırma işlemi ayrı bir yerde gerçekleştirildiği için veri tabanında sürekli bir güncelleme yapma ihtiyacı kalmıyor (zaten güncelleme işlemi yine UPDLOCK ile yapıldığı için işlemlerin birbirini engellemesinden kaçamıyor olacaktık). Ama uygulamanın çökmesi veya REDIS’in temizlenmesi senaryoları için son değeri bir yerde tutuyor olmamız gerekiyor. O yüzden tablomuzu aşağıdaki versiyona (yani aslında ilk versiyona) güncelliyoruz. Bu kez ufak bir ekleme yapıyoruz. Ön ek ve sayaç kolonlarının aynı anda eşsiz olmasını sağlamak için gerekli eşsiz indeksi ekliyoruz. Bunun temel amacı yapıyı korumak. Bir sebepten dolayı aynı değerin tekrar üretilmesi durumu oluşursa, uygulamanın hata üreterek bizi uyarmasını sağlıyoruz.
NOT: Tabi bu şekilde kurulan bir yapının tekrarlı veri üretmesi muhtemelen REDIS kaynaklı olacaktır. Olası senaryolardan bir tanesi REDIS üzerindeki master’ın erişilemeyen bir duruma dönmesi sebebi ile slave’lerden birinin görevi devralması olabilir. Bu durumda gerekli önlemler alınmamışsa slave’in değeri eski kalmış olabilir ve bize daha önce ürettiği bir değeri tekrar üretebilir. Bu sorunun çözümü aslında hatalı sayacın REDIS’ten silinmesi ve bir sonraki sayaç arttırma işleminin güncel değeri REDIS’e tekrar yazması ile çözülebilir. REDIS çökmesi durumlarının kontrol altına alındığı daha profesyonel çözümler de bu yapıyı koruyan başka bir çözüm olarak düşünülebilir.
Tablomuzda gerekli değişikliği yaptıktan sonra sırada kodumuzu yenilemek var. Öncelikle lockObject yapısından kurtuluyoruz. Çünkü artık ihtiyacımız yok. Sonrasında da sayacın REDIS’ten alınmasını ve veri tabanına kaydedilmesini sağlıyoruz (REDIS’de yoksa ekle gibi beklenmedik durumların üstesinden gelmek için gerekli olan detaylara girmiyorum 😊)
Sadece önceki sayaç üreten kodları yeni hali ile güncelleyip, tabloya güncelleme yerine ekleme işleminin gerçekleştirmesini sağlayarak implementasyonu tamamlamış olduk.
Çiçek gibi oldu, derken aklıma daha önce sadece veri tabanı işlemi ile gerçekleştirilen sayaç yönetiminin artık veri tabanı ve REDIS işlemi ile üretildiği geldi. Sanki sayaç üretme işlemini uzattık gibi. Evet aslında süresi REDIS bağlantısından dolayı (ki göz ardı edilebilecek kadar kısa bir süre) uzamış olabilir. Ama burada asıl önemli olan nokta, büyük resimde işlemlerin birbirlerini engellemeden devam edebilmeleridir. Çünkü engelledikleri senaryoda engellenen işlemin bekleme süresi sayaç arttırana kadar değil, sayacı kullanan işlem tamamlanana kadar uzayacaktır.