MyBatisPlus实现多租户数据隔离
作者:yifanghub
多租户是一个常见的架构需求,特别是在 SaaS应用中,其核心目标是在单个应用实例中为多个租户提供服务,同时确保他们的数据、配置和用户体验是隔离的,本文就来详细的介绍一下多租户数据隔离,需要的朋友可以参考下
多租户(Multi-tenancy)是一个常见的架构需求,特别是在 SaaS应用中。其核心目标是在单个应用实例中为多个租户(客户)提供服务,同时确保他们的数据、配置和用户体验是隔离的。
一、多租户的常见的三种模式
- 独立数据库,这是隔离级别最高、最安全的方案,为每个租户创建独立的、物理上隔离的数据库
- 共享数据库,独立 Schema,在同一个数据库实例中,为每个租户创建独立的 Schema,所有租户共享一个数据库实例,但每个租户拥有自己的一套表结构(Schema)
- 共享数据库,共享 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 多租户数据隔离内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
