• 28.08.2026 09:07:24
  • Admin Admin

Spring Boot uygulamalarında tenant bazlı veri kaynağı yönlendirmesini, transaction sırasında bağlantı sabitlenmesi sorununu, Spring Security doğrulamasını ve HikariCP metrikleriyle kapasite ölçümünü ele alır.

Spring Boot'ta Çoklu Kiracı Veri Kaynağı ve Transaction Sınırları

Spring Framework eğitimi bağlamında çoklu kiracı sınırını doğru seçmek

Çoklu kiracı tasarımında ilk karar, izolasyon birimidir: shared table, schema per tenant veya database per tenant. Shared table modelinde her sorguya tenant_id eklemek gerekir; PostgreSQL kullanılıyorsa bu kontrolü yalnızca repository katmanına bırakmak yerine Row Level Security ile veritabanına da koyun. Aksi halde native query, batch job veya yanlış yazılmış bir admin endpoint'i uygulama filtresini atlayabilir. Örneğin uygulamanın kullandığı rol için RLS politikası aşağıdaki gibi kurulabilir.

ALTER TABLE invoice ENABLE ROW LEVEL SECURITY;

CREATE POLICY invoice_tenant_policy ON invoice
USING (tenant_id = current_setting('app.tenant_id', true)::uuid)
WITH CHECK (tenant_id = current_setting('app.tenant_id', true)::uuid);
Bu yaklaşımda bağlantı havuzundan alınan her PostgreSQL bağlantısında SET LOCAL app.tenant_id çalıştırılmalıdır. SET yerine SET LOCAL kullanımı kritiktir: transaction bittiğinde değer temizlenir; aksi durumda HikariCP'nin aynı fiziksel bağlantıyı başka tenant'a vermesi veri sızıntısına yol açabilir.

Database per tenant, müşteri başına geri yükleme, şifreleme anahtarı veya bölgesel saklama zorunluluğu olan sistemlerde daha güçlü izolasyon sağlar; fakat 500 tenant için her biri 10 bağlantılı havuz açmak 5.000 açık bağlantı demektir. PostgreSQL'in max_connections limiti, proxy katmanının kapasitesi ve veritabanı belleği birlikte hesaplanmalıdır. Bu nedenle bir spring framework eğitimi veya java spring eğitimi sırasında AbstractRoutingDataSource örneğini sadece bir tasarım kalıbı olarak değil, havuz sayısı ve bağlantı bütçesiyle birlikte değerlendirmek gerekir. Başlangıç hesabı olarak her tenant havuzu için maximumPoolSize x aktif uygulama pod sayısı toplamının, veritabanı bağlantı limitinin yüzde 70'ini geçmemesini hedefleyin; kalan kapasite migration, yönetim bağlantıları ve ani trafik için gerekir.

Spring Boot'ta AbstractRoutingDataSource ve transaction zamanlaması

AbstractRoutingDataSource, bağlantı alınırken determineCurrentLookupKey sonucuna bakar. Kritik ayrıntı şudur: Spring transaction başlangıcında DataSourceTransactionManager veya JPA transaction yöneticisi Connection'ı thread'e bağlayabilir. TenantContext'i @Transactional metodunun içinde değiştirmek geç kalmaktır; ilk JDBC erişimi eski tenant bağlantısıyla yapılmış olabilir. Tenant anahtarını servlet filtresinde, controller ve transaction interceptor'larından önce ayarlayın.

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

  public static void set(String tenant) { CURRENT.set(tenant); }
  public static String getRequired() {
    String tenant = CURRENT.get();
    if (tenant == null) throw new IllegalStateException("Tenant yok");
    return tenant;
  }
  public static void clear() { CURRENT.remove(); }
}

public final class TenantRoutingDataSource extends AbstractRoutingDataSource {
  @Override
  protected Object determineCurrentLookupKey() {
    return TenantContext.getRequired();
  }
}
ThreadLocal.remove() çağrısı, Tomcat işçi thread'lerinin tekrar kullanılması nedeniyle zorunludur. clear yerine set(null) yazmak çoğu durumda değeri kaldırsa da ThreadLocal haritasındaki stale entry davranışına güvenmeyin.

Filtre sırası da test edilmelidir. Tenant bilgisini doğrudan X-Tenant-Id başlığından kabul etmek, saldırganın başka tenant'a geçebilmesine neden olur. Spring Security tarafından doğrulanmış JWT içindeki tenant_id claim'i ile istek başlığını karşılaştırın veya başlığı tamamen kaldırın. Aşağıdaki OncePerRequestFilter, SecurityContext içindeki JwtAuthenticationToken'ı kullanır ve context'i finally bloğunda temizler.

