目录

0. 前言

1. 需求分析

2. 系统架构设计

3. 环境准备

4. 编码实现

4.1 添加父项目依赖坐标

4.2 实现eureka注册中心

4.3 实现zuul网关

4.4 实现用户微服务mt2-user

4.5 实现资料微服务mt2-profile

5. 项目测试

总结

参考资料


0. 前言

上一篇文章中,我们自己实现了saas系统架构中租户数据隔离的其中一种解决方案,即使用租户id字段来实现同一张数据表中不同租户数据的增删改查。本文中,我们再来尝试实现另外一种解决方案,即每个租户使用独立的表空间(schema)的方式。

我们还是将编写一个小小的demo来实现这个方案,只不过这次我们将使用springcloud + mybatis-plus的框架组合,将上一篇文章中的demo升级一下。

事先声明,本文仅说明如何实现多租户的数据隔离,并不展开讨论其他问题,例如登录后会话有效期,超时后会话清理,数据库集群环境下的数据同步,微服务节点的负载均衡和路由等。

嫌看文章麻烦啰嗦的大神,可以直接去看本文所涉及的代码。下面是码云仓库地址

https://gitee.com/zectorlion/MultiTenancy

仓库中的Solution2项目既是本文相关的代码(带sql脚本哦)

在开始正文之前,有几个概念有必要先和大家交代一下,要不然大家后面看的可能会感觉晕。首先,表空间、schema,你可以把它俩理解为一个东西。不过数据库和schema,可能很多人会搞混。本文中,数据库和schema也可以理解为同一个概念。但是数据库和数据库系统是两个不同的概念,数据库系统就是我们安装在机器上的一个软件,而数据库就是数据库系统中保存数据的一个仓库,这个仓库中有表、视图、索引等数据模型,所以有人也叫它schema。一个数据库系统中可以创建有多个数据库或者schema。

 

1. 需求分析

本次我们实现的demo仍然是提供两个对外的api接口,调用方式也和上一篇文章中的demo一样。也既是用户登录接口和资料数据增删改查接口。

1. 用户登录接口:接口访问地址是/user/login/{id},用户将自己的id号传递给接口进行登录。用户登录成功后,系统为用户生成一个 token 并返回给用户。

2. 资料数据增删改查接口:接口访问地址是/profile/findAll/{token}、/profile/add/{token}等。用户调用接口时,必须携带其登录时系统返回给他的 token。

 

2. 系统架构设计

上一篇文章中的demo项目是将用户服务和资料管理服务写在一起的,本文中我们将这两个服务拆分成两个微服务,即user微服务和profile微服务,然后通过zuul网关实现统一的访问路径路由映射。当然,有了zuul,那就得配套一个eureka注册中心了。整个架构其实没那么复杂,如下图所示

从图中我们可以看到端口的分配情况,汇总如下表

微服务名称使用的端口号
eureka注册中心(mt2-eureka)8000
zuul网关(mt2-zuul)8080
用户微服务(mt2-user)8081
资料微服务(mt2-profile)8082

 

3. 环境准备

首先仍然给大家交代一下我所使用的系统环境和对应的版本,避免大家在版本号的问题上踩坑。

- springboot:2.1.4.RELEASE

- springcloud:Greenwich.SR1

- mybatis-plus:3.0.5

- mysql数据库:5.7.26-log MySQL Community Server (GPL)

- 谷歌浏览器:76.0.3809.132(正式版本) (64 位)
 

首先我们还是要先准备一些测试数据,只不过这次我们要创建三个库,也既是三个schema,分别是mt-user,profile1和profile2。其中mt-user库中就一张user表,内容和之前一样,有6个用户,分属于两个租户。profile1和profile2两个库各自有一张profile表,分别存放不同租户的数据。整个数据库中schema的结构如下图所示

使用下面的sql脚本,可以直接完成建库,建表和导数据的整个过程。

