说明
多租户的方案有多中。
第一种:说有的租户公用一个数据库中的所有表,根据表中的租户id进行区分
第二中:多租户使用同一个数据库,每个租户使用一个表空间
第三张,每个租户使用一个数据库
第一种,可以通过分表策略来实现,第二中和第三中比较类似,需要动态的切换数据源,下面的内容,只聊第二种和第三种的实现方式
默认可以从前端请求的请求头中可以得到 租户标识(tenantId)
方式一
说明
使用mybatis-plus-dynamic和拦截器来实现
原理
使用dynamic 的动态切换数据源的功能,新增拦截器,从前端的请求中得到租户标识,根据不同的租户标识来动态的切换数据库
dynamic还具有在代码中切换数据库的功能
需要注意的是,DynamicDataSourceContextHolder 的push方法执行后,方法结束后一定要把数据清理掉
准备
准备两个数据库
创建order表
/*
Navicat Premium Data Transfer
Source Server : mysql-master
Source Server Type : MySQL
Source Server Version : 80033
Source Host : 192.168.31.17:3306
Source Schema : db_order
Target Server Type : MySQL
Target Server Version : 80033
File Encoding : 65001
Date: 28/04/2023 21:36:23
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for t_order
-- ----------------------------
DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order` (
`id` bigint NOT NULL AUTO_INCREMENT,
`order_no` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`user_id` bigint NULL DEFAULT NULL,
`amount` decimal(10, 2) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of t_order
-- ----------------------------
SET FOREIGN_KEY_CHECKS = 1;
搭建(动态切换数据源)
pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo-tenant</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo-tenant</name>
<description>demo-tenant</description>
<properties>
<java.version>8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.6.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.mysql/mysql-connector-j -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
yml
spring:
datasource:
dynamic:
primary: master #设置默认的数据源或者数据源组,默认值即为master
strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
datasource:
ds0:
url: jdbc:mysql://mysql.sqlpub.com:3306/java0417
username: java0417
password: f345c26699a412e2
driver-class-name: com.mysql.cj.jdbc.Driver # 3.2.0开始支持SPI可省略此配置
ds1:
url: jdbc:mysql://mysql.sqlpub.com:3306/random_chat
username: random_chat
password: 08dec60e5f2fef20
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
mapper-locations: mapper/*.xml
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
type-aliases-package: com.example.xiong.entity
实体类
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
@TableName("t_order0")
@Data
public class Order {
@TableId(type = IdType.INPUT)
private Long id;
private String orderNo;
private Long userId;
private BigDecimal amount;
}
mapper
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.xiong.entity.Order;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface OrderMapper extends BaseMapper<Order> {
}
controller
package com.example.xiong.controller;
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.xiong.entity.Order;
import com.example.xiong.mapper.OrderMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* @Auther: java0
* @Date: 2023/4/29 00:17
* @Description: TenantController
* @Version 1.0.0
*/
@RestController
@RequestMapping("/tenant")
public class TenantController {
@Autowired
OrderMapper orderMapper;
@GetMapping("/test")
public void test(){
List<Order> orders = orderMapper.selectList(new QueryWrapper<Order>());
System.out.println(orders.size());
DynamicDataSourceContextHolder.push("ds0");
List<Order> orders1 = orderMapper.selectList(new QueryWrapper<Order>());
System.out.println(orders1.size());
DynamicDataSourceContextHolder.push("ds1");
List<Order> orders2 = orderMapper.selectList(new QueryWrapper<Order>());
System.out.println(orders2.size());
}
}
搭建拦截器
拦截器
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
/**
* 登录检查
* 1.配置到拦截器要拦截哪些请求
* 2.把这些配置放在容器中
*
* 实现HandlerInterceptor接口
*/
@Slf4j
public class TenantInterceptor implements HandlerInterceptor {
/**
* 目标方法执行之前
* 登录检查写在这里,如果没有登录,就不执行目标方法
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String tenantId = request.getHeader("tenantId");
DynamicDataSourceContextHolder.push(tenantId);
return true;
}
/**
* 目标方法执行完成以后
* @param request
* @param response
* @param handler
* @param modelAndView
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("执行完方法后……………………………………");
DynamicDataSourceContextHolder.poll();
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
/**
* 页面渲染以后
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("页面完成渲染后");
DynamicDataSourceContextHolder.clear();
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
拦截器配置
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Web配置类
*/
@Configuration
public class TenantConfig implements WebMvcConfigurer {
/**
* 添加Web项目的拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 对所有访问路径,都通过MyInterceptor类型的拦截器进行拦截
registry.addInterceptor(new TenantInterceptor()).addPathPatterns("/**")
.excludePathPatterns("/login", "/index.html", "/user/login", "/css/**",../https://img.myfox.fun/img/**", "/js/**", "/fonts/**");
//放行登录页,登陆操作,静态资源
}
}
测试
http://localhost:8080/tenant/test
方式二
使用mybatis-plus-dynamic和AOP来实现
方式三
自定义动态切换数据源