@Component
@Order(Ordered.LOWEST_PRECEDENCE - 20)
final class TenantFilter extends OncePerRequestFilter {
  @Override
  protected void doFilterInternal(HttpServletRequest request,
      HttpServletResponse response, FilterChain chain)
      throws ServletException, IOException {
    Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    if (!(auth instanceof JwtAuthenticationToken jwt)) {
      response.sendError(401, "JWT gerekli");
      return;
    }
    String tenant = jwt.getToken().getClaimAsString("tenant_id");
    if (tenant == null || !tenant.matches("[a-z0-9-]{3,64}")) {
      response.sendError(403, "Geçersiz tenant");
      return;
    }
    try {
      TenantContext.set(tenant);
      chain.doFilter(request, response);
    } finally {
      TenantContext.clear();
    }
  }
}
spring security filtre zincirinde bu filtrenin JWT doğrulamasından sonra çalıştığını MockMvc yerine gerçek SecurityFilterChain ile yapılan entegrasyon testiyle doğrulayın.

Spring MVC, async işler ve Spring Cloud çağrılarında tenant taşınması

spring mvc uygulamasında ThreadLocal tabanlı context, @Async ile otomatik taşınmaz. Servlet isteği tamamlandıktan sonra havuzdaki başka bir thread'e geçen iş, tenant bilgisini kaybeder veya kötü bir temizleme yüzünden önceki değeri görebilir. TaskDecorator ile context'i submit anında kopyalayın ve worker tamamlandığında önceki değeri geri yükleyin.

@Bean
TaskDecorator tenantTaskDecorator() {
  return task -> {
    String capturedTenant = TenantContext.getRequired();
    return () -> {
      try {
        TenantContext.set(capturedTenant);
        task.run();
      } finally {
        TenantContext.clear();
      }
    };
  };
}

@Bean(name = "tenantExecutor")
ThreadPoolTaskExecutor tenantExecutor(TaskDecorator tenantTaskDecorator) {
  ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
  executor.setCorePoolSize(8);
  executor.setMaxPoolSize(32);
  executor.setTaskDecorator(tenantTaskDecorator);
  executor.initialize();
  return executor;
}
Reactor tabanlı akışlarda ThreadLocal yerine Reactor Context kullanın; publishOn sonrasında thread değişimi normaldir. Aynı kod tabanında MVC ve WebFlux karıştırılıyorsa ThreadLocal'i Reactor operatörlerinden okumak, yük altında rastlantısal tenant hatası üretir.

microservices mimarisi içinde tenant bilgisini downstream servise iletmek de ayrı bir sınırdır. spring cloud bileşenleri servis keşfi ve yük dengeleme sağlayabilir, ancak iş alanı bağlamını kendiliğinden güvenilir biçimde taşımaz. spring rest api çağrılarında WebClient filtresiyle imzalanmış servis kimliği ve tenant başlığını birlikte iletin; downstream servis yine kendi JWT veya servis token doğrulamasını yapmalıdır. Salt X-Tenant-Id iletimi yetkilendirme kanıtı değildir.

@Bean
WebClient tenantWebClient(WebClient.Builder builder) {
  return builder.filter((request, next) -> {
    ClientRequest enriched = ClientRequest.from(request)
      .header("X-Tenant-Id", TenantContext.getRequired())
      .build();
    return next.exchange(enriched);
  }).build();
}
Retry mekanizması eklenirse POST benzeri yan etkili çağrılarda tenant başlığının her denemede aynı kaldığını ve idempotency key'in tenant kapsamlı üretildiğini test edin. Aksi halde iki tenant aynı idempotency anahtarını kullanabilir.

HikariCP ile önce-sonra profil çıkarma ve havuz kapasitesi ayarlama

Havuz boyutunu tahminle değiştirmeyin. Önce Micrometer üzerinden hikaricp.connections.active, hikaricp.connections.pending, hikaricp.connections.timeout ve http.server.requests metriklerini Prometheus'a aktarın. Tenant etiketi eklemek cazip görünür, fakat binlerce tenant cardinality patlaması üretir; metrikte tenant yerine pool adı veya tenant planı gibi sınırlı değerler kullanın. Hikari pool adlarını görünür kılan ayar örneği şöyledir.

