JPA/Hibernate Optimistic ve Pessimistic Locking?
Hangi yarış? Entity nesneleri üzerinde yapılacak eş-zamanlı işlemlerden hangisinin geçerli olacağından bahsediyorum.
Entity nesneleri üzerinden eşzamanlı (concurrent) işlemler yapılırken, entity nesnesi üzerinde yapılan veri değişikliğinin tutarlılığına dikkat edilmelidir.
Elimizde Kitap adında bir entity nesnesi var olduğunu düşünelim ve aynı anda 2 kullanıcının bu Kitap entity nesnesi üzerinde veri değişikliği yapıyor olduğunu hayal edelim. Yukarıdaki temsili resme göre Kitap entity nesnesinin bir fiyat alanı olsun ve bu nesnenin veritabanı tarafında kitap fiyatı 20 TL olarak kayıtlı olsun. (Bkz. kesikli çizgi öncesi). Ardından EntityManager#find yordamıyla 200L id’li nesne elde edilsin ve önce fiyatına 10 TL (solda) sonra da 5 TL (sağda) eklenmek isteniyor olsun. Peki bu yarışın kazananı kim olacak? Kitap fiyatı 30 TL mi olacak? Yoksa 25 TL mi? Yoksa yoksa 20+10+5=35 TL mi?
Bu yarışın sonucu belli değil. Çünkü fiyat değerine, son Transaction commit işlemi karar verecek. Yani sonuç 25 de olabilir, 30 da. Bu bir problem mi? Sizin uygulamanız için bu bir problem olabilir de olmayabilir de. Biz bu durumu sizin için bir problem olması açısından değerlendireceğiz.
Transaction Kilitleme (Locking) mekanizması
Locking mekanizması entity nesneleri üzerinde yapılan okuma ve yazma işlemlerine farklı kilit türlerinin konmasını tanımlayan mekanizmadır. Bu kilitleme türleri EntityManager nesnesinin çeşitli yordamlarıyla sağlanır. Ama bu yordamları incelemeden önce JPA standardının sunduğu kilit türlerini irdeleyelim.
READ (JPA 1.0) || OPTIMISTIC (JPA 2.0) { İyimser kilit }
Aynı işi gören bu lock türleri, Entity nesneleri üzerinde yapılan okuma işlemlerine kilit eklemeyi taahhüt ederler. Kilitleme mekanizmalarında genel olarak o anda bir yazma işlemi yapılmıyorsa, aynı anda yapılan birden fazla eşzamanlı okuma işlemine izin verilir. Fakat JPA iyimser kilitlemede okuma işlemi yapılırken ya da sonra yazma işlemi yapılmasına engel konulmaz. Sadece bu durumda geliştirici OptimisticLockingException istisnası fırlatılarak bilgilendirilir.
WRITE (JPA 1.0) || OPTIMISTIC_FORCE_INCREMENT (JPA 2.0)
Bu yordamlar entity nesneleri üzerinde yapılan yazma işlemlerinde kullanılırlar. Eğer bir entity nesnesi üzerinde eşzamanlı olarak veri değişikliği yapılmak isteniyorsa bu işleme müdahale edilmez, ama geliştiriciye OptimisticLockingException istisnası fırlatılarak bilgilendirme yapılır. Geliştirici arzu ediyorsa bu istisnaya karşı önlem alabilir. Bu türlerde varsayılan olarak eşzamanlı veri değişikliğinin olmayacağı varsayılır, ama bir karışıklık olduğu takdirde de geliştirici bilgilendirilir. İyimser kilit türlerinde veritabanı seviyesinde kilit uygulanmaz.
PESSIMISTIC_READ { Kötümser kilit }
İyimser kilitleme mekanizmalarının yeterli olmadığı durumlarda kötümser kilit kullanılabilir. Bu kilit mekanizması okuma işlemlerine kilit koyarken veritabanı seviyesinde kilit uygular. PESSIMISTIC_READ uygulanan bir entity nesnesi üzerinde eşzamanlı yapılan okuma işlemlerine müdahale edilmez. Okuma anında yapılmak istenen yazma işlemleri ise okuma işlemi tamamlanana kadar askıya alınır. Bu sayede karışıklıkta giderilmiş olur.
PESSIMISTIC_WRITE
PESSIMISTIC_WRITE uygulanmış bir entity nesnesi üzerinde işlem yapılırken o anki yazma ya da okuma işlemi haricindeki tüm diğer okuma ve yazma işlemleri o anki işlem tamamlanana kadar askıya alınırlar.
PESSIMISTIC_FORCE_INCREMENT
Bu işlem aslında bir üstteki PESSIMISTIC_WRITE gibi davranış gösterir. Fakat JPA 2.0 ile birlikte gelen versiyonlama özelliği ile. Versiyonlama özelliğiyle entity nesnesi üzerinde yapılan her bir değişiklik, entity nesnesinin @Version notasyonu ile sarmalanmış alanında (@Version Long vers; gibi) +1 değer arttırımına neden olur. Bu sayede her bir veri değişikliği versiyonlanmış olur. Çalışma anında bellek alanında bir entity nesnesinin birden fazla yönetimli karşılığı olabilir, versiyonlama sistemiyle en yüksek değerlikli versiyon değerine sahip entity nesnesi diğer eski versiyonlarına göre tercih edilmelidir, çünkü en günceli odur. ***_FORCE_INCREMENT kilit türleri eski versiyona sahip entity nesnelerini temizleyerek eskimiş entity nesnesinin sürece katılımını engellerler. Eğer bir entity nesnesi üzerinde @Version notasyonuyla sarmalanmış bir veri alanı varsa, o entity nesnesi üzerinde varsayılan olarak OPTIMISTIC_FORCE_INCREMENT kilit türü aktif edilir.
NONE
Kilit mekanizmasını o anki entity nesnesi için devre dışı bırakır.
Kilit türlerinin uygulanışı :
Entity nesnelerine uygulanacak kilit türleri LockModeType enum sınıfında tanımlı sabit alanlarla elde edilir. LockModeType’ a ait kilit türlerinin uygulanması içinse EntityManager nesnesinin lock, find ve refresh yordamları kullanılabilir.
Örnekler
- em.lock(kitap, LockModeType.PESSIMISTIC_FORCE_INCREMENT);
- em.find(Kitap.class, 200L,LockModeType.READ);
- em.refresh(kitap, LockModeType.PESSIMISTIC_READ);
Şimdi kendi senaryomuza göre örneğimizi oluşturalım.
Kitap entity sınıfı
@Entity public class Kitap { @Id private Long id; @Version private Long vers; private String kitapAdi; private String yayinevi; private Double fiyat; private String yazar; ... Getter ve Setter metodlar esgeçişmiştir. ... }
Yukarıdaki entity örneği üzerinde işlem yapılacak Kitap entity sınıfını belirtmektedir. Gerçek uygulamalarda birden fazla kullanıcının eşzamanlı olarak bir entity nesnesi üzerinde işlem yapması gayet olağandır. Peki biz test senaryomuzda eşzamanlı görevleri nasıl oluşturabiliriz? Cevap : Çok işlemcikli (MultiThreading) programlama ile. Tek çekirdeğe sahip bir işlemci için eşzamanlı yani aynı anda görev koşturma olasılığı yoktur. Bu test için bize asgari 2 çekirdekli bir işlemci gerekmektedir.
Eşzamanlı görevleri koşturmak adına Java 1.5 sürümüyle birlikte gelen Concurrency kütüphanesi bu test için biçilmiş kaftandır.
Klasik çok işlemcikli Java uygulamalarında ayrı iş parçacıkları olarak görevler, bir sınıfın Runnable arayüzünü uygulamasıyla gerçekleşebilir. Fakat biz uygulamamızda Runnable görevler yerine Callable arayüz ailesinden görevler kullanacağız. Callable görev türlerinin Runnable türlerine göre tek farkı, görev koşturulduktan sonra geriye bir sonuç döndürebildiğidir. Biz uygulamamızda hiçbir geribeslemeye ihtiyaç duymuyoruz fakat uygulamanın mimari şartlarından dolayı Callable türünden görevleri kullanmayı uygun gördük.
Görev :
public class Gorev implements Callable<Void> { EntityManagerFactory emf; EntityManager em; Double fiyatArttir; EntityTransaction trx; Kitap kitap; public Gorev(EntityManagerFactory emf, EntityManager em,EntityTransaction trx, Double fiyat) { this.emf = emf; this.em = em; this.fiyatArttir=fiyat; this.trx=trx; } @Override public Void call() { // Runnable#run yordamı gibi.. try { trx.begin(); // Transaction başla kitap=em.find(Kitap.class, 200L); // 200 id li entity elde et. Thread.sleep(500); // 500 ms bekletilebilir. Kesikli çizgi burası. em.lock(kitap, LockModeType.PESSIMISTIC_WRITE); // İlk gelen kilit koysun kitap.setFiyat(kitap.getFiyat()+fiyatArttir); // Fiyat arttır trx.commit(); // Transaction onayla } catch (Throwable e) { trx.rollback(); // İşlem başarısız geri al. e.printStackTrace(); } return null; } }
Uygulama test sınıfı :
public class App { public static void main(String[] args) throws IExc, ExExc { EntityManagerFactory emf = Persistence.createEntityManagerFactory("Yaris_PU"); EntityManager em1 = emf.createEntityManager(); // 1. Entity Yönetici EntityManager em2 = emf.createEntityManager(); // 2. Entity Yönetici EntityTransaction trx1 = em1.getTransaction(); // Transaction 1 EntityTransaction trx2 = em2.getTransaction(); // Transaction 2 Kitap kitap = new Kitap(); // 200 id li entity oluşturuluyor. kitap.setId(200L); kitap.setFiyat(20d); kitap.setKitapAdi("Java Mimarisiyle Kurumsal Çözümler"); kitap.setYazar("Rahman Usta"); kitap.setYayinevi("Papatya Yayıncılık"); trx1.begin(); em1.persist(kitap); // Veritabanına ilk senkronizasyonu yap trx1.commit(); // 2 işçi ve Eşzamanlı görev havuzu ExecutorService ex = Executors.newFixedThreadPool(2); Gorev gorev1 = new Gorev(emf, em1, trx1, 5d); // 1. Görev Gorev gorev2 = new Gorev(emf, em2, trx2, 10d); // 2. Görev List<Callable> gorevler = new ArrayList<Callable>(); gorevler.add(gorev1); // Görev1 torbaya ekleniyor gorevler.add(gorev2); // Görev2 torbaya ekleniyor ex.invokeAll(gorevler); // Görev 1 & 2 koştur ex.shutdown(); // Görev havuzunu kapatma sinyali // Görevlerin tamamlanmasını azami 5 dakika bekle ex.awaitTermination(5, TimeUnit.MINUTES); em1.refresh(em1.find(Kitap.class, 200L)); // En güncel bilgiyle tazele System.out.println("Kayıt edilen değer : "+kitap.getFiyat()); em1.close(); // Entity yöneticiyi kapat em2.close(); // Entity yöneticiyi kapat emf.close(); // Entity yönetici üretecini kapat } }
Yukarıda yer alan App sınıfı Gorev sınıfından 2 görevi koşturarak sonuçta veritabanına kayıt edilen fiyat bilgisini görüntülemektedir. Bu testin sonucunda fiyat bilgisi kilit mekanizmasından dolayı ve akabinde görevler sırasıyla koşturalacağı için 35 TL olacaktır. Şimdi sizde lock mekanizmasını kaldırarak ve farklı lock türlerini uygulayarak bu uygulamanın davranışını test edebilirsiniz.
Not : İlk önce Mysql veritabanında yarisDB adında bir DB oluşturunuz. Uygulamanın ilk çalıştırılmasından sonra persistence.xml içinde Table Generation Strategy bilgisini Drop and Create’ e çekmeyi unutmayınız.
Tekrar görüşmek dileğiyle..