MyBatisPlus實現(xiàn)多租戶數(shù)據(jù)隔離
多租戶(Multi-tenancy)是一個常見的架構(gòu)需求,特別是在 SaaS應(yīng)用中。其核心目標(biāo)是在單個應(yīng)用實例中為多個租戶(客戶)提供服務(wù),同時確保他們的數(shù)據(jù)、配置和用戶體驗是隔離的。
一、多租戶的常見的三種模式
- 獨立數(shù)據(jù)庫,這是隔離級別最高、最安全的方案,為每個租戶創(chuàng)建獨立的、物理上隔離的數(shù)據(jù)庫
- 共享數(shù)據(jù)庫,獨立 Schema,在同一個數(shù)據(jù)庫實例中,為每個租戶創(chuàng)建獨立的 Schema,所有租戶共享一個數(shù)據(jù)庫實例,但每個租戶擁有自己的一套表結(jié)構(gòu)(Schema)
- 共享數(shù)據(jù)庫,共享 Schema,所有租戶共享同一個數(shù)據(jù)庫實例和同一套表結(jié)構(gòu)。通過在每張業(yè)務(wù)表中增加一個 tenant_id 字段來區(qū)分不同租戶的數(shù)據(jù)。這是最經(jīng)濟(jì)、資源利用率最高的方案,也是最常見的 SaaS 多租戶模式
今天我們介紹是第三種方案——在同一個數(shù)據(jù)庫的同一張表中,通過tenant_id字段實現(xiàn)數(shù)據(jù)隔離。
二、MyBatisPlus多租戶原理解析
核心思想:SQL自動改寫
MyBatisPlus通過攔截器機(jī)制,在SQL執(zhí)行前自動加上租戶條件:
// 你寫的SQL: SELECT * FROM sys_user WHERE status = 1; // MyBatisPlus自動改寫的SQL: SELECT * FROM sys_user WHERE status = 1 AND tenant_id = 'T001';
關(guān)鍵技術(shù)點
- TenantLineHandler:租戶處理器,決定租戶值怎么取、哪些表要過濾等
- TenantLineInnerInterceptor:多租戶攔截器,SQL攔截和改寫核心邏輯
- Ignore注解:標(biāo)記不需要自動添加租戶條件的方法
三、案例
環(huán)境準(zhǔn)備
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>數(shù)據(jù)庫表結(jié)構(gòu):
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, -- 租戶標(biāo)識字段
created_time DATETIME DEFAULT CURRENT_TIMESTAMP
);核心代碼實現(xiàn)
配置文件
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租戶上下文管理:
/**
* 租戶上下文:用于在同一個線程內(nèi)傳遞租戶信息
*/
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();
// 創(chuàng)建租戶攔截器實例
TenantLineInnerInterceptor tenantInterceptor = new TenantLineInnerInterceptor();
// 設(shè)置租戶處理器
tenantInterceptor.setTenantLineHandler(new TenantLineHandler() {
// 獲取當(dāng)前租戶ID
@Override
public Expression getTenantId() {
String tenantId = TenantContext.getTenantId();
if (tenantId == null) {
throw new RuntimeException("租戶ID不能為空");
}
return new StringValue(tenantId);
}
// 租戶ID對應(yīng)的字段名
@Override
public String getTenantIdColumn() {
return "tenant_id";
}
// 默認(rèn)忽略租戶隔離的表(如系統(tǒng)配置表)
@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/**", // 排除認(rèn)證接口
"/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> {
}
業(yè)務(wù)服務(wù)層
@Service
@Slf4j
public class OrderService {
@Autowired
private OrderMapper orderMapper;
/**
* 創(chuàng)建訂單 - 會自動注入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("創(chuàng)建訂單成功,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;
/**
* 創(chuàng)建訂單接口
*/
@PostMapping
public ResponseEntity<String> createOrder(@RequestBody CreateOrderRequest request) {
try {
orderService.createOrder(request.getOrderNo(), request.getAmount());
return ResponseEntity.ok("訂單創(chuàng)建成功");
} catch (Exception e) {
log.error("創(chuàng)建訂單失敗", e);
return ResponseEntity.status(500).body("訂單創(chuàng)建失敗");
}
}
/**
* 查詢訂單列表
*/
@GetMapping("/list")
public List<Order> getOrders() {
return orderService.getOrders();
}
@Data
public static class CreateOrderRequest {
private String orderNo;
private BigDecimal amount;
}
}
測試驗證
模擬租戶A的請求創(chuàng)建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的請求創(chuàng)建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個訂單
到此這篇關(guān)于MyBatisPlus實現(xiàn)多租戶數(shù)據(jù)隔離的文章就介紹到這了,更多相關(guān)MyBatisPlus 多租戶數(shù)據(jù)隔離內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringCloud?Stream?整合RabbitMQ的基本步驟
這篇文章主要介紹了SpringCloud?Stream?整合RabbitMQ的基本步驟,從項目介紹到生產(chǎn)者結(jié)合示例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-03-03
Java快速實現(xiàn)Word轉(zhuǎn)圖片功能的多種方法與實踐
在軟件開發(fā)中,轉(zhuǎn)換文件格式是一個常見需求,尤其是將Word文件轉(zhuǎn)換為圖片格式,這在報表生成、文檔預(yù)覽、自動化處理等場景中非常有用,在 Java 中實現(xiàn)Word轉(zhuǎn)圖片的功能有多種思路,以下將介紹幾種常見的實現(xiàn)方式,并探討它們的優(yōu)缺點,需要的朋友可以參考下2025-08-08
Spring?@Bean?修飾方法時注入?yún)?shù)的操作方法
對于 Spring 而言,IOC 容器中的 Bean 對象的創(chuàng)建和使用是一大重點,Spring 也為我們提供了注解方式創(chuàng)建 bean 對象:使用 @Bean,這篇文章主要介紹了Spring?@Bean?修飾方法時如何注入?yún)?shù),需要的朋友可以參考下2023-10-10

