← Kursa Dön
📄 Text · 20 min

HikariCP Connection Pooling

Giriş

Bir restorana gittiğinizi düşünün. Her müşteri geldiğinde sıfırdan yeni bir masa ve sandalye imal etmek ne kadar saçma olurdu, değil mi? Restoran açılmadan önce masalar hazırlanır, müşteriler geldiğinde boş bir masaya oturtulur, yemek bittiğinde masa temizlenir ve bir sonraki müşteriye hazır hale getirilir. İşte connection pool tam olarak bu mantıkla çalışır.

Veritabanı bağlantısı oluşturmak pahalı bir operasyondur. Her bağlantı için TCP handshake, SSL negotiation, authentication ve session initialization adımları gerçekleşir — bu süreç her seferinde 20-100 milisaniye arası sürer. Saniyede yüzlerce istek alan bir uygulamada her istek için yeni bağlantı açıp kapatmak, performansı ciddi şekilde düşürür.

Connection pool, önceden oluşturulmuş bağlantıları bir havuzda tutar ve ihtiyaç duyulduğunda yeniden kullanır. Spring Boot, Java ekosisteminin en hızlı connection pool implementasyonu olan HikariCP'yi varsayılan olarak kullanır. HikariCP ("ışık" anlamında Japonca bir kelime), minimalist tasarımı ve sıfıra yakın overhead'i ile rakiplerinden (C3P0, DBCP2, Tomcat Pool) açık ara öndedir.

Bu derste HikariCP'nin iç mekanizmasını, kritik konfigürasyon parametrelerini, pool boyutu hesaplama formülünü, connection leak detection'ı, monitoring stratejilerini ve multi-datasource yapılandırmasını derinlemesine öğreneceğiz.


Connection Pool Kavramı

Neden Connection Pool?

Veritabanı bağlantısı oluşturma sürecini adım adım inceleyelim:

Uygulama                                Veritabanı
   │                                        │
   │── 1. TCP SYN ─────────────────────────▶│  (network round-trip)
   │◀─ 2. TCP SYN-ACK ─────────────────────│  
   │── 3. TCP ACK ─────────────────────────▶│  (~1-5ms)
   │                                        │
   │── 4. SSL ClientHello ─────────────────▶│  (TLS handshake)
   │◀─ 5. SSL ServerHello + Certificate ───│  
   │── 6. SSL Key Exchange ────────────────▶│  (~10-30ms)
   │◀─ 7. SSL Finished ───────────────────│  
   │                                        │
   │── 8. Authentication Request ──────────▶│  (kullanıcı doğrulama)
   │◀─ 9. Authentication OK ──────────────│  (~5-15ms)
   │                                        │
   │── 10. Session Init (charset, tz) ─────▶│  (oturum başlatma)
   │◀─ 11. Ready for Query ───────────────│  (~2-5ms)
   │                                        │
   Toplam: ~20-100ms (ağ gecikmesine bağlı)

Bu süreç her bağlantı için tekrarlanır. Saniyede 500 istek alan bir uygulama, bağlantı havuzu olmadan saniyede 500 kez bu el sıkışma sürecini yapar — bu da 10-50 saniyelik pür bağlantı kurma süresi demektir. İmkansız!

Connection pool ile aynı senaryo:

İstek geldi → Pool'dan bağlantı al (~0.1ms) → Sorguyu çalıştır → Bağlantıyı pool'a geri ver

Bağlantı oluşturma maliyeti 20-100ms'den 0.1ms'ye düşer. Bu, 200-1000 kat performans iyileştirmesi demektir.

Connection Pool Yaşam Döngüsü

Uygulama başlatılır
     ↓
HikariCP pool oluşturur (minimum-idle kadar bağlantı açılır)
     ↓
İstek gelir → Pool'dan boş bağlantı alınır (borrow)
     ↓
İşlem tamamlanır → Bağlantı pool'a geri verilir (return) — kapatılmaz!
     ↓
