MyBatisPlus实现多租户数据隔离

 更新时间:2026年06月21日 10:59:43   作者:yifanghub  
多租户是一个常见的架构需求,特别是在 SaaS应用中,其核心目标是在单个应用实例中为多个租户提供服务,同时确保他们的数据、配置和用户体验是隔离的,本文就来详细的介绍一下多租户数据隔离,需要的朋友可以参考下

多租户(Multi-tenancy)是一个常见的架构需求,特别是在 SaaS应用中。其核心目标是在单个应用实例中为多个租户(客户)提供服务,同时确保他们的数据、配置和用户体验是隔离的。

一、多租户的常见的三种模式

  1. 独立数据库,这是隔离级别最高、最安全的方案,为每个租户创建独立的、物理上隔离的数据库
  2. 共享数据库,独立 Schema,在同一个数据库实例中,为每个租户创建独立的 Schema,所有租户共享一个数据库实例,但每个租户拥有自己的一套表结构(Schema)
  3. 共享数据库,共享 Schema,所有租户共享同一个数据库实例和同一套表结构。通过在每张业务表中增加一个 tenant_id 字段来区分不同租户的数据。这是最经济、资源利用率最高的方案,也是最常见的 SaaS 多租户模式

今天我们介绍是第三种方案——在同一个数据库的同一张表中,通过tenant_id字段实现数据隔离。

二、MyBatisPlus多租户原理解析

核心思想:SQL自动改写

MyBatisPlus通过拦截器机制,在SQL执行前自动加上租户条件:

// 你写的SQL:
SELECT * FROM sys_user WHERE status = 1;
// MyBatisPlus自动改写的SQL:  
SELECT * FROM sys_user WHERE status = 1 AND tenant_id = 'T001';

关键技术点

  • TenantLineHandler:租户处理器,决定租户值怎么取、哪些表要过滤等
  • TenantLineInnerInterceptor:多租户拦截器,SQL拦截和改写核心逻辑
  • Ignore注解:标记不需要自动添加租户条件的方法

三、案例

环境准备

pom.xml依赖:

<dependencies>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.3.1</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.33</version>
    </dependency>
    <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-extension</artifactId>
            <version>3.5.3</version>
        </dependency>
</dependencies>

数据库表结构:

CREATE TABLE orders
(
    id           BIGINT PRIMARY KEY AUTO_INCREMENT,
    order_no     VARCHAR(64)    NOT NULL,
    amount       DECIMAL(10, 2) NOT NULL,
    tenant_id    VARCHAR(32)    NOT NULL, -- 租户标识字段
    created_time DATETIME DEFAULT CURRENT_TIMESTAMP
);

核心代码实现

配置文件

server:
  port: 8080
spring:
  application:
    name: multitenant
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://xxx:3306/test1?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&serverTimezone=GMT%2B8
    username: admin
    password: 123456
  sql:
    init:
      schema-locations: classpath:db/init.sql
      mode: always
mybatis:
  mapper-locations: classpath:/mapper/*.xml
logging:
  level:
    com:
      example: debug

租户上下文管理:

/**
 * 租户上下文:用于在同一个线程内传递租户信息
 */
public class TenantContext {
    private static final ThreadLocal<String> CURRENT_TENANT = new ThreadLocal<>();

    public static void setTenantId(String tenantId) {
        CURRENT_TENANT.set(tenantId);
    }

    public static String getTenantId() {
        return CURRENT_TENANT.get();
    }

    public static void clear() {
        CURRENT_TENANT.remove();
    }
}

MyBatisPlus多租户配置:

@Configuration
public class MybatisPlusConfig {
    /**
     * 多租户拦截器
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

        // 创建租户拦截器实例
        TenantLineInnerInterceptor tenantInterceptor = new TenantLineInnerInterceptor();

        // 设置租户处理器
        tenantInterceptor.setTenantLineHandler(new TenantLineHandler() {

            // 获取当前租户ID
            @Override
            public Expression getTenantId() {
                String tenantId = TenantContext.getTenantId();
                if (tenantId == null) {
                    throw new RuntimeException("租户ID不能为空");
                }
                return new StringValue(tenantId);
            }

            // 租户ID对应的字段名
            @Override
            public String getTenantIdColumn() {
                return "tenant_id";
            }

            // 默认忽略租户隔离的表(如系统配置表)
            @Override
            public boolean ignoreTable(String tableName) {
                return "system_config".equals(tableName) ||
                        "tenant_info".equals(tableName);
            }
        });

        interceptor.addInnerInterceptor(tenantInterceptor);
        return interceptor;
    }
}

拦截器注册

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private TenantInterceptor tenantInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(tenantInterceptor)
                .addPathPatterns("/api/**")           // 拦截所有API接口
                .excludePathPatterns(
                        "/api/public/**",                 // 排除公共接口
                        "/api/auth/**",                   // 排除认证接口
                        "/error"                         // 排除错误页面
                );
    }
}

实体类与Mapper:

/**
 * 订单实体(注意:不需要显式定义tenant_id字段)
 */
@Data
@TableName("orders")
public class Order {
    private Long id;
    private String orderNo;
    private BigDecimal amount;
    private LocalDateTime createdTime;
    // 不需要定义tenant_id,MyBatisPlus会自动处理
}
/**
 * 订单Mapper
 */
@Mapper
public interface OrderMapper extends BaseMapper<Order> {

}

业务服务层

@Service
@Slf4j
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;
    

    /**
     * 创建订单 - 会自动注入tenant_id
     */
    @Transactional
    public void createOrder(String orderNo, BigDecimal amount) {
        Order order = new Order();
        order.setOrderNo(orderNo);
        order.setAmount(amount);
        order.setCreatedTime(LocalDateTime.now());

        int result = orderMapper.insert(order);
        log.info("创建订单成功,ID: {}", order.getId());

    }

