# HikariCP ile Dinamik Veri Kaynağı Yönlendirme

> AbstractRoutingDataSource bağlantı havuzu oluşturmaz; transaction başlamadan seçilen yönlendirme anahtarına göre çağrıyı bağımsız HikariCP havuzlarından birine aktarır.

- Author: Muhammet Ali Köker
- Language: tr
- Canonical: https://alikoker.com.tr/hikaricp-ile-dinamik-veri-kaynagi-yonlendirme
- Translation: https://alikoker.com.tr/en/dynamic-data-source-routing-with-hikaricp
- Published: 2021-10-15T12:00:00+03:00
- Modified: 2026-08-31T22:45:00+03:00
- Verified: 2026-08-07T11:00:00+03:00
- Type: article

"AbstractRoutingDataSource", [bağlantı havuzu](/wiki/connection-pool) oluşturmaz. "getConnection()" çağrısını o anda geçerli olan yönlendirme anahtarına göre başka bir "DataSource" nesnesine aktarır. Hedefler HikariCP kullanıyorsa her hedef bağımsız bir "HikariDataSource" ve bağımsız bir fiziksel bağlantı havuzudur. Yönlendirme katmanı bu havuzları tek bir "DataSource" arayüzü altında toplar.

Bu ayrım, çok sayıda Oracle şeması bulunan sistemlerde belirleyicidir. Her şema için ayrı HikariCP havuzu tanımlanmışsa `maximumPoolSize` yalnız o havuzun sınırıdır; toplam teorik fiziksel bağlantı sınırı bütün havuzların `maximumPoolSize` değerlerinin toplamıdır. Bazı havuzlar hiç kullanılmasa bile `minimumIdle`, başlangıç davranışı ve bağlantı yaşam döngüsü toplam oturum sayısını etkiler.

Bu tasarım, çok sayıda Oracle şemasının tek uygulama katmanından yönlendirildiği sistemlerde geliştirdiğim veri erişim yaklaşımından türedi. Şema başına bağımsız havuz kullanıldığında tek bir `maximumPoolSize` değeri sistem genelindeki bağlantı sayısını temsil etmediği için yönlendirme anahtarı, transaction sınırı ve toplam bağlantı bütçesini birlikte modellemek zorunda kaldım.

## Yönlendirme anı

"AbstractRoutingDataSource", hedef veri kaynağını sorgu metnine, depo sınıfına veya transaction adına bakarak kendiliğinden seçmez. Alt sınıfın uyguladığı "determineCurrentLookupKey()" metodu bir anahtar döndürür. Spring bu anahtarı önceden çözümlenmiş hedef veri kaynakları haritasında arar ve seçilen "DataSource" üzerinden bağlantı alır. Anahtarın türü serbesttir, ancak dönen değer haritadaki anahtar türüyle eşleşmelidir.

En sade uygulama, geçerli anahtarı bir "ThreadLocal" içinde tutar:

```java
public final class DataSourceContext {
    private static final ThreadLocal<String> CURRENT = new ThreadLocal<>();

    private DataSourceContext() {
    }

    public static void set(final String key) {
        CURRENT.remove();
        CURRENT.set(java.util.Objects.requireNonNull(key));
    }

    public static String get() {
        return CURRENT.get();
    }

    public static void clear() {
        CURRENT.remove();
    }
}
```

Yönlendirici sınıf yalnızca bu değeri döndürür:

```java
public final class RoutingDataSource extends org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContext.get();
    }
}
```

"ThreadLocal.set(null)" yerine "remove()" kullanılmalıdır. Özellikle zamanlayıcı, platform iş parçacığı havuzu veya uzun ömürlü executor kullanan yapılarda iş parçacığı sonraki görevlerde tekrar kullanılabilir. Önceki görevden kalan anahtar temizlenmezse yeni görev yanlış Oracle şemasına yönlenebilir.

Güvenli yaşam döngüsü şu sırayı izler:

```java
public void dispatch(final String key) {
    DataSourceContext.clear();
    DataSourceContext.set(key);

    try {
        transactionalService.process();
    } finally {
        DataSourceContext.clear();
    }
}
```

Buradaki "transactionalService", ayrı bir Spring bean'i olmalıdır. Yönlendirme anahtarı transaction başlamadan ve ilk JDBC erişiminden önce atanır. "finally" bloğu normal dönüş, checked istisna ve çalışma zamanı istisna yollarında temizliği garanti eder.

"InheritableThreadLocal" bu problem için güvenli bir çözüm değildir. Alt iş parçacığına bağlam kopyalamak, transaction kaynağını veya JDBC bağlantısını aktarmak anlamına gelmez. Spring'in zorunlu transaction yönetimi kaynakları mevcut iş parçacığına bağlar. İş başka bir iş parçacığına taşındığında aynı transaction bağlamının devam ettiği varsayılamaz.

