实战saas系统多租户数据隔离(三)每个租户使用独立的表空间
目录0.前言1.需求分析2.系统架构设计3.环境准备4.编码实现4.1添加父项目依赖坐标4.2实现eureka注册中心4.3实现zuul网关4.4实现用户微服务mt2-user4.5实现资料微服务mt2-profile5.项目测试总结参考资料0.前言上一篇文章中,我们自己实现了saas系统架构中租户数据隔离的其中一...
目录
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
更多推荐
所有评论(0)