-- Dump created by MySQL pump utility, version: 5.7.26, Win64 (x86_64)
-- Dump start time: Sun Sep 01 20:55:29 2019
-- Server version: 5.7.24

SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0;
SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0;
SET @OLD_SQL_MODE=@@SQL_MODE;
SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";
SET @@SESSION.SQL_LOG_BIN= 0;
SET @OLD_TIME_ZONE=@@TIME_ZONE;
SET TIME_ZONE='+00:00';
SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT;
SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS;
SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION;
SET NAMES utf8mb4;
CREATE DATABASE /*!32312 IF NOT EXISTS*/ `mt-user` /*!40100 DEFAULT CHARACTER SET utf8 */;
CREATE TABLE `mt-user`.`user` (
`id` bigint(20) NOT NULL COMMENT '主键',
`tenant_id` bigint(20) NOT NULL COMMENT '服务商ID',
`name` varchar(30) DEFAULT NULL COMMENT '姓名',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8
;
INSERT INTO `mt-user`.`user` VALUES (1,1,"Tony老师"),(2,1,"William老师"),(3,2,"路人甲"),(4,2,"路人乙"),(5,2,"路人丙"),(6,2,"路人丁");
CREATE DATABASE /*!32312 IF NOT EXISTS*/ `profile1` /*!40100 DEFAULT CHARACTER SET utf8 */;
CREATE TABLE `profile1`.`profile` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(20) DEFAULT NULL,
`content` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8
;
INSERT INTO `profile1`.`profile` VALUES (1,"1号档案","1号档案");
CREATE DATABASE /*!32312 IF NOT EXISTS*/ `profile2` /*!40100 DEFAULT CHARACTER SET utf8 */;
CREATE TABLE `profile2`.`profile` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(20) DEFAULT NULL,
`content` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8
;
INSERT INTO `profile2`.`profile` VALUES (2,"2号档案","2号档案");
SET TIME_ZONE=@OLD_TIME_ZONE;
SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT;
SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS;
SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION;
SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS;
SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS;
SET SQL_MODE=@OLD_SQL_MODE;
-- Dump end time: Sun Sep 01 20:55:31 2019

 

搞定测试数据以后,下面我们再把项目骨架创建出来。本文中的项目结构还是使用典型的一父多子的结构,父项目是Solution2,子项目分别是mt2-eureka,mt2-zuul,mt2-user,mt2-profile。整个项目的结构如下图

至此,测试数据和项目骨架都已经准备好了,我们的准备工作也完成了。下面可以开始动手写代码了。

 

4. 编码实现

下面我们开始为项目添加代码,填充“血肉”,给项目赋予“灵魂”。

4.1 添加父项目依赖坐标

首先我们先编辑父项目Solution2的pom文件,把几个子项目都会用到的依赖坐标添加进去

<dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.1.4.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Greenwich.SR1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

4.2 实现eureka注册中心

这个没啥好说的,经典的三部曲,改pom文件,添加application.yml配置文件,编写启动引导类。我就不啰嗦了,直接上代码上配置。注意,改的是mt2-eureka项目中的代码和文件,别搞错了。

pom文件:

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
    </dependencies>

application.yml文件:

server:
  port: 8000
spring:
  application:
    name: mt2-zureka
eureka:
  client:
    register-with-eureka: false #是否将自己注册到eureka中
    fetch-registry: false #是否从eureka中获取信息
    service-url:
      defaultZone: http://0.0.0.0:${server.port}/eureka/

启动引导类:

@SpringBootApplication
@EnableEurekaServer
public class EurekaApplication {

    public static void main(String[] args) {
        SpringApplication.run(EurekaApplication.class);
    }

}

 

4.3 实现zuul网关

这个也没啥好说的,也是改pom文件,添加application.yml配置文件,编写启动引导类这经典的三部曲。不啰嗦,直接上代码上配置。注意,改的是mt2-zuul项目中的代码和文件,别搞错了。

pom文件:

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
    </dependencies>

application.yml文件:

server:
  port: ${SERVER_PORT:8080}
spring:
  application:
    name: mt2-zuul
ribbon:
  ReadTimeout: 300000
  ConnectTimeout: 300000
  MaxAutoRetries: 3
  MaxAutoRetriesNextServer: 3
  eureka:
    enabled: true

#hystrix超时熔断配置
hystrix:
  command:
    cmut-app-api:
      execution:
        timeout:
          enabled: true
        isolation:
          thread:
            timeoutInMilliseconds: 300000

eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:8000/eureka/
  instance:
    prefer-ip-address: true

zuul:
  routes:
    mt2-user: #用户
      path: /user/** #配置请求URL的请求规则
      serviceId: mt2-user #指定Eureka注册中心中的服务id
      strip-prefix: true
      sentiviteHeaders:
      customSensitiveHeaders: true
    mt2-profile: #用户
      path: /profile/** #配置请求URL的请求规则
      serviceId: mt2-profile #指定Eureka注册中心中的服务id
      strip-prefix: true
      sentiviteHeaders:
      customSensitiveHeaders: true

启动引导类:

@SpringBootApplication
@EnableEurekaClient
@EnableZuulProxy
public class ZuulApplication {


    public static void main(String[] args) {
        SpringApplication.run(ZuulApplication.class);
    }
}

 

4.4 实现用户微服务mt2-user

首先还是先进行经典三部曲,改pom文件,加配置,创建启动引导类。

pom文件:

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.0.5</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus</artifactId>
            <version>3.0.5</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.0.5</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.9</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
    </dependencies>

application.yml文件:

server: 
  port: 8081
spring: 
  application:  
    name: mt2-user
  datasource:
    url: jdbc:mysql://192.168.228.100:3306/mt-user?characterEncoding=UTF8
    username: root
    password: 123456
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.jdbc.Driver
    initialSize: 5
    minIdle: 5
    maxActive: 20
    maxWait: 60000
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    poolPreparedStatements: true
    maxPoolPreparedStatementPerConnectionSize: 20

eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:8000/eureka/
  instance:
    prefer-ip-address: true

启动引导类:

@SpringBootApplication
@EnableEurekaClient
public class Mt2UserApp {

    public static void main(String[] args) {
        SpringApplication.run(Mt2UserApp.class, args);
    }

    @Bean
    public TenantContext tenantContext() {
        return new TenantContext();
    }
}

然后把上一篇文章中的Solution3项目中所有关于user服务的controller,实体类,mapper等代码复制过来。当然,配置类MybatisPlusConfig需要重写。下面是MybatisPlusConfig配置类的代码

@Configuration
@MapperScan("mt2.user.mapper")
public class MybatisPlusConfig {

    @Bean
    public PaginationInterceptor paginationInterceptor() {
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();

        return paginationInterceptor;
    }

    @Bean(name = "performanceInterceptor")
    public PerformanceInterceptor performanceInterceptor() {
        return new PerformanceInterceptor();
    }
}

其实就是把paginationInterceptor方法中设置多租户sql处理的那段代码给去掉,变成标准的返回分页对象的代码。

 

UserController中也要添加一个tenantIdByToken接口,让其他微服务可以根据token获取到租户id。把下面这段代码添加到UserController中即可。

    @GetMapping("/tenantIdByToken/{token}")
    public Long tenantIdByToken(@PathVariable("token") String token) {
        return tenantContext.getTenantIdWithToken(token);
    }

 

至此,mt2-user用户微服务就搭建完成了。整个mt2-user项目的目录结构如下图

 

4.5 实现资料微服务mt2-profile

mt2-profile项目是今天的重点,每个租户使用独立表空间的方案,就是由这个项目来实现的。我的想法是使用租户id去识别应该要连接的schema,schema的名字是 profile 加租户id的格式。例如,租户id如果是1,那么租户1连接的就应该是 profile1 这个schema。具体是如何来实现的,后面我会详细去说。现在的话,我们还是先把经典三部曲搞完,保证mt2-profile项目有一个良好的基础环境。

pom文件:

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.0.5</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus</artifactId>
            <version>3.0.5</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.0.5</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.9</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
    </dependencies>

因为用户调用profile接口时会将token传递过来,而mt2-profile项目需要用token向mt2-user微服务换取租户id号,以便在进行数据库sql操作之前切换到租户对应的datasource上去,所以需要使用feign调用mt2-user微服务的接口。

 

application.yml文件:

server: 
  port: 8082
spring: 
  application:  
    name: mt2-profile

eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:8000/eureka/
  instance:
    prefer-ip-address: true

tenant:
  datasource:
    host: 192.168.228.100
    port: 3306
    username: root
    password: 123456
    schema: profile

在mt2-profile的application.yml配置文件中,并没有数据源datasource的配置,这是因为租户的数据源是在mt2-profile微服务运行过程中动态创建的。我们会在mt2-profile微服务中创建一个配置类TenantDatasouceConfig,读取该配置文件中前缀为tenant.datasource的配置,以便在动态创建datasource的时候使用。

 

启动引导类:

@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class Mt2ProfileApp {

    public static void main(String[] args) {
        SpringApplication.run(Mt2ProfileApp.class, args);
    }

    @Bean
    public DataSourceBuilder dataSourceBuilder() {
        DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create().driverClassName("com.mysql.jdbc.Driver");
        return dataSourceBuilder;
    }

    @Bean(name = "dataSourceMap")
    public Map<Object, Object> dataSourceMap() {
        Map<Object, Object> dataSourceMap = Maps.newConcurrentMap();
        return dataSourceMap;
    }
}

在mt2-profile微服务的启动引导类中,我们向springIOC容器中注入了两个bean对象,一个是用于在mt2-profile微服务运行时动态创建datasource的DataSourceBuilder,一个是用于保存租户id和datasource键值对的CocurrentHashMap,名字是dataSourceMap。我们随后将在很多地方使用到这两个bean。

 

然后我们创建两个工具类,AppContextHelper和DynamicRoutingDataSource,并把它们放到utils包下面。

AppContextHelper可以让我们根据class的名称,类型等信息,从springIOC容器中拿到对应的class实例。

DynamicRoutingDataSource是实现动态切换数据源的类,它是AbstractRoutingDataSource抽象类的扩展,也是我们实现根据租户id切换数据源的关键。以前也有人写过很多根据名称切换数据源的示例项目,只不过数据源的配置是提前在配置文件中定义好的,然后通过自定义注解,在程序运行的过程当中进行切换。而切换的关键,就是通过扩展AbstractRoutingDataSource抽象类,然后实现determineCurrentLookupKey方法,返回要切换的数据源的名称,实现切换数据源的目的。详细的情况,大家看一下AbstractRoutingDataSource抽象类的源码,就都清楚了。

AppContextHelper的代码

@Component
public class AppContextHelper implements ApplicationContextAware {
    private static ApplicationContext applicationContext;

    public AppContextHelper() {
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        AppContextHelper.applicationContext = applicationContext;
    }

    public static Object getBean(String beanName) {
        return applicationContext != null?applicationContext.getBean(beanName):null;
    }

    //通过class获取Bean.
    public static <T> T getBean(Class<T> clazz) {
        return applicationContext.getBean(clazz);
    }
}

DynamicRoutingDataSource的代码

/**
 * Multiple DataSource Configurer
 */
@Data
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {

    private Long tenantId;

    private final Logger logger = LoggerFactory.getLogger(getClass());

    /**
     * Set dynamic DataSource to Application Context
     *
     * @return
     */
    @Override
    protected Object determineCurrentLookupKey() {
        logger.debug("Current DataSource is [{}]", tenantId);
        return tenantId;
    }
}

 

然后我们创建一个TenantDatasouceConfig配置类,通过@ConfigurationProperties注解读取配置文件中前缀是tenant.datasource配置项的内容,并使用@Component注解将该配置类注入到springIOC容器中

@Data
@ConfigurationProperties(prefix = "tenant.datasource")
@Component
public class TenantDatasouceConfig {
    private String host;
    private int port;
    private String username;
    private String password;
    private String schema;
}

 

我们再在config包下面编写MybatisPlusConfig配置类,在该配置类中创建一个DynamicRoutingDataSource类的实例并注入到springIOC容器中

@Configuration
@MapperScan("mt2.profile.mapper")
public class MybatisPlusConfig {

    @Autowired
    private Map<Object, Object> dataSourceMap;

    @Autowired
    private DataSourceBuilder dataSourceBuilder;

    @Autowired
    private TenantDatasouceConfig tdc;

    @Bean
    public PaginationInterceptor paginationInterceptor() {

        return new PaginationInterceptor();
    }

    @Bean
    public PerformanceInterceptor performanceInterceptor() {
        return new PerformanceInterceptor();
    }

    /**
     * Dynamic data source.
     *
     * @return the data source
     */
    @Bean("dynamicDataSource")
    public DataSource dynamicDataSource() {
        DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource();

        dataSourceBuilder.url(String.format("jdbc:mysql://%s:%d?useSSL=false", tdc.getHost(), tdc.getPort()));
        dataSourceBuilder.username(tdc.getUsername());
        dataSourceBuilder.password(tdc.getPassword());
        DataSource dataSource = dataSourceBuilder.build();

        dataSourceMap.put((long) 0, dataSource);
        dynamicRoutingDataSource.setDefaultTargetDataSource(dataSource);
        // 可动态路由的数据源里装载了所有可以被路由的数据源
        dynamicRoutingDataSource.setTargetDataSources(dataSourceMap);

        return dynamicRoutingDataSource;
    }
}

 

接下来我们编写一个拦截器TenantInterceptor,它的功能是,拦截访问mt2-profile微服务的请求,从访问路径中获取到token,然后携带token调用mt2-user微服务的接口,拿到租户id。然后使用租户id从dataSourceMap中查询datasource对象,如果为空,则创建该租户id的专属数据源,并放入dataSourceMap和DynamicRoutingDataSource中。如果不为空,则将租户id设置进DynamicRoutingDataSource对象的tenantId属性。这样在进行数据库操作之前,mybatis-plus就知道该去哪个数据源执行sql操作了。TenantInterceptor代码如下

@Component
public class TenantInterceptor implements HandlerInterceptor {

    @Autowired
    private DataSourceBuilder dataSourceBuilder;

    @Autowired
    private Map<Object, Object> dataSourceMap;

    @Autowired
    private UserClient  userClient;

    @Autowired
    private TenantDatasouceConfig tdc;

    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
        String path=httpServletRequest.getRequestURI();
        String token = path.substring(path.lastIndexOf("/") + 1);
        if (null != token) {
            //UserClient userClient = (UserClient) AppContextHelper.getBean(UserClient.class);
            Long tenantId = userClient.tenantIdByToken(token);
            if(null != tenantId) {
                prepareDatasource(tenantId);

                return true;
            }
        }

        return false;
    }

    private void prepareDatasource(Long tenantId) {
        DynamicRoutingDataSource dynamicDataSource = (DynamicRoutingDataSource) AppContextHelper.getBean("dynamicDataSource");
        DataSource dataSource = (DataSource) dataSourceMap.get(tenantId);

        if (null == dataSource) {
            dataSourceBuilder.url(String.format("jdbc:mysql://%s:%d/%s%d?useSSL=false&characterEncoding=UTF8", tdc.getHost(), tdc.getPort(), tdc.getSchema(), tenantId));
            dataSourceBuilder.username(tdc.getUsername());
            dataSourceBuilder.password(tdc.getPassword());
            dataSource = dataSourceBuilder.build();

            dataSourceMap.put(tenantId, dataSource);
            dynamicDataSource.setTargetDataSources(dataSourceMap);
            dynamicDataSource.afterPropertiesSet();
        }

        dynamicDataSource.setTenantId(tenantId);
    }
}

