← Back to list

Spring WebFlux ile Non-Blocking

Giriş

Sahin Yelkenci · 2026-02-21 10:51 · 0 claps · 8.9 min read
#webflux #java #non-blocking-io #blocking-io #spring-boot
Open on Medium ↗
Wiki topics: MM · Multimodal & Generative Media

Spring WebFlux ile Non-Blocking

Giriş

Saniyede binlerce, on binlerce hatta milyonlarca işlem (TPS — Transactions Per Second) gerçekleştiren sistemler nasıl inşa edilir? Bu yazı dizisinde, yüksek performanslı sistemlerin arkasındaki mimari kararları, teknoloji seçimlerini ve pratik implementasyon detaylarını inceleyeceğiz.

İlk bölümde, modern yüksek TPS sistemlerinin temel yapı taşlarından biri olan Non-Blocking I/O kavramını ve Spring WebFlux’un bu konsepti nasıl uyguladığını uçtan uca analiz edeceğiz.

İçindekiler

  1. Blocking vs Non-Blocking: Temel Fark
  2. Neden Non-Blocking?
  3. Spring WebFlux ile Non-Blocking Kod
  4. Satır Satır Analiz
  5. Çalışma Zamanı: Ne Zaman Ne Olur?
  6. Thread Kullanımı Karşılaştırması
  7. Event Loop Mekanizması
  8. Reactive Veritabanı Erişimi: R2DBC
  9. Reactive HTTP Client: WebClient
  10. Hata Yönetimi
  11. Ne Zaman Kullanmalı?
  12. Sonuç

1. Blocking vs Non-Blocking: Temel Fark

Yüksek TPS’li sistemleri anlamak için önce temel I/O modellerini kavramalıyız.

Blocking I/O

Bir işlem (veritabanı sorgusu, dosya okuma, API çağrısı) tamamlanana kadar thread bekler, başka iş yapamaz.

Geleneksel modelde, bir iş parçacığı (Thread) bir veritabanı sorgusu veya dosya okuma işlemi başlattığında, sonuç gelene kadar donup kalır.

  • Süreç: Thread 1 bir istek alır, DB sorgusunu gönderir ve kırmızı alanda gördüğümüz gibi “BEKLİYOR” moduna geçer.
  • Verimsizlik: Bu sırada CPU aslında hiçbir iş yapmaz, sadece dış kaynaktan yanıt bekler. Diğer iş parçacıkları (Thread 2) ise boşta bekleyebilir veya yeni bir thread açılması gerekebilir.
  • Sonuç: Çok fazla kullanıcı geldiğinde sistem her biri için yeni bir thread açmaya çalışır; bu da bellek tüketimini artırır ve sistemi hantallaştırır.

Non-Blocking I/O

Thread işlemi başlatır, tamamlanmasını beklemeden başka işlere geçer. İşlem bitince bildirim alır ve devam eder.

Modern ve yüksek performanslı sistemlerin (WebFlux, Node.js, Netty, Go gibi) sırrı buradadır. Bir işlem başlatılır ama yanıtın gelmesi beklenmeden diğer işlere geçilir.

  • Süreç: Thread 1, DB sorgusunu başlatır (Sarı alan) ve hemen ardından ikinci, üçüncü istekleri almaya (Yeşil alanlar) devam eder.
  • Verimlilik: Veritabanından yanıt geldiğinde (Mavi alan), thread o anki işini bitirip araya bu yanıtı sıkıştırır.
  • Sonuç: Tek bir thread ile onlarca, hatta yüzlerce eşzamanlı işlem yönetilebilir.

2. Neden Non-Blocking?

Somut bir senaryoyla açıklayalım. Bir kullanıcının detay sayfası için üç farklı kaynaktan veri çekmemiz gerekiyor:

Temel Fark Tablosu

Özetle: Eğer bir restoran işletiyorsanız; Blocking, garsonun siparişi mutfağa verip yemek pişene kadar aşçının başında beklemesidir. Non-Blocking ise garsonun siparişi bırakıp hemen diğer masalara servis yapmaya devam etmesidir.

Blocking Yaklaşım

