19 - Spring Boot Multi-Tenancy
03/03/2025 - 3 phút
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úc | Mô tả |
---|---|
Database per Tenant | Mỗi tenant có một database riêng biệt. |
Schema per Tenant | Mỗi tenant có một schema riêng trong cùng một database. |
Table per Tenant | Một số bảng chung, một số bảng riêng cho từng tenant. |
Shared Database, Shared Schema | Cá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 Tenant và Shared 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.