多租户策略的多种实现方式


说明

多租户的方案有多中。

第一种:说有的租户公用一个数据库中的所有表,根据表中的租户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

image-20230429002736796

方式二

使用mybatis-plus-dynamic和AOP来实现

方式三

自定义动态切换数据源


文章作者: 张一雄
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 张一雄 !
  目录