Transactional Outbox Örüntüsü Nasıl Uygulanır?

Esat KOÇ
Akbank Teknoloji
Published in
5 min readMay 5, 2023

Bu yazımızda dağıtık sistemlerde veri akışındaki tutarlılığı sağlamak için sıklıkla kullanılan “Transactional Outbox” tasarım örüntüsünü inceleyeceğiz ve bir uygulama örneği üzerinden açıklayacağız.

Örnek Gereksinim

Mikroservis mimarisinde uygulamaları küçük iş parçalarına böldüğümüz için bu servislerin aralarındaki iletişim sıklıkla restful çağrılar, kuyruk (Kafka, rabbitmq) operasyonları ile sağlanıyor. Ayrıca, servisimizin dış bir sunucu ile ftp, smb, smtp vb. protokollerle iletişim kurma ihtiyacı da olabiliyor. Ancak bu gibi yapıların kontrolü çoğunlukla uygulamamızın kontrolü dışında kalabiliyor. Diğer bir deyişle, uygulama içindeki bir veritabanı işlemi ile başka bir servis iletişimini aynı transaction içine alamıyoruz. Bu da herhangi bir nedenden dolayı servisimizin hata alması durumunda veri akışındaki bütünlüğün bozulmasına sebep olabiliyor.

Sık kullanılan bir örnek durum üzerinden ilerlersek; bir e-ticaret uygulamasında siparişlerin yaratıldığı “order” servisi ile bu siparişin sonucunda aksiyon alarak kargo işlemlerini gerçekleştirecek bir “shipment” servisi olduğunu düşünelim. Bu iki servis arasında herhangi bir kuyruk mekanizması (Rabbitmq, kafka, redis vb.) kullanıldığını varsayalım. Yani, order servisinde yeni bir sipariş yaratıldığında, kuyruğa ilgili event order servisi tarafından yazılıyor. Shipment servisi ise bu kuyruğu sürekli olarak dinleyerek yeni sipariş event’i geldiğinde kendi sorumluluğundaki işlemleri başlatıyor. Burada aşağıdaki gibi bir tasarım düşünebiliriz:

Design Overview

Bu gereksinimi karşılamak için akla aşağıdaki ilk yöntem gelebilir:

Bu yöntemde order mikroservisimize yeni bir sipariş gelince, önce veritabanına kaydetme, sonrasında da shipment servisine iletmek üzere kuyruğa yazma işlemlerini sırasıyla yapıyoruz. Yalnız burada dikkat etmemiz gereken bir durum var. Siparişi veritabanına kaydettikten sonra kuyruğa yazamadan uygulama herhangi bir nedenden dolayı çökebilir. Bu durumda ise ilgili event’i kuyruğa yazamamış oluruz.

1st Method

İkinci yöntem ise sırayı tersine çevirmek. Yani, kuyruğa yazma işine öncelik veriyoruz. Önce kuyruğa event’i yazıp, sonrasında veritabanına siparişi kaydediyoruz. Bu yöntemde kuyruğa yazdıktan sonra veritabanına kaydedemeden uygulama çökebilir veya veritabanına başka sebeplerden (Constraint vb.) dolayı yazamayabiliriz.

2nd Method

Görüldüğü üzere, her iki yöntemde de veri akışında bütünlük bozulmuş olur ve tutarsız bir state içerisine düşmüş oluruz. Bu gibi durumların çözümünde “eventual consistency” sağlanması için bir seçenek 2PC (2-phase commit) yapısıdır. Ancak 2PC, birazdan inceleyeceğimiz Transactional Outbox örüntüsüne nazaran uygulaması daha zor ve daha kompleks bir yöntem ve bazı durumlarda iletişim kurulan dış entegrasyonların (Kuyruk, smtp, ftp, rest vb) 2PC desteği olmayabilir.

Şimdi gelin, ilgili event’in kuyruğa en az bir kez yazılmasını (At least once) garanti edecek Transactional Outbox örüntüsünü inceleyelim.