    /**
     * 查询订单列表
     */
    public List<Order> getOrders() {

        log.info("租户[{}]查询订单", TenantContext.getTenantId());
        List<Order> orders = orderMapper.selectList(null);

        return orders;
    }
}

接口层

@RestController
@RequestMapping("/api/orders")
@Slf4j
public class OrderController {

    @Autowired
    private OrderService orderService;

    /**
     * 创建订单接口
     */
    @PostMapping
    public ResponseEntity<String> createOrder(@RequestBody CreateOrderRequest request) {
        try {
            orderService.createOrder(request.getOrderNo(), request.getAmount());
            return ResponseEntity.ok("订单创建成功");
        } catch (Exception e) {
            log.error("创建订单失败", e);
            return ResponseEntity.status(500).body("订单创建失败");
        }
    }

    /**
     * 查询订单列表
     */
    @GetMapping("/list")
    public List<Order> getOrders() {
        return orderService.getOrders();
    }



    @Data
    public static class CreateOrderRequest {
        private String orderNo;
        private BigDecimal amount;
    }
}

测试验证

模拟租户A的请求创建2个订单

curl --location --request POST 'http://localhost:8080/api/orders' \
--header 'X-Tenant-ID: 001' \
--header 'Content-Type: application/json' \
--data-raw '{
    "orderNo":"A_001",
    "amount":"100"
}'
curl --location --request POST 'http://localhost:8080/api/orders' \
--header 'X-Tenant-ID: 001' \
--header 'Content-Type: application/json' \
--data-raw '{
    "orderNo":"A_001",
    "amount":"120"
}'

模拟租户B的请求创建1个订单

curl --location --request POST 'http://localhost:8080/api/orders' \
--header 'X-Tenant-ID: 002' \
--header 'Content-Type: application/json' \
--data-raw '{
    "orderNo":"B_001",
    "amount":"150"
}'

查询租户A订单及租户B订单

curl --location --request GET 'http://localhost:8080/api/orders/list' \
--header 'X-Tenant-ID: 001'
curl --location --request GET 'http://localhost:8080/api/orders/list' \
--header 'X-Tenant-ID: 002'

可以看到租户A返回了2个订单,租户B只返回了1个订单

到此这篇关于MyBatisPlus实现多租户数据隔离的文章就介绍到这了,更多相关MyBatisPlus 多租户数据隔离内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Mybatis SQL运行流程源码详解

    Mybatis SQL运行流程源码详解

    这篇文章主要介绍了Mybatis SQL运行流程源码详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-10-10
  • SpringCloud Stream 整合RabbitMQ的基本步骤

    SpringCloud Stream 整合RabbitMQ的基本步骤

    这篇文章主要介绍了SpringCloud Stream 整合RabbitMQ的基本步骤,从项目介绍到生产者结合示例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-03-03
  • Java基础知识之Java语言概述

    Java基础知识之Java语言概述

    这篇文章主要介绍了Java基础知识之Java语言概述,本文介绍了Java语言相关的基础知识、历史介绍、主要应用方向等内容,需要的朋友可以参考下
    2015-03-03
  • Java快速实现Word转图片功能的多种方法与实践

    Java快速实现Word转图片功能的多种方法与实践

    在软件开发中,转换文件格式是一个常见需求,尤其是将Word文件转换为图片格式,这在报表生成、文档预览、自动化处理等场景中非常有用,在 Java 中实现Word转图片的功能有多种思路,以下将介绍几种常见的实现方式,并探讨它们的优缺点,需要的朋友可以参考下
    2025-08-08
  • Java之Arrays的各种功能和用法总结

    Java之Arrays的各种功能和用法总结

    数组在 Java 中是一种常用的数据结构,用于存储和操作大量数据。Arrays 是我们在处理数组时的一把利器。它提供了丰富的方法和功能,使得数组操作变得更加简单、高效和可靠。接下来我们一起看看 Arrays 的各种功能和用法,,需要的朋友可以参考下
    2023-05-05
  • Java内存溢出实现原因及解决方案

    Java内存溢出实现原因及解决方案

    这篇文章主要介绍了Java内存溢出实现原因及解决方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-03-03
  • Java代码读取文件缓存问题解决

    Java代码读取文件缓存问题解决

    最近遇到了一个Java文件读取的缓存问题,打远程断点出现的也是原来的老代码参数,本文就介绍一下解决方法,感兴趣的可以了解一下
    2021-05-05
  • Java利用位运算实现加减运算详解

    Java利用位运算实现加减运算详解

    这篇文章主要为大家介绍了如何使用位运算来实现加减功能,也就是在整个运算过程中不能出现加减符号。文中的示例代码讲解详细,感兴趣的可以了解一下
    2022-12-12
  • JVM垃圾回收分配及算法使用说明

    JVM垃圾回收分配及算法使用说明

    这篇文章主要介绍了Java垃圾回收机制,包括判断对象是否可以回收、堆空间分配(年轻代、老年代及对应回收算法)、永久代(元空间)的管理和其他垃圾回收算法(复制算法、标记-整理算法)
    2025-12-12
  • Spring @Bean 修饰方法时注入参数的操作方法

    Spring @Bean 修饰方法时注入参数的操作方法

    对于 Spring 而言,IOC 容器中的 Bean 对象的创建和使用是一大重点,Spring 也为我们提供了注解方式创建 bean 对象:使用 @Bean,这篇文章主要介绍了Spring @Bean 修饰方法时如何注入参数,需要的朋友可以参考下
    2023-10-10

最新评论