Bağlantı idle kalırsa → idle-timeout sonra kapatılır
     ↓
Bağlantı max-lifetime'a ulaşırsa → Kapatılıp yenisi oluşturulur
     ↓
Uygulama kapanır → Tüm bağlantılar kapatılır

💡 İpucu: connection.close() çağırdığınızda bağlantı gerçekten kapatılmaz — pool'a geri verilir. HikariCP, JDBC Connection nesnesini bir proxy ile sararak close() metodunu override eder.


HikariCP Konfigürasyonu

Temel Parametreler

HikariCP'nin her parametresini anlamak, production ortamında sağlıklı bir uygulama çalıştırmak için zorunludur:

# ═══════════════════════════════════════════════
# POOL BOYUTU — En kritik ayarlar!
# ═══════════════════════════════════════════════

# Maksimum bağlantı sayısı (havuzdaki toplam bağlantı üst sınırı)
spring.datasource.hikari.maximum-pool-size=10

# Boşta tutulacak minimum bağlantı sayısı
# ÖNERİ: minimum-idle = maximum-pool-size yapın → sabit pool boyutu, daha tahmin edilebilir
spring.datasource.hikari.minimum-idle=10

# ═══════════════════════════════════════════════
# TIMEOUT AYARLARI
# ═══════════════════════════════════════════════

# Bağlantı almak için maksimum bekleme süresi (ms)
# Pool doluysa bu süre kadar bekler, sonra SQLTransientConnectionException fırlatır
spring.datasource.hikari.connection-timeout=30000

# Boş bağlantının pool'da kalma süresi (ms)
# minimum-idle < maximum-pool-size olduğunda geçerlidir
spring.datasource.hikari.idle-timeout=600000

# Bir bağlantının maksimum yaşam süresi (ms)
# Veritabanı tarafındaki timeout'tan 30-60 saniye KISA olmalı
spring.datasource.hikari.max-lifetime=1800000

# Bağlantı geçerlilik testi zaman aşımı (ms)
spring.datasource.hikari.validation-timeout=5000

# ═══════════════════════════════════════════════
# LEAK DETECTION — Bağlantı sızıntısı tespiti
# ═══════════════════════════════════════════════

# Bağlantı bu süreden fazla tutulursa WARNING loglanır
# 0 = devre dışı, önerilen: 60000ms (60 saniye)
spring.datasource.hikari.leak-detection-threshold=60000

# ═══════════════════════════════════════════════
# DİĞER AYARLAR
# ═══════════════════════════════════════════════

# Pool ismi (monitoring ve loglarda görünür)
spring.datasource.hikari.pool-name=MyApp-HikariPool

# Yeni bağlantıda çalıştırılacak SQL
spring.datasource.hikari.connection-init-sql=SET NAMES utf8mb4

# Auto-commit davranışı
spring.datasource.hikari.auto-commit=true

# Read-only modu (replica datasource için)
spring.datasource.hikari.read-only=false

Parametrelerin Etkileşimi

Bu parametrelerin birbirleriyle nasıl etkileştiğini anlamak kritiktir:

maximum-pool-size = 10
minimum-idle = 10 (sabit pool → idle-timeout etkisiz)

Pool dolu ve yeni istek geldi:
  → connection-timeout kadar bekle (30 saniye)
  → Hâlâ müsait bağlantı yoksa → Exception!

Bağlantı 30 dakikadır (max-lifetime) açık:
  → HikariCP bağlantıyı nazikçe kapatır
  → Yerine yeni bağlantı açar (soft eviction)
  → Bağlantı aktif kullanımdaysa → kullanım bitene kadar bekler

Bağlantı 60 saniyeden fazla checkout'ta:
  → leak-detection-threshold tetiklenir
  → WARNING log + stack trace yazılır
  → Bağlantı GERİ ALINMAZ — sadece uyarı verilir

Pool Boyutu: Altın Formül

