19 - Spring Boot Multi-Tenancy

03/03/2025 - 3 phút

Follow  on Google News

Trong bài viết này, chúng ta sẽ tìm hiểu về Spring Boot Multi-Tenancy, cách xây dựng ứng dụng hỗ trợ nhiều khách hàng (tenants).

1. Giới Thiệu

Trong các hệ thống SaaS (Software-as-a-Service), một ứng dụng có thể phục vụ nhiều khách hàng (tenants). Mỗi tenant có thể có dữ liệu riêng biệt hoặc chung một cơ sở dữ liệu nhưng có cách phân tách logic khác nhau. Đây chính là Multi-Tenancy.

1.1. Lợi Ích Của Multi-Tenancy

  • Tối ưu hóa tài nguyên, giảm chi phí vận hành.
  • Tách biệt dữ liệu giữa các khách hàng.
  • Dễ mở rộng và quản lý hệ thống.

1.2. Các Kiến Trúc Multi-Tenancy

Kiến trúcMô tả
Database per TenantMỗi tenant có một database riêng biệt.
Schema per TenantMỗi tenant có một schema riêng trong cùng một database.
Table per TenantMột số bảng chung, một số bảng riêng cho từng tenant.
Shared Database, Shared SchemaCác tenant chia sẻ chung database, phân tách bằng cột tenant_id.

Trong bài viết này, chúng ta sẽ triển khai Multi-Tenancy với Spring Boot, tập trung vào Schema per TenantShared Database, Shared Schema.


2. Cấu Hình Multi-Tenancy Trong Spring Boot

2.1. Thêm Dependencies

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
</dependency>

2.2. Cấu Hình application.properties

spring.datasource.url=jdbc:postgresql://localhost:5432/main_db
spring.datasource.username=postgres
spring.datasource.password=yourpassword
spring.datasource.driver-class-name=org.postgresql.Driver
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect

3. Triển Khai Multi-Tenancy Với Schema per Tenant

3.1. Xác Định Tenant ID

Mỗi request sẽ có một tenant ID, có thể lấy từ Header, JWT Token hoặc Subdomain.

Tạo TenantContext.java:

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

    public static void setTenant(String tenant) {
        CURRENT_TENANT.set(tenant);
    }

    public static String getTenant() {
        return CURRENT_TENANT.get();
    }

    public static void clear() {
        CURRENT_TENANT.remove();
    }
}

3.2. Middleware Lấy Tenant ID Từ Request

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component;

@Component
public class TenantFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
        HttpServletRequest req = (HttpServletRequest) request;
        String tenant = req.getHeader("X-Tenant-ID");
        TenantContext.setTenant(tenant);
        try {
            chain.doFilter(request, response);
        } finally {
            TenantContext.clear();
        }
    }
}

3.3. Cấu Hình Dynamic DataSource

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class TenantAwareDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return TenantContext.getTenant();
    }
}

Tạo DataSourceConfig.java:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class DataSourceConfig {
    @Autowired
    private TenantProperties tenantProperties;

    @Bean
    public DataSource dataSource() {
        TenantAwareDataSource dataSource = new TenantAwareDataSource();
        Map<Object, Object> dataSources = new HashMap<>();
        
        tenantProperties.getTenants().forEach((tenant, config) -> {
            DataSource ds = DataSourceBuilder.create()
                .url(config.getUrl())
                .username(config.getUsername())
                .password(config.getPassword())
                .driverClassName("org.postgresql.Driver")
                .build();
            dataSources.put(tenant, ds);
        });
        
        dataSource.setTargetDataSources(dataSources);
        return dataSource;
    }
}

3.4. Cấu Hình TenantProperties.java

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.Map;

@Component
@ConfigurationProperties(prefix = "tenants")
public class TenantProperties {
    private Map<String, DataSourceConfig> tenants;

    public Map<String, DataSourceConfig> getTenants() {
        return tenants;
    }
}

Cấu hình application.yml:

tenants:
  tenant1:
    url: jdbc:postgresql://localhost:5432/tenant1_db
    username: tenant1_user
    password: tenant1_pass
  tenant2:
    url: jdbc:postgresql://localhost:5432/tenant2_db
    username: tenant2_user
    password: tenant2_pass

4. Triển Khai API Đa Tenant

4.1. Tạo Model

import jakarta.persistence.*;

@Entity
@Table(name = "products")
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;
}

4.2. Tạo Repository

import org.springframework.data.jpa.repository.JpaRepository;

public interface ProductRepository extends JpaRepository<Product, Long> {}

4.3. Tạo API

import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController
@RequestMapping("/products")
public class ProductController {
    private final ProductRepository repository;

    public ProductController(ProductRepository repository) {
        this.repository = repository;
    }

    @GetMapping
    public List<Product> getProducts() {
        return repository.findAll();
    }
}

Gửi request với X-Tenant-ID:

curl -H "X-Tenant-ID: tenant1" http://localhost:8080/products

5. Kết Luận

Spring Boot Multi-Tenancy giúp xây dựng ứng dụng hỗ trợ nhiều khách hàng, tách biệt dữ liệu hiệu quả.

👉 Trong bài viết tiếp theo, chúng ta sẽ tìm hiểu về Spring Boot OAuth2, cách bảo mật ứng dụng với OAuth2.