spring结合mybatis多租户实现单库分表

news2025/4/21 14:45:52

实现单库分表-水平拆分

思路:student表数据量大,所以将其进行分表处理。一共有三个分表,分别是student0,student1,student2,在新增数据的时候,根据请求头中的meta-tenant参数决定数据存在哪张表。

数据库

1. 建立数据库study1

2. 在数据库中建表,分别为student,student0,student1,student2,四个表结构都一样,只是表名不一样

CREATE TABLE `student` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(50) DEFAULT NULL,
  `age` int DEFAULT NULL,
  `credit` varchar(14) DEFAULT NULL,
  `tenant_id` int DEFAULT NULL COMMENT '租户id',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

引入pom

<!-- https://mvnrepository.com/artifact/org.apache.shardingsphere/shardingsphere-jdbc-core-spring-boot-starter -->
<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
    <version>5.1.1</version>
</dependency>

配置文件application.yml

#单库分表
spring:
  main:
    allow-bean-definition-overriding: true
  shardingsphere:
    datasource:
      names: ds1
      ds1:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/study1?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
        username: root
        password: root1234
    mode:
      type: Memory
      overwrite: true
    rules:
      sharding:
        tables:
          student:
            actual-data-nodes: ds1.student$->{0..2}
            key-generate-strategy:
              column: id
              key-generator-name: snowflake
        binding-tables:
          - student
        default-table-strategy:
          standard:
            sharding-algorithm-name: custom_inline
            sharding-column: tenant_id
        default-sharding-column: tenant_id
        sharding-algorithms:
          custom_inline:
            type: CLASS_BASED
            props:
              strategy: STANDARD
              algorithmClassName: com.cyy.config.TablePreciseShardingAlgorithm
    props:
      sql-show: true
tenant:
  enable: true #启用多租户
  column: tenant_id

实现标准分片算法接口

package com.cyy.config;

import org.apache.shardingsphere.sharding.api.sharding.standard.PreciseShardingValue;
import org.apache.shardingsphere.sharding.api.sharding.standard.RangeShardingValue;
import org.apache.shardingsphere.sharding.api.sharding.standard.StandardShardingAlgorithm;
import org.springframework.stereotype.Component;

import java.util.Collection;

/**
 * 标准分表算法
 */
@Component
public class TablePreciseShardingAlgorithm implements StandardShardingAlgorithm<Integer> {

    @Override
    public String doSharding(Collection<String> collection, PreciseShardingValue<Integer> preciseShardingValue) {
        String tableName = preciseShardingValue.getLogicTableName().toLowerCase() + (preciseShardingValue.getValue() %10);
        return tableName;
    }

    @Override
    public Collection<String> doSharding(Collection<String> collection, RangeShardingValue<Integer> rangeShardingValue) {
        return collection;
    }

    @Override
    public void init() {

    }

    @Override
    public String getType() {
        return null;
    }
}

配置租户上下文相关内容

用到的常量
package com.cyy.constant;


/**
 * 多租户相关的常量
 */
public class TenantConstant {
    public static final String META_TENANT_ID = "meta-tenant";
    public static final String META_TENANT_ID_PARAM = "tenantId";
    public static final Long TENANT_ID_DEFAULT = 1l;
}
定义租户上下文
package com.cyy.config;

/**
 * 定义租户上下文,通过上下文保存当前租户的信息
 */
public class TenantContextHolder {
    private static final ThreadLocal<Long> CONETXT_HOLDER = new ThreadLocal<>();

    public static void set(Long l){
        CONETXT_HOLDER.set(l);
    }

    public static Long getTenantId(){
        return CONETXT_HOLDER.get();
    }

    public static void remove(){
        CONETXT_HOLDER.remove();
    }
}
设置多租户的相关的属性
package com.cyy.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

import java.util.HashSet;
import java.util.Set;

@Data
/**
 * 将配置文件中的属性自动映射都Java对象的字段中
 * 指定配置文件中的配置项的前缀为tenant,简化@Vlue注解,不需要加很多的@Value
 */