Pool boyutu belirlemek, en kritik konfigürasyon kararıdır. Daha fazla bağlantı = daha iyi performans düşüncesi tamamen yanlıştır. PostgreSQL wiki'sinden gelen formül:

Optimal pool size = (core_count × 2) + effective_spindle_count
  • core_count = Fiziksel CPU çekirdek sayısı (hyper-thread dahil değil)

  • effective_spindle_count = SSD ise 1, HDD ise disk sayısı

Örnek: 4 çekirdek CPU, SSD disk:

Pool size = (4 × 2) + 1 = 9 ≈ 10

Neden Bu Kadar Az?

Bu sayı sizi şaşırtabilir. "10 bağlantı ile 10.000 kullanıcıya nasıl hizmet veririz?" diye düşünebilirsiniz. Cevap, bağlantı kullanım süresinin çok kısa olmasında:

Tipik bir veritabanı sorgusu:
  Sorgu gönder → ~0.5ms
  Sorgu çalıştır → ~2ms  
  Sonuç al     → ~0.5ms
  Toplam       → ~3ms

1 bağlantı, 1 saniyede: 1000ms / 3ms = ~333 sorgu çalıştırabilir
10 bağlantı, 1 saniyede: 3.330 sorgu!

Çoğu web isteği 1-3 veritabanı sorgusu yapar. Yani 10 bağlantı ile saniyede 1.000-3.000 HTTP isteğini rahatlıkla karşılayabilirsiniz.

Çok Büyük Pool Neden Kötü?

❌ maximum-pool-size = 100

Veritabanı sunucusu perspektifi:
  - 100 aktif bağlantı = 100 eşzamanlı thread
  - Her thread CPU zamanı ister → context switching patlar
  - Diskten okuma sırasında diğer thread'ler bekler → lock contention artar
  - Sorgu süresi 3ms'den 30ms'ye çıkar (10x yavaşlama!)
  - Toplam throughput DÜŞER

✅ maximum-pool-size = 10

  - 10 bağlantı veritabanını rahat tutar
  - Minimum context switching
  - Sorgular hızlı çalışır (3ms)
  - Uygulama tarafı bağlantı bekler ama TOPLAM throughput ARTAR

⚠️ Dikkat: Bu formül tek uygulama instance'ı içindir. 5 instance çalışıyorsanız, her biri 10 bağlantı = toplamda 50 bağlantı. Veritabanının max_connections limitini aşmadığınızdan emin olun!

Farklı Senaryolar İçin Pool Boyutu

// CPU-bound uygulamalar (hesaplama ağırlıklı)
// Pool size = CPU çekirdek sayısı (daha az bağlantı yeterli)
spring.datasource.hikari.maximum-pool-size=4  // 4 çekirdek CPU

// I/O-bound uygulamalar (çok sayıda dış servis çağrısı)
// Pool size = CPU × 2 + disk sayısı
spring.datasource.hikari.maximum-pool-size=10  // 4 çekirdek, SSD

// Uzun süren sorgular (rapor, batch işlem)
// Ayrı bir pool tanımlamayı düşünün
// Ana pool: 10, Rapor pool: 3

Connection Leak Detection

Connection leak (bağlantı sızıntısı), havuzdan alınan bir bağlantının geri verilmemesidir. Bu, havuzun yavaş yavaş tükenmesine ve sonunda uygulamanın çökmesine yol açar. Restoran analojisine dönersek: müşteri yemeğini bitirir ama masadan kalkmaz — yeni müşteriler için masa kalmaz.

Leak'in Oluşma Nedenleri

// ❌ LEAK — Bağlantı asla geri verilmiyor!
@Service
public class BadService {
    
    @Autowired
    private DataSource dataSource;
    
    public String getData() throws SQLException {
        Connection conn = dataSource.getConnection();  // Pool'dan alındı
        Statement stmt = conn.createStatement();
        ResultSet rs = stmt.executeQuery("SELECT name FROM users WHERE id = 1");
        rs.next();
        return rs.getString("name");
        // conn.close() UNUTULDU! → LEAK
        // Connection pool'a geri verilmedi
        // Her çağrıda bir bağlantı "kaybolur"
    }
}