## Transaction sınırı

Yönlendirme anahtarı her SQL komutundan önce yeniden değerlendirilmez. Spring transaction yöneticisi, routing "DataSource" üzerinden aldığı JDBC bağlantısını mevcut iş parçacığına bağlar. Aynı transaction içindeki "JdbcTemplate" ve diğer Spring JDBC işlemleri bu bağlı bağlantıyı yeniden kullanır.

Bu nedenle aşağıdaki akış beklenen sonucu üretmez:

```java
@Transactional
public void process() {
    DataSourceContext.set("SCHEMA_A");
    repository.insertA();

    DataSourceContext.set("SCHEMA_B");
    repository.insertB();
}
```

İlk JDBC erişimi "SCHEMA_A" havuzundan bağlantı aldıysa transaction süresince aynı fiziksel bağlantı kullanılır. Bağlam daha sonra "SCHEMA_B" olarak değiştirilse bile ikinci işlem otomatik olarak başka havuza geçmez. Yönlendirme anahtarı ile bağlı bağlantı birbirinden kopmuş olur.

Tek transaction içinde birden fazla veri kaynağına yazmak gerekiyorsa "AbstractRoutingDataSource" tek başına yeterli değildir. İşlem iki bağımsız yerel transaction olarak tasarlanabilir, [dağıtık transaction](/wiki/distributed-transaction) yöneticisi kullanılabilir veya iş akışı idempotent adımlara ayrılabilir. Birden fazla Oracle şeması aynı veritabanı oturumu üzerinden erişilebiliyorsa ayrı havuz yerine yetkilendirilmiş şema adlarıyla sorgu yürütmek de farklı bir mimari seçenektir. Bu karar güvenlik sınırlarına ve şema sahipliği modeline bağlıdır.

"REQUIRES_NEW" yeni fiziksel transaction ve yeni bağlantı gerektirebilir. Dış transaction bağlantıyı tutarken iç transaction ikinci bir bağlantı bekler. Çok sayıda eş zamanlı iş akışında her iş parçacığı dış bağlantıyı tutup iç bağlantı beklerse havuz tükenebilir. Spring belgeleri, bu propagation türünün havuz kapasitesini aşması halinde kilitlenmeye kadar gidebilen kaynak beklemeleri oluşturabileceğini belirtir.

Routing anahtarının AOP ile atanması halinde aspect sırası da transaction sınırının parçasıdır. Routing aspect, transaction interceptor çalışmadan önce bağlamı hazırlamalıdır. Aksi halde transaction yöneticisi varsayılan veya önceki veri kaynağından bağlantı alabilir.

Spring'in varsayılan proxy tabanlı transaction yönetiminde aynı sınıf içindeki self-invocation, "@Transactional" metodunu proxy üzerinden geçirmez. Bu nedenle routing ayarlandıktan sonra aynı nesne üzerindeki başka bir metoda doğrudan çağrı yapmak transaction sınırını beklenen yerde başlatmayabilir. Spring belgeleri proxy modunda yalnız dışarıdan proxy üzerinden gelen çağrıların yakalandığını açıkça belirtir.

## Hedef havuzların kurulması

Routing veri kaynağı oluşturulurken hedefler anahtar ve "DataSource" eşleşmesi olarak verilir:

```java
@Bean
public DataSource dataSource(final Map<String, HikariDataSource> pools) {
    final RoutingDataSource routing = new RoutingDataSource();
    final Map<Object, Object> targets = new java.util.HashMap<>();

    for (final Map.Entry<String, HikariDataSource> entry : pools.entrySet()) {
        targets.put(entry.getKey(), entry.getValue());
    }

    routing.setTargetDataSources(targets);
    routing.setLenientFallback(false);
    routing.afterPropertiesSet();
    return routing;
}
```

"afterPropertiesSet()" iç durumu hazırlar ve tanımlanan veri kaynaklarını çözümler. Spring tarafından [bean yaşam döngüsü](/wiki/bean-lifecycle) içinde oluşturulan bir "AbstractRoutingDataSource" için bu çağrı otomatik yapılır. Nesne elle oluşturulup Spring dışında kullanılıyorsa başlatma sorumluluğu uygulamaya aittir. Çözümlenmiş veri kaynakları haritası dışarıya değiştirilemez görünümle sunulur.

"setLenientFallback(false)" kritik sistemlerde daha güvenli davranır. Varsayılan değer "true" olduğundan haritada bulunmayan bir anahtar sessizce varsayılan veri kaynağına düşebilir. Yanlış yazılmış şema anahtarı bu durumda hata vermek yerine başka şemada işlem yapabilir. "false" kullanıldığında null olmayan fakat eşleşmeyen anahtar "IllegalStateException" üretir.