@ConfigurationProperties(prefix = "tenant")
@Component
public class TenantProperties {

    //不需要加tenantid的mapper方法名
    public static Set<String> NOT_PROCEED = new HashSet<>();

    //不需要加tenentid的表名
    public static Set<String> NOT_TABLES = new HashSet<>();

    //是否开启多租户,读取的是配置文件中的tenant.enable的值,等价于@Value(${tenant.enable})
    private boolean enable = false;

    //多租户字段
    private String column = "tenant_id";
}
配置租户上下文拦截器
package com.cyy.interceptor;

import com.cyy.config.TenantContextHolder;
import com.cyy.config.TenantProperties;
import com.cyy.constant.TenantConstant;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * HandlerInterceptor是在handler处理请求之前或者之后执行的拦截器,可以对请求做预处理或者对响应结果做统一处理,实现日志记录或者权限认证等功能
 * HandlerInterceptor可以拦截所有的请求,也可以只拦截特定的亲故
 */
@Slf4j
@Component
public class TenantContextHandlerInterceptor implements HandlerInterceptor {

    @Resource
    private TenantProperties tenantProperties;

    public TenantContextHandlerInterceptor(){
    }

    /**
     * 在请求处理前设置租户上下文
     * @param request current HTTP request
     * @param response current HTTP response
     * @param handler chosen handler to execute, for type and/or instance evaluation
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("=====对请求进行统一拦截=====");
        if (tenantProperties.isEnable()){
            //从请求头中获取tenantId
            String tenantId = request.getHeader(TenantConstant.META_TENANT_ID);
            if (StringUtils.isNotBlank(tenantId)){
                TenantContextHolder.set(Long.parseLong(tenantId));
            } else {
                TenantContextHolder.set(TenantConstant.TENANT_ID_DEFAULT);
            }
            log.info("获取到的租户id为:【{}】",TenantContextHolder.getTenantId());
        }
        return true;
    }

    /**
     * 请求处理完成之后的回调
     * @param request current HTTP request
     * @param response current HTTP response
     * @param handler handler (or {@link HandlerMethod}) that started asynchronous
     * execution, for type and/or instance examination
     * @param ex exception thrown on handler execution, if any
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        TenantContextHolder.remove();
    }
}
将租户上下文拦截器添加到springmvc配置中

对服务器的所有请求进行拦截,从请求头的meta-tenant参数中获取tenant_id值,并设置租户id

package com.cyy.config;

import com.cyy.interceptor.TenantContextHandlerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.annotation.Resource;
import java.util.List;

/**
 * 通过实现WebMvcConfigurer接口,可以自定义springmvc的配置,例如添加拦截器
 */
@Configuration
public class CustomeConfig implements WebMvcConfigurer {

    @Resource
    private MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter;

    /**
     * TenantContextHandlerInterceptor类本身就是一个Bean,在这块再次声明一个相同Bean的时候,会报错
     * 可以在配置文件中添加配置allow-bean-definition-overriding: true,允许bean定义覆盖
     * @return
     */
    @Bean("tenantContextHandlerInterceptor")
    public HandlerInterceptor customerInterceptor(){
        return new TenantContextHandlerInterceptor();
    }

    /**
     * 添加拦截器
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(customerInterceptor());
    }

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(0,mappingJackson2HttpMessageConverter);
    }
}

配置mybatis的插件

继承多租户拦截器TenantLineInnerInterceptor
package com.cyy.interceptor;

import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import com.cyy.config.TenantContextHolder;
import com.cyy.config.TenantProperties;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import java.sql.SQLException;
import java.util.Objects;

/**
 * 继承多租户拦截器
 */
public class CustomTenantLineInnerInterceptor extends TenantLineInnerInterceptor {
    private TenantProperties tenantProperties;
    public TenantProperties getTenantProperties(){
        return tenantProperties;
    }
    public void setTenantProperties(TenantProperties tenantProperties){
        this.tenantProperties = tenantProperties;
    }