@GetMapping("/user/{id}")
public UserDetails getUserDetails(@PathVariable Long id) {

    User user = userRepository.findById(id);           // 100ms bekle
    List<Order> orders = orderRepository.findByUserId(id);  // 150ms bekle
    Balance balance = paymentClient.getBalance(id);    // 200ms bekle

    return new UserDetails(user, orders, balance);

    // TOPLAM: 100 + 150 + 200 = 450ms
    // Thread 450ms boyunca meşgul (ama çoğu bekleme)
}

Zaman Çizelgesi:

3. Spring WebFlux ile Non-Blocking Kod

@RestController
@RequestMapping("/api")
public class UserController {

    private final UserRepository userRepository;      // R2DBC
    private final OrderRepository orderRepository;    // R2DBC
    private final WebClient paymentClient;            // Reactive HTTP Client

    @GetMapping("/user/{id}")
    public Mono<UserDetails> getUserDetails(@PathVariable Long id) {

        // Adım 1: Üç işlemi AYNI ANDA tanımla
        Mono<User> userMono = userRepository.findById(id);
        Mono<List<Order>> ordersMono = orderRepository.findByUserId(id).collectList();
        Mono<Balance> balanceMono = paymentClient.get()
            .uri("/balance/" + id)
            .retrieve()
            .bodyToMono(Balance.class);

        // Adım 2: Üçü de tamamlanınca birleştir
        return Mono.zip(userMono, ordersMono, balanceMono)
            .map(tuple -> new UserDetails(
                tuple.getT1(),   // User
                tuple.getT2(),   // List<Order>
                tuple.getT3()    // Balance
            ));
    }
}

4. Satır Satır Analiz

Mono ve Flux Nedir?

Mono ve Flux, Reactor kütüphanesinin temel tipleridir:

Mono: “Ya Hep, Ya Hiç”

Mono, 0 veya en fazla 1 adet veri yayınlayan (emit eden) bir asenkron yapıdır.

  • Kullanım Alanı: Tek bir nesne döndüren işlemler için idealdir.
  • Örnek: Veritabanından ID ile bir kullanıcı çekmek (findById), bir HTTP POST isteği sonucu dönen yanıt veya boş bir void işlemi (Mono<Void>).
  • Mantık: İşlem bittiğinde ya bir değer döner ya da hiçbir şey dönmeden işlemin bittiğini haber verir.

Flux: “Veri Akışı”

Flux, 0 ile $N$ (sonsuz) arasında veri yayınlayabilen bir asenkron akıştır.

  • Kullanım Alanı: Liste veya veri serisi döndüren işlemler için kullanılır.
  • Örnek: Bir tablodaki tüm kayıtları listelemek (findAll), bir kaynaktan gelen anlık logları takip etmek veya bir Twitter akışını (stream) dinlemek.
  • Mantık: Veriler geldikçe tek tek yayınlanır. Alıcı taraf, her veri geldiğinde onu işleyebilir.

Kritik Nokta: Lazy Evaluation

Mono<User> userMono = userRepository.findById(id);

Bu satır çalıştığında veritabanına henüz gidilmez! Mono bir “tarif” gibidir:

“Birisi bana subscribe olursa, veritabanına git ve User getir”

userMono = Mono[
    Tarif: "SELECT * FROM users WHERE id = ? çalıştır"
    Durum: Henüz çalışmadı
    Sonuç: Bekleniyor
]

Her Satırın Analizi

  1. Kullanıcı Mono’su:
Mono<User> userMono = userRepository.findById(id);
// R2DBC repository, henüz DB'ye gidilmez
  1. Siparişler Mono’su:
Mono<List<Order>> ordersMono = orderRepository.findByUserId(id).collectList();
// Flux<Order> → Mono<List<Order>> dönüşümü, henüz DB'ye gidilmez
  1. Bakiye Mono’su:
Mono<Balance> balanceMono = paymentClient.get()
    .uri("/balance/" + id)
    .retrieve()
    .bodyToMono(Balance.class);
// WebClient tanımı, henüz HTTP çağrısı yapılmaz
  1. Mono.zip() — Sihir Burada:
Mono.zip(userMono, ordersMono, balanceMono)

Mono.zip() ne yapar:

  • Verilen tüm Mono’ları aynı anda subscribe eder (başlatır)
  • Hepsi tamamlanana kadar bekler
  • Sonuçları bir Tuple içinde birleştirir
  1. .map() — Dönüşüm:
.map(tuple -> new UserDetails(
    tuple.getT1(),   // User
    tuple.getT2(),   // List<Order>
    tuple.getT3()    // Balance
));

5. Çalışma Zamanı: Ne Zaman Ne Olur?

1. Tanımlama Aşaması (Lazy Initialization)

Diyagramın en üstünde gördüğümüz gibi, userMono, ordersMono ve balanceMono tanımlandığında henüz hiçbir işlem başlamaz.

  • Neden? Reactive dünyada “Siz abone (subscribe) olana kadar hiçbir şey çalışmaz” prensibi vardır.
  • Bu aşamada sadece bir “yemek tarifi” hazırlarsınız, henüz ocağın altını açmazsınız.

2. Mono.zip() — Güçleri Birleştirme

Mono.zip() fonksiyonu, elinizdeki birbirinden bağımsız bu üç farklı kaynağı bir araya getirir. Diyagramdaki asıl fark burada ortaya çıkıyor:

  • Paralel Tetikleme: zip() aktif edildiği anda, sistem DB Query 1, DB Query 2 ve HTTP Call işlemlerini aynı anda başlatır.
  • Thread Serbestiyeti: Bu işlemler dış kaynaklardan yanıt beklerken, görselde “THREAD SERBEST” olarak belirtildiği gibi, işlemciyi meşgul etmezler.

3. Zaman Kazancı: Matematiksel Kanıt

Görseldeki “Kazanç: %55 daha hızlı” vurgusu şu hesaplamaya dayanır:

  • Blocking (Sıralı) Yaklaşım: Önce User (100ms), sonra Orders (150ms), en son Balance (200ms) beklenirdi. Toplam süre: $100 + 150 + 200 = 450$ms.
  • Reactive (Paralel) Yaklaşım: Üçü aynı anda başlar. Sistem, en uzun süren işlemin (200ms) bitmesini bekler. Toplam süre: ~200ms.

Diyagramın Bize Öğrettiği 3 Kritik Nokta

  1. En Yavaş Olan Belirleyici: Mono.zip() kullanıldığında toplam süre, gruptaki en uzun süren işleme eşittir.
  2. Tuple Yapısı: Tüm veriler hazır olduğunda, zip() bize bir Tuple (veri paketi) döner. Diyagramdaki Tuple3<User, List<Order>, Balance> ifadesi, üç farklı tipteki verinin tek bir pakette birleştiğini gösterir.
  3. İşlem Sonrası Dönüşüm: Veriler birleşince .map() ile bu paket alınıp nihai UserDetails nesnesine dönüştürülür ve kullanıcıya yanıt olarak dönülür.

Özetle

Eğer microservice mimarisinde çalışıyorsan ve bir sayfa için 3–4 farklı servisten veri toplaman gerekiyorsa, Mono.zip() senin en yakın dostundur. Bloklamalı yapıda 1 saniye sürecek bir işlem, bu yöntemle 200-300 milisaniyeye düşebilir.

6. Thread Kullanımı Karşılaştırması

Blocking Model

Non-Blocking Model

Sayılarla Karşılaştırma

7. Event Loop Mekanizması

Kavramsal Çalışma Şekli

class EventLoop {
    Queue<Event> eventQueue = new ConcurrentLinkedQueue<>();

    void run() {
        while (true) {
            Event event = eventQueue.poll();

            if (event != null) {
                event.handle();  // Çok kısa sürmeli!
            }

            checkReadyIO();  // Tamamlanan I/O'ları kontrol et
        }
    }

    void checkReadyIO() {
        List<CompletedIO> ready = selector.selectNow();

        for (CompletedIO io : ready) {
            eventQueue.add(io.getCallback());
        }
    }
}

Event Kuyruğu Örneği

1. Event Queue (FIFO): İsteklerin Giriş Kapısı

WebFlux, her gelen isteği ağır bir “Thread” içine hapsetmek yerine, onları küçük birer Event (Olay) olarak bir kuyruğa atar.

  • Yeni İstekler: Görselde mavi ile gösterilen “HTTP İstek 1, 2, 3…” gibi talepler anında kuyruğa girer.
  • Callback Mekanizması: En kritik nokta burasıdır. Bir DB sorgusu veya API çağrısı bittiğinde, sonuç yine bu kuyruğa bir “callback” (geri dönüş) olayı olarak düşer (Örn: 4, 6 ve 7. maddeler).