Varsayılan veri kaynağı yalnız gerçekten tanımlı bir iş kuralı varsa kullanılmalıdır. Anahtarın unutulması programlama hatasıysa null değerin de hata üretmesi daha doğru olabilir. Bunun için "determineCurrentLookupKey()" veya "determineTargetDataSource()" çevresinde ek doğrulama yapılabilir.

Hedef haritayı çalışma sırasında değiştirip aynı routing nesnesini kullanmak dikkat gerektirir. "AbstractRoutingDataSource", yapılandırılmış hedefleri başlatma sırasında çözümler. Yeni bir havuz eklemek için yalnız kaynak haritaya eleman eklemek yeterli değildir. Yeniden başlatma işlemi, eş zamanlı "getConnection()" çağrıları ve eski havuzun kapanışı birlikte yönetilmelidir. Resmi sınıf genel amaçlı, kilitsiz bir dinamik havuz kayıt sistemi sunmaz.

Sabit sayıda Oracle şeması için hedefleri uygulama başlangıcında oluşturmak daha öngörülebilir davranır. Yeni veri kaynağının çalışma sırasında eklenmesi zorunluysa routing katmanının değişmez bir kayıt görüntüsü üzerinde çalışması, yeni görüntünün atomik olarak yayımlanması ve çıkarılan Hikari havuzlarının aktif bağlantıları tamamlandıktan sonra kapatılması gerekir.

## HikariCP kapasite hesabı

Her hedef ayrı havuz olduğu için kapasite hesabı iki düzeyde yapılmalıdır. İlk düzey tek şemanın eş zamanlı işlem ihtiyacıdır. İkinci düzey bütün havuzların Oracle üzerinde oluşturabileceği toplam oturum sayısıdır.

Toplam teorik sınır şöyledir:

```text
C_toplam = Σ maximumPoolSize_i
```

Çok sayıdaki havuzun her birine aynı `maximumPoolSize` değerini vermek, her şemanın aynı anda aynı bağlantı kapasitesine ihtiyaç duyduğu anlamına gelmez. Trafik birkaç yoğun şemada toplanıyorsa havuzlar farklı boyutlandırılabilir. Eşit konfigürasyon yönetimi kolaylaştırır, fakat veritabanı kapasitesini doğru dağıtmayabilir.

"minimumIdle" belirtilmezse HikariCP sabit boyutlu havuza yakın davranmayı önerir. Bu davranış ani yükte bağlantı oluşturma gecikmesini azaltır. Çok sayıda seyrek kullanılan veri kaynağında ise bütün havuzların yüksek sayıda boş bağlantı tutmasına yol açabilir. "minimumIdle < maximumPoolSize" kullanıldığında "idleTimeout" devreye girer ve havuz ihtiyaç azaldığında boş bağlantıları asgari değere kadar azaltabilir.

"connectionTimeout", havuz doluyken çağrının bağlantı bekleyebileceği en uzun süredir. Sanal iş parçacıkları kullanılması bu sınırı kaldırmaz. Sanal iş parçacıkları beklemeyi daha düşük platform iş parçacığı maliyetiyle taşıyabilir, ancak Oracle tarafındaki fiziksel bağlantı sayısı yine "maximumPoolSize" ile sınırlıdır. Çok sayıda görev havuz önünde bekliyorsa sistemin aktarım kapasitesi artmaz, yalnız kuyruk büyür.

"maxLifetime", ağ veya veritabanı altyapısının bağlantıyı sonlandırdığı süreden kısa seçilmelidir. HikariCP kullanımda olan bağlantıyı zorla kapatmaz. Bağlantı havuza döndüğünde emekliye ayırır. "keepaliveTime" yalnız boş bağlantılarda çalışır ve "maxLifetime" değerinden küçük olmalıdır. JDBC4 destekleyen sürücülerde özel "connectionTestQuery" yerine "Connection.isValid()" kullanılması önerilir.

Her "HikariDataSource" uygulama kapanırken kapatılmalıdır. Hedef havuzlar ayrı Spring bean'leri olarak kaydedilmişse container "close()" yaşam döngüsünü yönetebilir. Havuzlar yalnız routing bean'i oluşturulurken yerel nesneler olarak üretilip haritaya konulursa Spring bunları ayrı bean olarak görmez. Bu durumda routing kayıt sınıfı bütün havuzları açık biçimde kapatmalıdır. Spring, kendi yönettiği bean'lerde public "close()" ve "shutdown()" metotlarını varsayılan destroy metodu olarak algılayabilir.

## Lazy bağlantı ve gözlemlenebilirlik

"DataSourceTransactionManager", transaction başlarken bağlantıyı erken alabilir. "LazyConnectionDataSourceProxy", gerçek JDBC bağlantısını ilk "Deyim" oluşturulana kadar erteleyebilir. Bu davranış, SQL çalıştırmayan transaction'ların havuzdan bağlantı tüketmesini önler. Read-only veya isolation tabanlı routing kullanıldığında transaction özelliklerinin hedef seçimine dahil edilmesini de kolaylaştırır.