    @Override
    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        if (TenantProperties.NOT_PROCEED.stream().anyMatch(s -> s.equalsIgnoreCase(ms.getId()))){
            return;
        }
        if (Objects.isNull(TenantContextHolder.getTenantId())){
            return;
        }
        super.beforeQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql);
    }

    public CustomTenantLineInnerInterceptor(final TenantProperties tenantProperties, final TenantLineHandler tenantLineHandler){
        super(tenantLineHandler);
        this.setTenantProperties(tenantProperties);
        this.setTenantLineHandler(tenantLineHandler);
    }
}
添加多租户拦截器到mybatisplus拦截器中

将多租户拦截器CustomTenantLineInnerInterceptor添加到MybatisPlusInterceptor的拦截器中

package com.cyy.config;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import com.cyy.interceptor.CustomTenantLineInnerInterceptor;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.Resource;
import java.util.Objects;

@Configuration
@MapperScan("com.cyy.mapper")
public class MybatisPlusConfig {
    @Resource
    private TenantProperties tenantProperties;

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        //添加多租户拦截器
        if (tenantProperties.isEnable()){
            mybatisPlusInterceptor.addInnerInterceptor(tenantLineInnerInterceptor());
        }

        //分页插件
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        //防止全表更新与删除插件
        mybatisPlusInterceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
        //乐观锁插件
        mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return mybatisPlusInterceptor;

    }

    public TenantLineInnerInterceptor tenantLineInnerInterceptor(){
        return new CustomTenantLineInnerInterceptor(tenantProperties, new TenantLineHandler() {
            /**
             * 获取租户id
             * @return
             */
            @Override
            public Expression getTenantId() {
                Long tenantId = TenantContextHolder.getTenantId();
                if (Objects.nonNull(tenantId)){
                    return new LongValue(tenantId);
                }

                return null;
            }

            /**
             * 获取多租户的字段名
             * @return
             */
            @Override
            public String getTenantIdColumn() {
                return tenantProperties.getColumn();
            }

            /**
             * 根据表名判断是否忽略拼接多租户条件
             * @param tableName 表名
             * @return
             */
            @Override
            public boolean ignoreTable(String tableName) {
                return tenantProperties.NOT_TABLES.stream().anyMatch((t) -> t.equalsIgnoreCase(tableName));
            }
        });
    }
}

测试代码 

测试数据插入的controller

package com.cyy.controller;

import com.cyy.domain.Student;
import com.cyy.service.StudentService;
import com.cyy.util.RoundRobinUtil;
import com.cyy.config.TenantContextHolder;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * 学生类控制器
 */
@RestController
@RequestMapping("/student")
public class StudentController {
    @Resource
    private StudentService studentService;

    /**
     * 多租户测试-插入一个用户
     */
    @PostMapping("/insert")
    public ResponseEntity insert(@RequestBody Student student){
        student.setTenantId(TenantContextHolder.getTenantId().intValue());
        int i = studentService.insert(student);
        if (i > 0){
            return ResponseEntity.ok("插入成功");
        }
        return ResponseEntity.ok("插入失败");
    }
}

请求参数

{
    "id": 3,
    "name": "孙三",
    "age": 3,
    "credit": "3"
}

请求头

运行结果

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2308528.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

YoloV8改进策略:Block改进|CBlock,Transformer式的卷积结构|即插即用

摘要 论文标题: SparseViT: Nonsemantics-Centered, Parameter-Efficient Image Manipulation Localization through Spare-Coding Transformer 论文链接: https://arxiv.org/pdf/2412.14598 官方GitHub: https://github.com/scu-zjz/SparseViT 这段代码出自SparseViT ,代码如…

微服务架构实践:SpringCloud与Docker容器化部署