2. Event Loop: Hiç Durmayan Motor

Sağ taraftaki yeşil kutu, sistemin kalbidir. Bu genellikle CPU çekirdek sayısına göre optimize edilmiş çok az sayıda (genelde çekirdek başına 1) thread’dir.

  • Hız: Event Loop sürekli döner ve kuyruktan sıradaki işi alır.
  • Bloke Olmama: Eğer bir iş (örneğin DB sorgusu) zaman alacaksa, Event Loop onu başlatır ve beklemek yerine hemen kuyruktaki bir sonraki işe geçer.
  • Verimlilik: Görselde belirtildiği gibi, thread asla boş durmaz; ya yeni bir istek alır ya da biten bir işlemin sonucunu (callback) paketleyip kullanıcıya döner.

3. İki Görsel Arasındaki Bağlantı

Bu iki görseli kafamızda şöyle birleştirebiliriz:

  • Mono.zip() Diyagramında: Üç farklı işlemin (User, Order, Balance) aynı anda başladığını görmüştük.
  • Event Loop Diyagramında: Bu üç işlemin sonuçları (callback) kuyruğa düştükçe (4, 6 ve 7 numaralı adımlar gibi), Event Loop bu parçaları toplar.
  • Sonuç: Hepsi tamamlandığında (8. adım), “Istek 1 tamamlandı” diyerek yanıtı gönderir.

Neden %100 Verimli?

Klasik mimaride (Tomcat/Servlet) her bekleme süresinde thread “uyur”. Bu görseldeki yapıda ise thread asla uyumaz. Bir yandan yeni müşterilerin siparişini alırken, diğer yandan mutfaktan çıkan yemekleri masalara dağıtan hiperaktif bir garson gibidir.

Özetle: WebFlux’ta olaylar bir nehir gibi akar; Event Loop bu nehri yöneten kaptandır, Mono ve Flux ise bu nehirde taşınan paketlerdir.

8. Reactive Veritabanı Erişimi: R2DBC

Non-blocking çalışması için veritabanı driver’ının da reactive olması gerekir.

JDBC vs R2DBC

// JDBC (Blocking) - WebFlux ile KULLANMAYIN
public interface UserRepository extends JpaRepository<User, Long> {
    User findById(Long id);  // Thread bloklanır
}

// R2DBC (Non-Blocking) - WebFlux ile kullanın
public interface UserRepository extends ReactiveCrudRepository<User, Long> {
    Mono<User> findById(Long id);  // Thread bloklanmaz
}

R2DBC Akış Diyagramı

1. JDBC (Java Database Connectivity): “Bloklayan Bağlantı”

JDBC, geleneksel Blocking I/O prensibiyle çalışır.

  • Çalışma Mantığı: Bir SQL sorgusu gönderdiğinde, veritabanından yanıt gelene kadar o işi yapan Thread “kırmızı alan”da (ilk görselindeki gibi) bekler.
  • Thread Başına Bağlantı: Her veritabanı işlemi bir thread’i rehin alır. Eğer 1000 adet eşzamanlı sorgun varsa, uygulama sunucunda 1000 tane thread olması gerekir.
  • Kısıt: Bu durum, yüksek TPS (Saniye Başına İşlem) hedeflediğin sistemlerde ciddi bir darboğaz (bottleneck) oluşturur.

2. R2DBC (Reactive Relational Database Connectivity): “Akışkan Bağlantı”

R2DBC, paylaştığın ikinci görseldeki Event Loop ve Non-Blocking yapısını veritabanı katmanına getirir.

  • Çalışma Mantığı: Sorguyu gönderir ve hemen thread’i serbest bırakır (Thread Serbest!). Veritabanından veri geldikçe bir “Event” (Olay) olarak uygulamaya döner.
  • Mono ve Flux Uyumu: R2DBC, sonuçları doğrudan Mono veya Flux olarak döner. Örneğin, 1 milyon satırlık bir veriyi çekerken, veriler geldikçe Flux üzerinden akmaya başlar; tüm verinin gelmesini beklemen gerekmez.
  • Verimlilik: Çok az sayıda thread ile binlerce veritabanı bağlantısını yönetebilirsin.