然后我们再为TenantInterceptor拦截器编写一个配置类,将拦截器注入进springMVC容器中,使其生效。

@Configuration
@Order()
public class InterceptorConfig extends WebMvcConfigurerAdapter {

    @Autowired
    private TenantInterceptor tenantInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(tenantInterceptor).addPathPatterns("/**").excludePathPatterns("/user/login/**");
        super.addInterceptors(registry);
    }
}

 

由于TenantInterceptor拦截器中使用了feign远程调用,所以我们还需要编写feign client远程调用代码。在client包下面创建UserClient接口,代码如下

@FeignClient(value="mt2-user")
public interface UserClient {

    @GetMapping("/user/tenantIdByToken/{token}")
    public Long tenantIdByToken(@PathVariable("token") String token);
}

 

最后是controller的代码

@RestController
@RequestMapping("/profile")
public class ProfileController {

    @Autowired
    private ProfileMapper profileMapper;

    @GetMapping("/findAll/{token}")
    public String findAll(@PathVariable String token) {

        //prepareTenantContext(token);

        List<Profile> profiles = profileMapper.selectList(null);
        profiles.forEach(System.out::println);
        return "operation complete, the following is the result \n" + profiles.toString();
    }

    @GetMapping("/add/{token}")
    public String add(@PathVariable String token) {

        Profile p = new Profile();
        p.setId((long) 3);
        p.setTitle("3号档案");
        p.setContent("3号档案");

        int result = profileMapper.insert(p);
        return "operation complete, the following is the result \n" + String.valueOf(result);
    }