Bu metot her çağrıldığında bir bağlantı sızar. 10 bağlantılık pool'da 10 çağrıdan sonra pool tükenir ve tüm uygulama durur.

Doğru Kullanım: try-with-resources

// ✅ DOĞRU — try-with-resources ile otomatik kapanma
@Service
public class GoodService {
    
    @Autowired
    private DataSource dataSource;
    
    public String getData() throws SQLException {
        try (Connection conn = dataSource.getConnection();
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery("SELECT name FROM users WHERE id = 1")) {
            rs.next();
            return rs.getString("name");
        }
        // try bloğu bittiğinde conn, stmt, rs otomatik kapatılır
        // conn.close() → aslında pool'a geri verilir
    }
}

En İyi Yaklaşım: Spring/JPA'ya Bırakın

// ✅ EN DOĞRU — Connection yönetimini Spring'e bırakın
@Service
@RequiredArgsConstructor
public class BestService {
    
    private final UserRepository userRepository;
    
    @Transactional(readOnly = true)
    public String getUserName(Long id) {
        return userRepository.findById(id)
            .map(User::getName)
            .orElseThrow(() -> new UserNotFoundException(id));
        // Spring, @Transactional sınırlarında bağlantıyı otomatik yönetir
        // Transaction commit/rollback olduğunda bağlantı pool'a geri verilir
    }
}

Leak Detection Log Çıktısı

Leak detection aktifken, bağlantı belirtilen süreden fazla tutulursa şu log çıktısını görürsünüz:

WARN  HikariPool-1 - Connection leak detection triggered for 
  com.zaxxer.hikari.pool.ProxyConnection@1a2b3c4d on thread http-nio-8080-exec-5, 
  stack trace follows:
  java.lang.Exception: Apparent connection leak detected
    at com.example.service.BadService.getData(BadService.java:15)
    at com.example.controller.UserController.getUser(UserController.java:28)
    at sun.reflect.NativeMethodAccessorImpl.invoke(...)
    ...

Bu stack trace, leak'in tam olarak hangi satırda oluştuğunu gösterir — production'da hayat kurtaran bir özelliktir.


max-lifetime ve Veritabanı Timeout İlişkisi

max-lifetime parametresi, HikariCP'nin en çok yanlış yapılandırılan ayarlarından biridir. Bu değer, veritabanı tarafındaki bağlantı timeout'undan mutlaka kısa olmalıdır.

Problem Senaryosu

Veritabanı (MySQL):  wait_timeout = 28800 (8 saat)
HikariCP:            max-lifetime = 36000000 (10 saat) ← YANLIŞ!

Ne olur?
1. Bağlantı 8 saat açık kalır
2. MySQL, 8 saatte bağlantıyı KESER (wait_timeout)
3. HikariCP hâlâ bağlantıyı "aktif" sanır (max-lifetime dolmadı)
4. Uygulama bu "ölü" bağlantıyı kullanmaya çalışır
5. "Communications link failure" hatası!

Doğru Yapılandırma

# MySQL wait_timeout = 28800 saniye (8 saat)
# HikariCP max-lifetime = 25200000 ms (7 saat) — 1 saat kısa
spring.datasource.hikari.max-lifetime=25200000

# PostgreSQL idle_in_transaction_session_timeout'a da dikkat edin
# Genel kural: max-lifetime = DB timeout - 60 saniye (en az)
// max-lifetime'ın doğru çalıştığını test eden kod
@Component
@Slf4j
public class ConnectionLifetimeVerifier {
    
    @Autowired
    private DataSource dataSource;
    