## 微服务架构实践&#xff1a;SpringCloud与Docker容器化部署 随着互联网应用的复杂性不断增加&#xff0c;传统的单体应用架构面临着诸多挑战&#xff0c;如难以部署、维护困难、开发效率低下等问题凸显出来。为了解决这些问题&#xff0c;微服务架构应运而生&#xff0c;它通…

[原创]openwebui解决searxng通过接口请求不成功问题

openwebui 对接 searxng 时 无法查询到联网信息&#xff0c;使用bing搜索&#xff0c;每次返回json是正常的 神秘代码&#xff1a; http://172.30.254.200:8080/search?q北京市天气&formatjson&languagezh&time_range&safesearch0&languagezh&locale…

8 SpringBootWeb(下):登录效验、异步任务和多线程、SpringBoot中的事务管理@Transactional

文章目录 案例-登录认证1. 登录功能1.1 需求1.2 接口文档1.3 思路分析1.4 功能开发1.5 测试2. 登录校验2.1 问题分析2.2 会话技术2.2.1 会话技术介绍2.2.2 会话跟踪方案2.2.2.1 方案一 - Cookie2.2.2.2 方案二 - Session2.2.2.3 方案三 - 令牌技术2.2.3 JWT令牌(Token)2.2.3.…

2025年山东省职业院校技能大赛(高职组)“云计算应用”赛项赛卷1

“云计算应用”赛项赛卷1 2025年山东省职业院校技能大赛&#xff08;高职组&#xff09;“云计算应用”赛项赛卷1模块一 私有云&#xff08;30分&#xff09;任务1 私有云服务搭建&#xff08;5分&#xff09;1.1.1 基础环境配置1.1.2 yum源配置1.1.3 配置无秘钥ssh1.1.4 基础安…

MySQL数据库基本概念

目录 什么是数据库 从软件角度出发 从网络角度出发 MySQL数据库的client端和sever端进程 mysql的client端进程连接sever端进程 mysql配置文件 MySql存储引擎 MySQL的sql语句的分类 数据库 库的操作 创建数据库 不同校验规则对查询的数据的影响 不区分大小写 区…

塔能科技:工厂智慧照明,从底层科技实现照明系统的智能化控制

在全球节能减碳和智慧生活需求激增的背景下&#xff0c;基于“用软件定义硬件&#xff0c;让物联运维更简捷更节能”的产品理念&#xff0c;塔能科技的智慧照明一体化方案如新星般崛起&#xff0c;引领照明行业新方向。现在&#xff0c;我们来深入探究其背后的创新技术。该方案…

P3398 仓鼠找 sugar【题解】

这是LCA的一个应用&#xff0c;关于LCA P3398 仓鼠找 sugar 题目描述 小仓鼠的和他的基&#xff08;mei&#xff09;友&#xff08;zi&#xff09;sugar 住在地下洞穴中&#xff0c;每个节点的编号为 1 ∼ n 1\sim n 1∼n。地下洞穴是一个树形结构。这一天小仓鼠打算从从他…

Android Trace埋点beginSection打tag标签,Kotlin

Android Trace埋点beginSection打tag标签&#xff0c;Kotlin import android.os.Bundle import android.os.Trace import android.util.Log import androidx.appcompat.app.AppCompatActivityclass ImageActivity : AppCompatActivity() {companion object {const val TRACE_TA…

Lua的table(表)

Lua表的基本概念 Lua中的表&#xff08;table&#xff09;是一种多功能数据结构&#xff0c;可以用作数组、字典、集合等。表是Lua中唯一的数据结构机制&#xff0c;其他数据结构如数组、列表、队列等都可以通过表来实现。 表的实现 Lua的表由两部分组成&#xff1a; 数组部分…

51页精品PPT | 农产品区块链溯源信息化平台整体解决方案

PPT展示了一个基于区块链技术的农产品溯源信息化平台的整体解决方案。它从建设背景和需求分析出发&#xff0c;强调了农产品质量安全溯源的重要性以及国际国内的相关政策要求&#xff0c;指出了食品安全问题在流通环节中的根源。方案提出了全面感知、责任到人、定期考核和追溯反…