    @GetMapping("/update/{token}")
    public String update(@PathVariable String token) {

        Profile p = new Profile();
        p.setId((long) 3);
        p.setTitle("4号档案");
        p.setContent("4号档案");

        int result = profileMapper.updateById(p);
        return "operation complete, the following is the result \n" + String.valueOf(result);
    }

    @GetMapping("/delete/{token}")
    public String delete(@PathVariable String token) {

        int result = profileMapper.deleteById((long)3);
        return "operation complete, the following is the result \n" + String.valueOf(result);
    }
}

实体类和mapper文件的代码我就不贴了,没啥技术含量。

 

至此,mt2-profile微服务就编写完成了。整个mt2-profile微服务项目的文件结构如下

 

5. 项目测试

测试方法和Solution3项目的测试方法一样。我们首先把mt2-eureka,mt2-zuul,mt2-user,mt2-profile这几个微服务依次运行起来,然后打开浏览器,依次访问 http://localhost:8080/user/user/login/1 和 http://localhost:8080/user/user/login/6,得到两个token,然后再分别携带两个不同的token去调用 http://localhost:8080/profile/profile/findAll/{token} 接口,验证一下是否返回了对应租户的数据。篇幅问题,我就不亲自演示了。

 

总结

本文所展示的多租户使用独立的表空间的解决方案,核心是如何使用AbstractRoutingDataSource抽象类实现动态的数据源的切换,也就是本文中mt2-profile微服务所做的主要的工作。通过在进行数据库操作之前指定租户的schema,就可以对指定租户的数据进行增删改查操作,而不影响其他租户的schema中的数据,实现了租户数据的隔离。

 

参考资料

https://www.cnblogs.com/hsbt2333/p/9347249.html

 

https://blog.csdn.net/jinsedeme0881/article/details/79171151

 

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