Bu örüntü aynı zamanda Saga pattern’inin uygulanması için de kullanılır. Transactional Outbox örüntüsü, order tablosunun yanında bir tablo (Outbox tablosu) daha yaratılmasını gerektiriyor. Yeni bir sipariş geldiğinde aşağıdaki akış ile işlemleri yürütüyoruz:

  • Aynı transaction içinde hem siparişi order tablosuna kaydediyoruz hem de kuyruğa yazılacak event’in durumunu tutacak bir kaydı outbox dediğimiz bir tabloya kaydediyoruz. Bu iki kayıt ya beraber gerçekleşiyorlar ya da beraber gerçekleşemiyorlar. (Rollback) Bu garantiyi günümüzde kullanılan tüm rdm veritabanı sistemleri sağlayabilir.
  • Bu pattern’in uygulamasında arka planda çalışacak bir “background processor”e ihtiyacımız var. (Bunun yerine debezzium gibi transaction log’larını tarayan üçüncü parti change-data-capture sistemleri de kullanılabilir.) Background Processor, belirli periyotlarla outbox tablosunu tarayarak henüz kuyruğa yazılmamış veya daha önce denenmiş ancak kuyruğa yazılamamış event kayıtlarını okuyarak kuyruğa yazma işlemini yapıyor. Tabii ki kuyruğa yazma işleminin sonucuna göre outbox tablosundaki kayıtların statüsünü değiştiriyor.
Transactional Outbox Pattern
  • Shipment servisi kuyruğa yazılan event’leri tüketerek yeni siparişler için gerekli işlemleri gerçekleştiriyor.
  • Eğer kuyruğa yazılamadan uygulama çökerse uygulama ayağa kalktığında, background processor yazılamayan event’leri tekrar kuyruğa yazabilir.
  • Eğer event kuyruğa yazıldıktan sonra, Background Processor outbox kaydını başarılı olarak güncelleyemeden uygulamamız çökerse uygulama ayağa kalktığında aynı kaydı tekrar kuyruğa yazmaya çalışır. Bunun için aşağıdaki uyarıya dikkat etmek gerekir:

UYARI: Transactional outbox örüntüsünde event’in kuyruğa yazılması en az bir kez gerçekleşir. Bu demek oluyor ki event’in birden fazla kez yazılması (Mükerrer kayıt) da mümkündür. Bu durumda, event’ler kuyruğa yazılırken unique bir key ile yazılmalı ve kuyruğun tüketicileri aynı kaydı iki kez okusa bile mükerrer kayıtları dikkate almamalıdır. Diğer bir deyişle, kuyruğun kendisi veya kuyruk tüketicileri idempotent servisler olmalıdır.

Örnek Uygulama

Şimdi yukarıda verdiğimiz tasarım örüntüsünü Java Spring Boot altyapısı ile koda dökelim. Veritabanı olarak PostgreSql kullanacağız. Kuyruk implementasyonu odağımız dışında olacak, bunu simule eden bir metot kullanacağız.

Öncelikle, aşağıdaki entity sınıflarını yaratıyoruz.

OrderEvent sınıfını outbox kaydı olarak kullanacağız.

Şimdi ise her iki sınıfın veritabanı işlerini yapmak üzere aşağıdaki repository sınıflarını Spring Data JPA kullanarak oluşturacağız.

OrderEventRepository sınıfına aşağıdaki custom sql cümleciklerini yazıyoruz

Yeni sipariş geldiğinde Order ve OrderEvent nesnelerini yaratarak veritabanına kaydetmek üzere repository’ye gönderen ve statü güncellemelerini gerçekleştirecek aşağıdaki metotları barındıracak bir sınıf yazıyoruz.

OrderService implementasyonunda önemli bir nokta create() metodunun transactional olarak yazılması. Bu sınıfın implementasyonu ve diğer detaylar için Github reposunu ayrıca paylaşacağız.

Şimdi BackgroundProcessor olarak kullanacağımız OrderEventQueueJob sınıfını yazıyoruz. Arka planda belirli periyotlarla çalışacak bu Job’ın uygulaması için Quartz kütüphanesinden faydalanıyoruz. (Quartz kütüphanesini detaylı incelemek için daha önceki blog yazımı inceleyebilirsiniz.)

Bu arka plan Job’ı, son iki gündeki gönderilemeyen event’leri alarak tek tek kuyruğa yazmaya çalışıyor. Kuyruğa yazma işleminin başarım durumuna göre ise ilgili event’in statüsünü güncelliyor. Burada, writeToQueue() metodu simüle edilmiştir. Aslında, kuyruğa yazma işlemi yerine dış servislerle iletişime geçecek başka bir işlem (Ftp ile dosya yazımı, mail gönderimi vs.) de olabilirdi.

OrderEventQueueJob adlı görevimizi uygulama ayağa kalkarken register ediyoruz ve her 5 saniyede bir çalışacak şekilde yapılandırıyoruz. Uygulamamız tamamlandı. Çalışan uygulamayı detaylarıyla birlikte incelemek için aşağıdaki Github repoma göz atabilirsiniz.

Bir sonraki yazıda görüşmek üzere…

--

--