    @Scheduled(fixedRate = 3600000) // Her saat
    public void verifyConnections() {
        if (dataSource instanceof HikariDataSource hikari) {
            HikariPoolMXBean pool = hikari.getHikariPoolMXBean();
            log.info("Connection pool status: total={}, active={}, idle={}, waiting={}",
                pool.getTotalConnections(),
                pool.getActiveConnections(),
                pool.getIdleConnections(),
                pool.getThreadsAwaitingConnection());
        }
    }
}

Programmatic Monitoring

JMX Bean ile Pool İzleme

HikariCP, pool durumunu JMX (Java Management Extensions) üzerinden expose eder. Spring Boot Actuator ile birlikte güçlü monitoring imkânı sunar:

@RestController
@RequestMapping("/admin/pool")
@RequiredArgsConstructor
public class PoolMonitorController {
    
    private final DataSource dataSource;
    
    @GetMapping("/stats")
    public Map<String, Object> getPoolStats() {
        HikariDataSource hikari = (HikariDataSource) dataSource;
        HikariPoolMXBean pool = hikari.getHikariPoolMXBean();
        
        Map<String, Object> stats = new LinkedHashMap<>();
        stats.put("poolName", hikari.getPoolName());
        stats.put("maximumPoolSize", hikari.getMaximumPoolSize());
        stats.put("minimumIdle", hikari.getMinimumIdle());
        stats.put("activeConnections", pool.getActiveConnections());
        stats.put("idleConnections", pool.getIdleConnections());
        stats.put("totalConnections", pool.getTotalConnections());
        stats.put("threadsAwaitingConnection", pool.getThreadsAwaitingConnection());
        
        // Uyarı durumları
        double utilizationRate = (double) pool.getActiveConnections() 
            / hikari.getMaximumPoolSize() * 100;
        stats.put("utilizationPercent", String.format("%.1f%%", utilizationRate));
        
        if (pool.getThreadsAwaitingConnection() > 0) {
            stats.put("WARNING", "Threads are waiting for connections! " +
                "Consider increasing pool size or optimizing queries.");
        }
        
        return stats;
    }
}

Micrometer Metrics (Otomatik)

Spring Boot Actuator + Micrometer ile HikariCP metrikleri otomatik olarak expose edilir:

management.endpoints.web.exposure.include=health,metrics,prometheus
Otomatik olarak mevcut metrikler:

hikaricp.connections           = toplam bağlantı sayısı
hikaricp.connections.active    = kullanımda olan bağlantılar
hikaricp.connections.idle      = boşta bekleyen bağlantılar
hikaricp.connections.pending   = bağlantı bekleyen thread sayısı
hikaricp.connections.max       = maximum pool size
hikaricp.connections.min       = minimum idle
hikaricp.connections.creation  = bağlantı oluşturma süresi (Timer)
hikaricp.connections.acquire   = bağlantı alma süresi (Timer)
hikaricp.connections.usage     = bağlantı kullanım süresi (Timer)
hikaricp.connections.timeout   = timeout olan istek sayısı (Counter)

Custom Alert Sistemi

@Component
@Slf4j
public class ConnectionPoolAlertService {
    
    private final HikariDataSource dataSource;
    private final MeterRegistry meterRegistry;
    
    public ConnectionPoolAlertService(DataSource dataSource, MeterRegistry meterRegistry) {
        this.dataSource = (HikariDataSource) dataSource;
        this.meterRegistry = meterRegistry;
    }
    
    @Scheduled(fixedRate = 10000) // Her 10 saniyede bir
    public void checkPoolHealth() {
        HikariPoolMXBean pool = dataSource.getHikariPoolMXBean();
        
        int active = pool.getActiveConnections();
        int max = dataSource.getMaximumPoolSize();
        int waiting = pool.getThreadsAwaitingConnection();
        double utilization = (double) active / max;
        
        // %80 üzeri kullanım → WARNING
        if (utilization > 0.8) {
            log.warn("⚠️ Connection pool utilization HIGH: {}/{} ({}%)",
                active, max, (int)(utilization * 100));
            meterRegistry.counter("pool.alert", "level", "warning").increment();
        }
        
        // Bekleyen thread var → CRITICAL
        if (waiting > 0) {
            log.error("🚨 {} threads waiting for database connection!", waiting);
            meterRegistry.counter("pool.alert", "level", "critical").increment();
        }
        
        // Pool tamamen dolu → EMERGENCY
        if (active >= max) {
            log.error("🔥 Connection pool EXHAUSTED! All {} connections in use. " +
                "Application may become unresponsive!", max);
        }
    }
}

