java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > MyBatisPlus 多租户数据隔离

MyBatisPlus实现多租户数据隔离

作者: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';

关键技术点

三、案例

环境准备

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 多租户数据隔离内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文