Jenkins 自动打包项目镜像部署到服务器 ---(前端项目)

Jenkins 新增前端项目Job 指定运行的节点 选择部署运行的节点标签&#xff0c;dev标签对应开发环境 节点的远程命令执行配置 jenkins完整流程 配置源码 拉取 Credentials添加 触发远程构建 配置后可以支持远程触发jenkins构建&#xff08;比如自建的CICD自动化发布平台&…

使用AoT让.NetFramework4.7.2程序调用.Net8编写的库

1、创建.Net8的库&#xff0c;双击解决方案中的项目&#xff0c;修改如下&#xff0c;启用AoT&#xff1a; <Project Sdk"Microsoft.NET.Sdk"><PropertyGroup><OutputType>Library</OutputType><PublishAot>true</PublishAot>&…

第49天:Web开发-JavaEE应用SpringBoot栈模版注入ThymeleafFreemarkerVelocity

#知识点 1、安全开发-JavaEE-开发框架-SpringBoot&路由&传参 2、安全开发-JavaEE-模版引擎-Thymeleaf&Freemarker&Velocity 一、开发框架-SpringBoot 参考&#xff1a;https://springdoc.cn/spring-boot/ 访问SpringBoot创建的网站 1、路由映射 RequestMapping…

数据集笔记:NUSMods API

1 介绍 NUSMods API 包含用于渲染 NUSMods 的数据。这些数据包括新加坡国立大学&#xff08;NUS&#xff09;提供的课程以及课程表的信息&#xff0c;还包括上课地点的详细信息。 可以使用并实验这些数据&#xff0c;它们是从教务处提供的官方 API 中提取的。 该 API 由静态的…

SpringBoot新闻推荐系统设计与实现

随着信息时代的快速发展&#xff0c;新闻推荐系统成为用户获取个性化内容的重要工具。本文将介绍一个幽络源的基于SpringBoot开发的新闻推荐系统&#xff0c;该系统功能全面&#xff0c;操作简便&#xff0c;能够满足管理员和用户的多种需求。 管理员模块 管理员模块为系统管…

谷歌推出PaliGemma 2 mix:用于多任务的视觉语言模型,开箱即用。

去年 12 月&#xff0c;谷歌推出了 PaliGemma 2 &#xff0c;这是Gemma系列中的升级版视觉语言模型。该版本包含不同大小&#xff08;3B、10B 和 28B 参数&#xff09;的预训练检查点&#xff0c;可轻松针对各种视觉语言任务和领域进行微调&#xff0c;例如图像分割、短视频字幕…

linux中断调用流程(arm)

文章目录 ARM架构下Linux中断处理全流程解析&#xff1a;从硬件触发到驱动调用 ⚡**一、中断触发与硬件层响应** &#x1f50c;**1. 设备触发中断** &#x1f4e1; **二、CPU阶段&#xff1a;异常入口与上下文处理** &#x1f5a5;️**1. 异常模式切换** &#x1f504;**2. 跳转…

250301-OpenWebUI配置DeepSeek-火山方舟+硅基流动+联网搜索+推理显示

A. 最终效果 B. 火山方舟配置&#xff08;一定要点击添加&#xff09; C. 硅基流动配置&#xff08;最好要点击添加&#xff0c;否则会自动弹出所有模型&#xff09; D. 联网搜索配置 E. 推理过程显示 默认是没有下面的推理过程的显示的 设置步骤&#xff1a; 在Functions函…

【算法】图论 —— Floyd算法 python

洛谷 B3647 【模板】Floyd 题目描述 给出一张由 n n n 个点 m m m 条边组成的无向图。 求出所有点对 ( i , j ) (i,j) (i,j) 之间的最短路径。 输入格式 第一行为两个整数 n , m n,m n,m&#xff0c;分别代表点的个数和边的条数。 接下来 m m m 行&#xff0c;每行三…