Proxy sırası önemlidir. "TransactionAwareDataSourceProxy" kullanılacaksa en dış katmanda bulunmalıdır. "LazyConnectionDataSourceProxy" onun altında, routing veri kaynağı ise fiziksel hedeflerin önünde konumlandırılabilir. Kesin sıra kullanılan transaction manager ve erişim tekniğine göre kurulmalıdır. Spring, "TransactionAwareDataSourceProxy" ile lazy proxy birlikte kullanıldığında transaction-aware proxy'nin dışta olmasını ister.

JPA kullanılan sistemlerde routing anahtarının "EntityManager" fiziksel bağlantı almadan önce belirlenmesi gerekir. [Persistence context](/wiki/persistence-context) oluşturulduktan veya ilk sorgu çalıştıktan sonra anahtarı değiştirmek güvenilir bir yönlendirme yöntemi değildir. Aynı "EntityManagerFactory", routing "DataSource" üzerinden çalışabilir, ancak bütün hedef şemaların varlık modeli ve Hibernate beklentileriyle uyumlu olması gerekir.

İzleme yalnız routing veri kaynağı üzerinde yapılmamalıdır. Her Hikari havuzu için en az şu değerler ayrı izlenmelidir:

- Aktif bağlantı
- Boş bağlantı
- Bekleyen iş parçacığı
- Toplam bağlantı
- Bağlantı edinme süresi
- Zaman aşımı sayısı
- Bağlantı kullanım süresi

Spring Boot, veri kaynağı metriklerinde aktif, boş, azami ve asgari bağlantı sayılarını sunabilir ve metrikleri bean adına göre etiketleyebilir. Çok sayıda havuz bulunan sistemlerde "poolName", routing anahtarı ve Oracle servis adı tutarlı olmalıdır. SQL metni veya kullanıcı kimliği gibi yüksek çeşitliliğe sahip değerler metrik etiketi yapılmamalıdır.

Routing hataları için anahtarın kendisi loglanabilir, ancak bağlantı parolası, tam JDBC URL içindeki gizli bilgiler ve kişisel veriler loga yazılmamalıdır. Her işlemde routing anahtarını bilgi seviyesinde loglamak yüksek trafikte gereksiz I/O oluşturabilir. Hata, zaman aşımı ve beklenmeyen fallback olayları ayrı sayaçlarla izlenmelidir.

"AbstractRoutingDataSource" ile HikariCP birlikte kullanıldığında ana tasarım ilkesi nettir. Yönlendirme anahtarı transaction başlamadan önce atanır, transaction boyunca değişmez kabul edilir ve iş tamamlandığında mutlaka temizlenir. Her hedef havuz bağımsız kapasite ve yaşam döngüsüne sahiptir. Toplam bağlantı sınırı bütün havuzların toplamından hesaplanır. Sessiz fallback kapatılır ve yanlış şema seçimi olağan bir çalışma durumu değil, sistem hatası olarak ele alınır.

## Daha geniş veri sistemi bağlamı

Dinamik veri kaynağı yönlendirme connection-pool sınırında çözülse de transaction ve veri erişimi daha geniş bir sistem probleminin parçalarıdır. Bu çerçeveyi [Veri Tabanı Sistemleri ve Veri İşleme](/vtys) sayfasında topluyorum.

## Kaynakça

- Brett Wooldridge et al. (n.d.). HikariCP - High-performance JDBC connection pool. HikariCP project. [URL](https://github.com/brettwooldridge/HikariCP)

- Ron Pressler; Alan Bateman. (2023). JEP 444: [Virtual Threads](/wiki/virtual-thread). OpenJDK. [URL](https://openjdk.org/jeps/444)

- Spring Framework. (2020). AbstractRoutingDataSource - Spring Framework 5.3 API. VMware / Spring. [URL](https://docs.spring.io/spring-framework/docs/5.3.0/javadoc-api/org/springframework/jdbc/datasource/lookup/AbstractRoutingDataSource.html)

## Bu Çalışmaya Atıf

Köker, M. A. (2021). HikariCP ile Dinamik Veri Kaynağı Yönlendirme. alikoker.com.tr. https://alikoker.com.tr/hikaricp-ile-dinamik-veri-kaynagi-yonlendirme

- BibTeX: https://alikoker.com.tr/hikaricp-ile-dinamik-veri-kaynagi-yonlendirme.bib
- RIS: https://alikoker.com.tr/hikaricp-ile-dinamik-veri-kaynagi-yonlendirme.ris
- CSL-JSON: https://alikoker.com.tr/hikaricp-ile-dinamik-veri-kaynagi-yonlendirme.csl.json