Multi-DataSource Konfigürasyonu

Production ortamlarında okuma ve yazma trafiğini farklı veritabanlarına yönlendirmek (read/write splitting) yaygın bir performans optimizasyonudur. Master veritabanına yazma, replica veritabanlarından okuma yaparsınız.

Routing DataSource

// 1. DataSource tipi enum
public enum DataSourceType {
    WRITER, READER
}

// 2. Routing DataSource — ThreadLocal üzerinden seçim yapar
public class RoutingDataSource extends AbstractRoutingDataSource {
    
    private static final ThreadLocal<DataSourceType> currentDataSource = 
        ThreadLocal.withInitial(() -> DataSourceType.WRITER);
    
    @Override
    protected Object determineCurrentLookupKey() {
        return currentDataSource.get();
    }
    
    public static void setDataSourceType(DataSourceType type) {
        currentDataSource.set(type);
    }
    
    public static void clear() {
        currentDataSource.remove();
    }
}

// 3. Konfigürasyon
@Configuration
public class DataSourceConfig {
    
    @Bean("writerDataSource")
    @ConfigurationProperties("spring.datasource.writer.hikari")
    public DataSource writerDataSource() {
        return DataSourceBuilder.create()
            .type(HikariDataSource.class)
            .build();
    }
    
    @Bean("readerDataSource")
    @ConfigurationProperties("spring.datasource.reader.hikari")
    public DataSource readerDataSource() {
        return DataSourceBuilder.create()
            .type(HikariDataSource.class)
            .build();
    }
    
    @Primary
    @Bean
    public DataSource routingDataSource(
            @Qualifier("writerDataSource") DataSource writer,
            @Qualifier("readerDataSource") DataSource reader) {
        
        RoutingDataSource routing = new RoutingDataSource();
        routing.setTargetDataSources(Map.of(
            DataSourceType.WRITER, writer,
            DataSourceType.READER, reader
        ));
        routing.setDefaultTargetDataSource(writer);
        return routing;
    }
}

application.yml Yapılandırması

spring:
  datasource:
    writer:
      hikari:
        jdbc-url: jdbc:postgresql://master-db:5432/myapp
        username: app_writer
        password: ${DB_WRITER_PASSWORD}
        maximum-pool-size: 10
        minimum-idle: 10
        pool-name: Writer-Pool
        max-lifetime: 1800000
        leak-detection-threshold: 60000
    
    reader:
      hikari:
        jdbc-url: jdbc:postgresql://replica-db:5432/myapp
        username: app_reader
        password: ${DB_READER_PASSWORD}
        maximum-pool-size: 15       # Okuma trafiği genelde daha yoğun
        minimum-idle: 15
        pool-name: Reader-Pool
        read-only: true             # Replica'ya yazma engellenir
        max-lifetime: 1800000

AOP ile Otomatik Routing

// @ReadOnly anotasyonu ile otomatik routing
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional(readOnly = true)
public @interface ReadOnly {
}

@Aspect
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class DataSourceRoutingAspect {
    
    @Before("@annotation(readOnly) || @within(readOnly)")
    public void setReadDataSource(ReadOnly readOnly) {
        RoutingDataSource.setDataSourceType(DataSourceType.READER);
    }
    
    @After("@annotation(readOnly) || @within(readOnly)")
    public void clearDataSource(ReadOnly readOnly) {
        RoutingDataSource.clear();
    }
}

// Kullanım
@Service
public class ProductService {
    