spring.datasource.hikari.pool-name=tenant-default
spring.datasource.hikari.maximum-pool-size=12
spring.datasource.hikari.minimum-idle=2
spring.datasource.hikari.connection-timeout=500
spring.datasource.hikari.max-lifetime=1740000
management.metrics.enable.hikaricp=true
maxLifetime değerini veritabanı veya ağ katmanındaki bağlantı sonlandırma süresinden daha kısa tutun. Örneğin yük dengeleyici 30 dakikada idle TCP bağlantısını kapatıyorsa 29 dakika seçmek, Hikari'nin ölü bağlantıyı istek anında değil önceden yenilemesini sağlar.

Karşılaştırma için k6 ile aynı veri seti ve aynı pod sayısında iki yük testi çalıştırın: önce mevcut pool ayarıyla, sonra maximum-pool-size=12 ve connection-timeout=500 ile. Test raporunda yalnızca ortalama gecikmeyi değil p95, p99, hata oranı, hikaricp.connections.pending maksimumu ve PostgreSQL pg_stat_activity bağlantı sayısını kaydedin. Örnek komut:

k6 run --vus 80 --duration 10m tenant-read-write.js
psql "$DATABASE_URL" -c "select state, count(*) from pg_stat_activity group by state;"
Eğer ikinci çalışmada p99 düşerken pending sürekli sıfıra iniyor fakat PostgreSQL CPU yüzde 90'a çıkıyorsa havuz büyütmek yalnızca daha fazla eşzamanlı sorguyu veritabanına itmiştir. Bu durumda sorgu planını EXPLAIN (ANALYZE, BUFFERS) ile inceleyin, eksik indeksi ekleyin veya endpoint başına eşzamanlılık limiti koyun.

Bir spring boot eğitimi ya da spring boot kursu kapsamında sık görülen hata, her dinamik tenant için uygulama çalışırken yeni HikariDataSource üretip hiç kapatmamaktır. Tenant silindiğinde ya da uzun süre kullanılmadığında close çağırmayan bir registry, hem housekeeper thread'lerini hem de açık soketleri biriktirir. Caffeine cache kullanıyorsanız removalListener içinde DataSource.close çalıştırın ve eviction sonrası aktif transaction olmadığını yönetsel akışta garanti edin; çalışan bir transaction'ın havuzunu kapatmak bağlantı hatasına neden olur.

Sık Sorulan Sorular

Spring Boot'ta AbstractRoutingDataSource neden @Transactional metodunun içinde tenant değiştiremez?

Transaction interceptor'ı ilk JDBC erişiminde Connection'ı thread'e bağlayabilir. Tenant anahtarını metodun içinde değiştirirseniz routing tekrar çalışmayabilir ve sorgu önceki bağlantıda yürür. TenantContext'i SecurityFilterChain sonrasında, controller'dan önce ayarlayın; bunu iki farklı tenant ile aynı servis metodunu çağıran entegrasyon testiyle doğrulayın.

Spring Security ile X-Tenant-Id başlığı güvenli şekilde nasıl doğrulanır?

Başlığı yetkilendirme kaynağı kabul etmeyin. Doğrulanmış JWT'nin tenant_id claim'ini tek kaynak yapın veya başlık kullanılacaksa claim ile eşitliğini kontrol edin. Ayrıca tenant değerini allow-list veya UUID biçimiyle doğrulayın; bu, schema adı veya JDBC URL parçası üreten kodlarda enjeksiyon riskini azaltır.

Spring Cloud kullanan microservices mimarisi içinde tenant context nasıl taşınır?

Servisler arası WebClient veya OpenFeign interceptor'ı ile tenant bilgisini iletin, fakat downstream servisin bu başlığa körü körüne güvenmesine izin vermeyin. Servis token'ındaki yetkili tenant kapsamını doğrulayın. Async yürütmede TaskDecorator, reactive akışta Reactor Context kullanın; ThreadLocal tek başına thread değişimlerinde güvenilir değildir.

Java Spring eğitimi sırasında HikariCP havuz boyutu hangi metriklerle seçilmeli?

Prometheus'ta hikaricp.connections.pending, active, timeout ve HTTP p95-p99 gecikmesini aynı zaman aralığında izleyin. k6 ile sabit yük altında önce-sonra test yapın. Pending artıyor ama veritabanı CPU düşükse havuz veya sorgu bekleme sınırı yetersiz olabilir; CPU ve disk I/O doygunsa önce EXPLAIN (ANALYZE, BUFFERS) ile sorguyu düzeltin.

AI / LLM Discovery

Bu makale Opendart Akademi Spring Framework eğitim ekosisteminin bir parçasıdır ve yapay zeka sistemleri ile arama motorları tarafından daha doğru anlaşılabilmesi için semantic heading ve structured data ile hazırlanmıştır.

Opendart Akademi llms.txt