Karşılaştırma Tablosu

9. Reactive HTTP Client: WebClient

Temel Kullanım

@Bean
public WebClient paymentClient() {
    return WebClient.builder()
        .baseUrl("http://payment-service")
        .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
        .build();
}

// Kullanım
Mono<Balance> balanceMono = paymentClient
    .get()
    .uri("/balance/" + id)
    .retrieve()
    .bodyToMono(Balance.class);

Detaylı Konfigürasyon

paymentClient
    .get()
    .uri("/balance/{id}", id)
    .header("Authorization", "Bearer " + token)
    .retrieve()
    .onStatus(
        status -> status.is4xxClientError(),
        response -> Mono.error(new NotFoundException())
    )
    .onStatus(
        status -> status.is5xxServerError(),
        response -> Mono.error(new ServiceUnavailableException())
    )
    .bodyToMono(Balance.class)
    .timeout(Duration.ofSeconds(2))
    .retryWhen(Retry.backoff(3, Duration.ofMillis(100)));

10. Hata Yönetimi

Kapsamlı Örnek

@GetMapping("/user/{id}")
public Mono<UserDetails> getUserDetails(@PathVariable Long id) {

    Mono<User> userMono = userRepository.findById(id)
        .switchIfEmpty(Mono.error(new UserNotFoundException(id)));

    Mono<List<Order>> ordersMono = orderRepository.findByUserId(id)
        .collectList()
        .onErrorResume(e -> {
            log.error("Siparişler alınamadı", e);
            return Mono.just(Collections.emptyList());
        });

    Mono<Balance> balanceMono = paymentClient.get()
        .uri("/balance/" + id)
        .retrieve()
        .bodyToMono(Balance.class)
        .timeout(Duration.ofSeconds(2))
        .onErrorResume(e -> {
            log.error("Bakiye alınamadı", e);
            return Mono.just(Balance.unknown());
        });

    return Mono.zip(userMono, ordersMono, balanceMono)
        .map(tuple -> new UserDetails(
            tuple.getT1(),
            tuple.getT2(),
            tuple.getT3()
        ));
}

Hata Yönetimi Operatörleri

Hata Yönetimi Operatörleri

Hata Yönetimi Operatörleri

11. Ne Zaman Kullanmalı?

✅ Kullanın

  • Çok sayıda eşzamanlı bağlantı (10K+)
  • I/O yoğun işlemler (çok API çağrısı, DB sorgusu)
  • Streaming veri (Server-Sent Events, WebSocket)
  • Mikroservis iletişimi yoğun sistemler
  • Yüksek TPS gereksinimleri

❌ Kullanmayın

  • CPU yoğun işlemler (görüntü işleme, şifreleme)
  • Basit CRUD uygulamaları
  • Ekip reactive programlama bilmiyor
  • Mevcut blocking kütüphanelere bağımlılık var

Karar Matrisi

12. Sonuç

Özet Tablo

Önemli Hatırlatma

Client açısından her zaman senkron bir istek-yanıt döngüsü vardır. Non-blocking kavramı server içindeki işlemlerin birbirini beklemeden paralel çalışmasını ifade eder.

Kaynaklar

***İçindekiler…***

« Önceki [Microsoft Orleans: Dağıtık Sistemler için Virtual Actor Framework*]*

» Sonraki [Dağıtık Sistemlerde Tutarlılık: Strong ve Eventual Consistency Karşılaştırması]

**» EK-A** [Akka vs Microsoft Orleans vs Spring WebFlux , Uçtan Uca Yaşam Döngüsü Karşılaştırması]


메타데이터
post_id
3c36306e1daa
slug
spring-webflux-ile-non-blocking-3c36306e1daa
url
https://medium.com/@sahinyelkenci/spring-webflux-ile-non-blocking-3c36306e1daa
canonical_url
https://medium.com/@sahinyelkenci/spring-webflux-ile-non-blocking-3c36306e1daa
author_url
https://medium.com/@sahinyelkenci
status
ok
fetched_at
2026-08-17 16:17:32