Spring Boot - 多租户 - 优化 API 的响应时间

Posted

技术标签:

【中文标题】Spring Boot - 多租户 - 优化 API 的响应时间【英文标题】:Spring Boot - Multitenancy - Optimise response time for API 【发布时间】:2021-03-18 16:41:11 【问题描述】:

我有一个 Spring Boot 应用程序,它实现了多模式多租户。在没有多租户的情况下,相同的 API 响应时间为 300-400 毫秒。但在实施多租户后,响应时间增加到 6-7 秒(在同一服务器和同一架构上)。

我知道读取header,根据header切换数据库等需要额外的处理。但我觉得应该不是6-7秒。有人可以建议我如何减少此响应时间。以下是为多租户添加的类

public class TenantAwareRoutingSource extends AbstractRoutingDataSource 
    @Override
    protected Object determineCurrentLookupKey() 
        return ThreadLocalStorage.getTenantName();
    

public class TenantNameInterceptor extends HandlerInterceptorAdapter 
    
    @Value("$schemas.list")
    private String schemasList;
    private Gson gson = new Gson();
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception 
        String tenantName = request.getHeader("tenant-id");
        if(StringUtils.isBlank(schemasList)) 
            response.setContentType("application/json");
            response.setCharacterEncoding("UTF-8");
            response.getWriter().write(gson.toJson(new Error("Tenants not initalized...")));
            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            return false;
        
        
        if(!schemasList.contains(tenantName)) 
            response.setContentType("application/json");
            response.setCharacterEncoding("UTF-8");
            response.getWriter().write(gson.toJson(new Error("User not allowed to access data")));
            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            return false;
        
        ThreadLocalStorage.setTenantName(tenantName);
        return true;
    

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception 
        ThreadLocalStorage.setTenantName(null);
    
    
    @Setter
    @Getter
    @AllArgsConstructor
    public static class Error 
        private String message;
    

public class ThreadLocalStorage 
    private static ThreadLocal<String> tenant = new ThreadLocal<>();

    public static void setTenantName(String tenantName) 
        tenant.set(tenantName);
    

    public static String getTenantName() 
        return tenant.get();
    

@Configuration
public class AutoDDLConfig


    @Value("$spring.datasource.username")
    private String username;

    @Value("$spring.datasource.password")
    private String password;

    @Value("$schemas.list")
    private String schemasList;

    @Value("$db.host")
    private String dbHost;

    @Bean
    public DataSource dataSource()
    
        AbstractRoutingDataSource multiDataSource = new TenantAwareRoutingSource();
        if (StringUtils.isBlank(schemasList))
        
            return multiDataSource;
        

        String[] tenants = schemasList.split(",");
        Map<Object, Object> targetDataSources = new HashMap<>();
        for (String tenant : tenants)
        
            System.out.println("####" + tenant);
            tenant = tenant.trim();
            DriverManagerDataSource dataSource = new DriverManagerDataSource();
            dataSource.setDriverClassName("com.mysql.jdbc.Driver"); // Change here to MySql Driver
            dataSource.setSchema(tenant);
            dataSource.setUrl("jdbc:mysql://" + dbHost + "/" + tenant
                    + "?autoReconnect=true&characterEncoding=utf8&useSSL=false&useTimezone=true&serverTimezone=Asia/Kolkata&useLegacyDatetimeCode=false&allowPublicKeyRetrieval=true");
            dataSource.setUsername(username);
            dataSource.setPassword(password);
            targetDataSources.put(tenant, dataSource);
            LocalContainerEntityManagerFactoryBean emfBean = new LocalContainerEntityManagerFactoryBean();
            emfBean.setDataSource(dataSource);
            emfBean.setPackagesToScan("com"); // Here mention JPA entity path / u can leave it scans all packages
            emfBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
            emfBean.setPersistenceProviderClass(HibernatePersistenceProvider.class);
            Map<String, Object> properties = new HashMap<>();

            properties.put("hibernate.hbm2ddl.auto", "update");
            properties.put("hibernate.default_schema", tenant);
            properties.put("hibernate.dialect", "org.hibernate.dialect.MySQL5InnoDBDialect");
            emfBean.setJpaPropertyMap(properties);
            emfBean.setPersistenceUnitName(dataSource.toString());
            emfBean.afterPropertiesSet();
        
        multiDataSource.setTargetDataSources(targetDataSources);
        multiDataSource.afterPropertiesSet();
        return multiDataSource;

    


来自 application.properties 的片段

spring.datasource.username=<<username>>
spring.datasource.password=<<pssword>>


schemas.list=suncitynx,kalpavrish,riddhisiddhi,smartcity,businesspark
db.host=localhost


########## JPA Config ###############
spring.jpa.open-in-view=false
spring.jpa.show-sql=false
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
spring.jpa.database=mysql
spring.datasource.initialize=false
spring.jpa.hibernate.ddl-auto=none
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect
spring.jpa.properties.hibernate.jdbc.time_zone = Asia/Kolkata
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect

##############Debug Logging#########################
#logging.level.org.springframework=DEBUG
#logging.level.org.hibernate.SQL=DEBUG
#logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE


#########    HIkari  Pool ##############
spring.datasource.hikari.maximum-pool-size=20

######### Jackson ############
spring.jackson.serialization.WRITE_ENUMS_USING_TO_STRING=true
spring.jackson.deserialization.READ_ENUMS_USING_TO_STRING=true
spring.jackson.time-zone: Asia/Kolkata


#common request logger
logging.level.org.springframework.web.filter.CommonsRequestLoggingFilter=DEBUG

#Multi part file size
spring.servlet.multipart.max-file-size = 15MB
spring.servlet.multipart.max-request-size = 15MB

【问题讨论】:

【参考方案1】:

您确定维护每个租户的连接池吗?

【讨论】:

我没有在任何地方明确编码连接池。 Hibernate 可能会采用默认值。也由 application.properties 提供

以上是关于Spring Boot - 多租户 - 优化 API 的响应时间的主要内容,如果未能解决你的问题,请参考以下文章

使用多租户时的 Spring Boot 范围问题

Spring Boot 中的多租户

Spring Boot Keycloak 多租户配置

Spring Boot 构建多租户SaaS平台核心技术指南

Spring Boot 构建多租户SaaS平台核心技术指南

Spring Boot 构建多租户SaaS平台核心技术指南