實戰saas系統多租戶數據隔離(三)每個租戶使用獨立的表空間

目錄

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

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章