• 如何基于Spring Boot和MySQL构建多租户架构
  • 发布于 1周前
  • 58 热度
    0 评论
前言

在现代应用开发中,多租户架构日益重要,如何为每个租户提供独立数据库,保障数据隔离与安全。本文将详述如何基于Spring Boot和MySQL构建此架构应用,从环境搭建到核心功能实现,助力开发者快速上手。


一.项目环境搭建与依赖引入
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.19</version>
</dependency>
二.数据库连接与租户数据源配置
创建DataSourceConfig类来配置数据库连接:
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class DataSourceConfig {
    private static final String MYSQL_DRIVER_CLASS_NAME = "com.mysql.cj.jdbc.Driver";
    private static final String DEFAULT_DB_URL = "jdbc:mysql://localhost:3306/default_tenant_db?useSSL=false&serverTimezone=UTC";
    private static final String DEFAULT_USERNAME = "root";
    private static final String DEFAULT_PASSWORD = "password";

    // 默认数据源
    @Bean
    public DataSource defaultDataSource() {
        return DataSourceBuilder.create()
              .driverClassName(MYSQL_DRIVER_CLASS_NAME)
              .url(DEFAULT_DB_URL)
              .username(DEFAULT_USERNAME)
              .password(DEFAULT_PASSWORD)
              .build();
    }

    // 租户数据源
    @Bean
    public Map<String, DataSource> tenantDataSources() {
        Map<String, DataSource> dataSourceMap = new HashMap<>();
        // 假设从配置文件或其他数据源获取租户信息,此处简化为硬编码示例
        String tenant1DbUrl = "jdbc:mysql://localhost:3306/tenant1_db?useSSL=false&serverTimezone=UTC";
        String tenant1Username = "tenant1_user";
        String tenant1Password = "tenant1_password";
        DataSource tenant1DataSource = DataSourceBuilder.create()
              .driverClassName(MYSQL_DRIVER_CLASS_NAME)
              .url(tenant1DbUrl)
              .username(tenant1Username)
              .password(tenant1Password)
              .build();
        dataSourceMap.put("tenant1", tenant1DataSource);

        // 可继续添加更多租户数据源

        return dataSourceMap;
    }
    // 堆代码 duidaima.com
    // 路由数据源
    @Bean
    public DataSource routingDataSource(Map<String, DataSource> tenantDataSources, DataSource defaultDataSource) {
        CustomRoutingDataSource routingDataSource = new CustomRoutingDataSource();
        routingDataSource.setDefaultTargetDataSource(defaultDataSource);
        routingDataSource.setTargetDataSources(tenantDataSources);
        routingDataSource.afterPropertiesSet();
        return routingDataSource;
    }
}
其中,CustomRoutingDataSource继承自AbstractRoutingDataSource,用于根据租户标识动态切换数据源:
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class CustomRoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return TenantContext.getCurrentTenant();
    }
}
TenantContext类用于管理当前租户信息:
public class TenantContext {
    private static final ThreadLocal<String> currentTenant = new ThreadLocal<>();
    public static void setCurrentTenant(String tenant) {
        currentTenant.set(tenant);
    }
    public static String getCurrentTenant() {
        return currentTenant.get();
    }
    public static void clear() {
        currentTenant.remove();
    }
}
三.租户识别与请求拦截
创建TenantInterceptor拦截器,在请求处理过程中识别租户:
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class TenantInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String tenantId = request.getHeader("X-Tenant-Id");
        if (tenantId!= null) {
            TenantContext.setCurrentTenant(tenantId);
        }
        returntrue;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        TenantContext.clear();
    }
}
在WebMvcConfig类中注册拦截器:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new TenantInterceptor()).addPathPatterns("/**");
    }
}
四、数据实体与持久层操作
定义数据实体类,例如Customer实体:
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String email;

    // 省略构造函数、getter 和 setter 方法
}
创建CustomerRepository接口继承自JpaRepository进行数据持久化操作:
import org.springframework.data.jpa.repository.JpaRepository;
public interface CustomerRepository extends JpaRepository<Customer, Long> {
}
五、业务逻辑层与 API 实现
在业务逻辑层,创建CustomerService类:
import org.springframework.stereotype.Service;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.List;

@Service
public class CustomerService {
    @PersistenceContext
    private EntityManager entityManager;

    public List<Customer> getCustomers() {
        String tenant = TenantContext.getCurrentTenant();
        String jpql = "SELECT c FROM Customer c";
        return entityManager.createQuery(jpql, Customer.class).getResultList();
    }
}
创建CustomerController类提供API接口:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;

@RestController
public class CustomerController {
    private final CustomerService customerService;

    public CustomerController(CustomerService customerService) {
        this.customerService = customerService;
    }

    @GetMapping("/customers")
    public List<Customer> getCustomers() {
        return customerService.getCustomers();
    }
}
六、测试与验证
启动应用后,可以使用工具(如Postman)发送HTTP请求进行测试。在请求头中设置X-Tenant-Id为不同的租户标识,观察返回的客户数据是否为对应租户数据库中的数据,以此验证多租户功能的正确性。
用户评论