    @ReadOnly  // Otomatik olarak replica'dan okur
    public List<Product> searchProducts(String query) {
        return productRepository.findByNameContaining(query);
    }
    
    @Transactional  // Master'a yazar (varsayılan)
    public Product createProduct(ProductDto dto) {
        return productRepository.save(mapToEntity(dto));
    }
}

Yaygın Hatalar ve Çözümleri

1. Pool Boyutunu Çok Büyük Yapmak

# ❌ YANLIŞ — "Daha fazla bağlantı = daha hızlı" değil!
spring.datasource.hikari.maximum-pool-size=100

# ✅ DOĞRU — Formülü kullanın
spring.datasource.hikari.maximum-pool-size=10

2. max-lifetime'ı DB Timeout'undan Büyük Yapmak

# ❌ YANLIŞ — DB 8 saatte bağlantıyı keser ama HikariCP farkında değil
spring.datasource.hikari.max-lifetime=36000000  # 10 saat

# ✅ DOĞRU — DB timeout'undan 30-60 saniye kısa
spring.datasource.hikari.max-lifetime=25200000  # 7 saat (MySQL wait_timeout=8h için)

3. minimum-idle'ı 0 Yapmak

# ❌ YANLIŞ — İlk istekler bağlantı oluşturulana kadar bekler (cold start)
spring.datasource.hikari.minimum-idle=0

# ✅ DOĞRU — Sabit pool boyutu (öngörülebilir performans)
spring.datasource.hikari.minimum-idle=10
spring.datasource.hikari.maximum-pool-size=10

4. Leak Detection'ı Kapatmak

# ❌ YANLIŞ — Leak'i fark etmezsiniz, pool tükenir, uygulama çöker
spring.datasource.hikari.leak-detection-threshold=0

# ✅ DOĞRU — Production'da mutlaka aktif edin
spring.datasource.hikari.leak-detection-threshold=60000

5. Connection Test Query Kullanmak

# ❌ GEREKSİZ — JDBC4+ driver'larda HikariCP otomatik isValid() çağırır
spring.datasource.hikari.connection-test-query=SELECT 1

# ✅ DOĞRU — Bu satırı kaldırın, HikariCP kendi mekanizmasını kullansın
# connection-test-query kullanmak her bağlantı alımında ekstra sorgu çalıştırır

6. @Transactional Scope'unu Geniş Tutmak

// ❌ YANLIŞ — Bağlantı gereksiz yere uzun süre tutulur
@Transactional
public void processOrder(OrderRequest request) {
    Order order = orderRepository.save(createOrder(request));
    
    // Bu HTTP çağrısı 2-5 saniye sürebilir!
    // Bu süre boyunca veritabanı bağlantısı TUTULUR
    PaymentResult payment = paymentClient.charge(order);  
    
    // Bu e-posta gönderimi 1-3 saniye sürebilir!
    emailService.sendConfirmation(order);
    
    order.setStatus(OrderStatus.COMPLETED);
    orderRepository.save(order);
}

// ✅ DOĞRU — Transaction scope'unu minimize edin
public void processOrder(OrderRequest request) {
    // 1. Veritabanı işlemi (kısa transaction)
    Order order = createOrderInTransaction(request);
    
    // 2. Harici çağrılar (transaction dışında)
    PaymentResult payment = paymentClient.charge(order);
    
    // 3. Güncelleme (kısa transaction)
    updateOrderStatus(order, OrderStatus.COMPLETED);
    
    // 4. Bildirim (asenkron, transaction dışında)
    eventPublisher.publishEvent(new OrderCompletedEvent(order));
}

@Transactional
protected Order createOrderInTransaction(OrderRequest request) {
    return orderRepository.save(createOrder(request));
}

Gerçek Dünya Örneği: E-Ticaret Connection Pool Stratejisi

Büyük bir e-ticaret uygulaması düşünün. Farklı iş yükleri için farklı pool stratejileri gerekir:

@Configuration
public class ECommerceDataSourceConfig {
    
    // Ana uygulama pool'u — CRUD işlemleri
    @Bean("primaryDataSource")
    @ConfigurationProperties("app.datasource.primary.hikari")
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create()
            .type(HikariDataSource.class)
            .build();
    }
    
    // Rapor pool'u — uzun süren sorgular
    @Bean("reportDataSource")
    @ConfigurationProperties("app.datasource.report.hikari")
    public DataSource reportDataSource() {
        return DataSourceBuilder.create()
            .type(HikariDataSource.class)
            .build();
    }
    
    // Batch pool'u — toplu işlemler
    @Bean("batchDataSource")
    @ConfigurationProperties("app.datasource.batch.hikari")
    public DataSource batchDataSource() {
        return DataSourceBuilder.create()
            .type(HikariDataSource.class)
            .build();
    }
}
app:
  datasource:
    primary:
      hikari:
        jdbc-url: jdbc:postgresql://master:5432/ecommerce
        maximum-pool-size: 10
        minimum-idle: 10
        connection-timeout: 10000      # 10 saniye — hızlı yanıt bekleniyor
        leak-detection-threshold: 30000 # 30 saniye — kısa sorgular
        pool-name: Primary-Pool
    
    report:
      hikari:
        jdbc-url: jdbc:postgresql://replica:5432/ecommerce
        maximum-pool-size: 3           # Az bağlantı — uzun sorgular
        minimum-idle: 1
        connection-timeout: 60000      # 60 saniye — raporlar bekleyebilir
        leak-detection-threshold: 300000 # 5 dakika — raporlar uzun sürer
        pool-name: Report-Pool
        read-only: true
    
    batch:
      hikari:
        jdbc-url: jdbc:postgresql://master:5432/ecommerce
        maximum-pool-size: 5
        minimum-idle: 2
        connection-timeout: 30000
        leak-detection-threshold: 600000 # 10 dakika — batch işlemler
        pool-name: Batch-Pool
// Servisler uygun pool'u kullanır
@Service
public class ReportService {
    
    private final JdbcTemplate reportJdbc;
    
    public ReportService(@Qualifier("reportDataSource") DataSource dataSource) {
        this.reportJdbc = new JdbcTemplate(dataSource);
    }
    
    public SalesReport generateMonthlySalesReport(YearMonth month) {
        // Bu sorgu 30 saniye - 2 dakika sürebilir
        // report pool kullandığı için ana pool'u ETKİLEMEZ
        return reportJdbc.queryForObject(
            "SELECT ... FROM orders WHERE ... GROUP BY ...",
            new SalesReportRowMapper(),
            month.atDay(1), month.atEndOfMonth()
        );
    }
}

Özet

  • HikariCP, Spring Boot'un varsayılan ve en hızlı connection pool implementasyonudur. Her veritabanı bağlantısı 20-100ms maliyetlidir; pool bu maliyeti 0.1ms'ye düşürür.

  • Pool boyutu formülü: (CPU × 2) + 1. Çoğu uygulama için 10 bağlantı yeterlidir. Daha fazla bağlantı performansı artırmaz, aksine düşürür (context switching).

  • max-lifetime, veritabanı tarafındaki timeout'tan mutlaka kısa olmalıdır. Aksi halde "ölü bağlantı" sorunu yaşarsınız.

  • Leak detection production'da mutlaka aktif olmalı. leak-detection-threshold=60000 ayarı ile bağlantı sızıntılarını erken yakalayın.

  • Transaction scope'unu minimize edin — harici servis çağrıları, e-posta gönderimi gibi uzun süren işlemleri transaction dışına taşıyın.

  • Farklı iş yükleri (CRUD, rapor, batch) için ayrı pool'lar tanımlayın. Bir rapor sorgusunun tüm uygulamayı bloke etmesini önleyin.

  • Monitoring kritiktir — Actuator metrikleri ile pool kullanımını sürekli izleyin. hikaricp.connections.pending > 0 ise pool yetersizdir.