SpringBoot集成Mybatis-Plus多租戶架構(gòu)實(shí)現(xiàn)
目前公司產(chǎn)品就是對(duì)外企業(yè)服務(wù),入職后了解到SaaS模式和私有部署,當(dāng)我第一次聽(tīng)到SaaS時(shí),我不是很理解。經(jīng)過(guò)查閱資料,以及在后續(xù)研發(fā)功能時(shí),不斷的加深了對(duì)多租戶的理解。
那么接下來(lái)讓我們問(wèn)自己幾個(gè)問(wèn)題:
1.什么是多租戶架構(gòu)?
2.多租戶架構(gòu)方案以及各自的優(yōu)缺點(diǎn)?
3.多租戶架構(gòu)的適用場(chǎng)景?
一. 什么是多租戶
多租戶技術(shù)或稱多重租賃技術(shù),簡(jiǎn)稱SaaS,是一種軟件架構(gòu)技術(shù),是實(shí)現(xiàn)如何在多用戶環(huán)境下(多用戶一般是面向企業(yè)用戶)共用相同的系統(tǒng)或程序組件,并且可確保各用戶間數(shù)據(jù)的隔離性。簡(jiǎn)單講:在一臺(tái)服務(wù)器上運(yùn)行單個(gè)應(yīng)用實(shí)例,它為多個(gè)租戶(客戶)提供服務(wù)。從定義中我們可以理解:多租戶是一種架構(gòu),目的是為了讓多用戶環(huán)境下使用同一套程序,且保證用戶間數(shù)據(jù)隔離。那么重點(diǎn)就很淺顯易懂了,多租戶的重點(diǎn)就是同一套程序下實(shí)現(xiàn)多用戶數(shù)據(jù)的隔離。
二. 多租戶架構(gòu)以及數(shù)據(jù)隔離方案
多租戶在數(shù)據(jù)存儲(chǔ)上主要存在三種方案,分別是:
1. 獨(dú)立數(shù)據(jù)庫(kù)
即一個(gè)租戶一個(gè)數(shù)據(jù)庫(kù),這種方案的用戶數(shù)據(jù)隔離級(jí)別最高,安全性最好,但成本較高。
- 優(yōu)點(diǎn):為不同的租戶提供獨(dú)立的數(shù)據(jù)庫(kù),有助于簡(jiǎn)化數(shù)據(jù)模型的擴(kuò)展設(shè)計(jì),滿足不同租戶的獨(dú)特需求;如果出現(xiàn)故障,恢復(fù)數(shù)據(jù)比較簡(jiǎn)單。
- 缺點(diǎn):增多了數(shù)據(jù)庫(kù)的安裝數(shù)量,隨之帶來(lái)維護(hù)成本和購(gòu)置成本的增加。
2. 共享數(shù)據(jù)庫(kù),獨(dú)立 Schema
也就是說(shuō) 共同使用一個(gè)數(shù)據(jù)庫(kù) 使用表進(jìn)行數(shù)據(jù)隔離
多個(gè)或所有租戶共享Database,但是每個(gè)租戶一個(gè)Schema(也可叫做一個(gè)user)。底層庫(kù)比如是:DB2、ORACLE等,一個(gè)數(shù)據(jù)庫(kù)下可以有多個(gè)SCHEMA。
優(yōu)點(diǎn):為安全性要求較高的租戶提供了一定程度的邏輯數(shù)據(jù)隔離,并不是完全隔離;每個(gè)數(shù)據(jù)庫(kù)可支持更多的租戶數(shù)量。
缺點(diǎn):如果出現(xiàn)故障,數(shù)據(jù)恢復(fù)比較困難,因?yàn)榛謴?fù)數(shù)據(jù)庫(kù)將牽涉到其他租戶的數(shù)據(jù);
3. 共享數(shù)據(jù)庫(kù),共享 Schema,共享數(shù)據(jù)表
也就是說(shuō) 共同使用一個(gè)數(shù)據(jù)庫(kù)一個(gè)表 使用字段進(jìn)行數(shù)據(jù)隔離
即租戶共享同一個(gè)Database、同一個(gè)Schema,但在表中增加TenantID多租戶的數(shù)據(jù)字段。這是共享程度最高、隔離級(jí)別最低的模式。
簡(jiǎn)單來(lái)講,即每插入一條數(shù)據(jù)時(shí)都需要有一個(gè)客戶的標(biāo)識(shí)。這樣才能在同一張表中區(qū)分出不同客戶的數(shù)據(jù),這也是我們系統(tǒng)目前用到的(tenant_id)
- 優(yōu)點(diǎn):三種方案比較,第三種方案的維護(hù)和購(gòu)置成本最低,允許每個(gè)數(shù)據(jù)庫(kù)支持的租戶數(shù)量最多。
- 缺點(diǎn):隔離級(jí)別最低,安全性最低,需要在設(shè)計(jì)開發(fā)時(shí)加大對(duì)安全的開發(fā)量;數(shù)據(jù)備份和恢復(fù)最困難,需要逐表逐條備份和還原。
三.多租戶架構(gòu)適用場(chǎng)景?
衡量三種模式主要考慮的因素是隔離還是共享。
1.成本角度因素
隔離性越好,設(shè)計(jì)和實(shí)現(xiàn)的難度和成本越高,初始成本越高。共享性越好,同一運(yùn)營(yíng)成本下支持的用戶越多,運(yùn)營(yíng)成本越低。
2.安全因素
要考慮業(yè)務(wù)和客戶的安全方面的要求。安全性要求越高,越要傾向于隔離。
3.從租戶數(shù)量上考慮
主要考慮下面一些因素
- 系統(tǒng)要支持多少租戶?上百?上千還是上萬(wàn)?可能的租戶越多,越傾向于共享。
- 平均每個(gè)租戶要存儲(chǔ)數(shù)據(jù)需要的空間大小。存貯的數(shù)據(jù)越多,越傾向于隔離。
- 每個(gè)租戶的同時(shí)訪問(wèn)系統(tǒng)的最終用戶數(shù)量。需要支持的越多,越傾向于隔離。
- 是否想針對(duì)每一租戶提供附加的服務(wù),例如數(shù)據(jù)的備份和恢復(fù)等。這方面的需求越多, 越傾向于隔離
4.技術(shù)儲(chǔ)備
共享性越高,對(duì)技術(shù)的要求越高。
四. 技術(shù)實(shí)現(xiàn)
技術(shù)選型: Mybatis-Plus
這里我們選用了第三種方案(共享數(shù)據(jù)庫(kù),共享 Schema,共享數(shù)據(jù)表)來(lái)實(shí)現(xiàn),也就意味著,每個(gè)數(shù)據(jù)表都需要有一個(gè)租戶標(biāo)識(shí)(tenant_id)
現(xiàn)在有數(shù)據(jù)庫(kù)表(user)如下:
| 字段名 | 字段類型 | 描述 |
|---|---|---|
| id | int(11) | 主鍵 |
| name | varchar(30) | 姓名 |
| tenant_id | int(11) | 多租戶id |
將tenant_id視為租戶ID,用來(lái)隔離租戶與租戶之間的數(shù)據(jù),如果要查詢當(dāng)前租戶的用戶,SQL大致如下:
SELECT * FROM user WHERE tenant_id = 1;
試想一下,除了一些系統(tǒng)共用的表以外,其他租戶相關(guān)的表,我們都需要加上AND tenant_id = ?查詢條件,數(shù)據(jù)表多的情況時(shí)就會(huì)漏加導(dǎo)致數(shù)據(jù)泄露。
幸虧有mybatis-plus這個(gè)插件,可以極為方便的實(shí)現(xiàn)多租戶SQL解析器,官方文檔如下:
多租戶 SQL 解析器
正式進(jìn)入主題
環(huán)境搭建演示
1. 創(chuàng)建Spring Boot項(xiàng)目
pom文件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.7.RELEASE</version>
<relativePath/>
</parent>
<groupId>com.xd</groupId>
<artifactId>mybatis-plus-multi-tenancy</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>mybatis-plus-multi-tenancy</name>
<description>基于Spring Boot Mybatis-Plus的多租戶架構(gòu)</description>
<properties>
<java.version>1.8</java.version>
<mybatis-plus.version>3.1.2</mybatis-plus.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--Mybatis-Plus依賴-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!--測(cè)試相關(guān)依賴-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.2.0.M1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.properties
# 數(shù)據(jù)源配置 spring.datasource.type=com.zaxxer.hikari.HikariDataSource spring.datasource.hikari.minimum-idle=3 spring.datasource.hikari.maximum-pool-size=10 # 不能小于30秒,否則默認(rèn)回到1800秒 spring.datasource.hikari.max-lifetime=30000 spring.datasource.hikari.connection-test-query=SELECT 1 spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.datasource.url=jdbc:mysql://localhost:3306/multi?useUnicode=true&characterEncoding=UTF-8 spring.datasource.username=root spring.datasource.password=root logging.level.com.xd.mybatisplusmultitenancy=debug
對(duì)應(yīng)的SQL數(shù)據(jù)庫(kù)初始化schema文件
SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for user -- ---------------------------- DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵', `name` varchar(30) DEFAULT NULL COMMENT '姓名', `tenant_id` int(11) NOT NULL COMMENT '多租戶ID', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; SET FOREIGN_KEY_CHECKS = 1;
MybatisPlusConfig
核心配置:TenantSqlParser
多租戶處理器
package com.xd.mybatisplusmultitenancy.config;
import com.baomidou.mybatisplus.extension.plugins.tenant.TenantHandler;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.expression.NullValue;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* @Classname PreTenantHandler
* @Description 租戶處理器 -主要實(shí)現(xiàn)mybatis-plus https://mp.baomidou.com/guide/tenant.html
* @Author Created by Lihaodong (alias:小東啊) lihaodongmail@163.com
* @Date 2019-08-09 23:34
* @Version 1.0
*/
@Slf4j
@Component
public class MyTenantHandler implements TenantHandler {
/**
* 多租戶標(biāo)識(shí)
*/
private static final String SYSTEM_TENANT_ID = "tenant_id";
/**
* 需要過(guò)濾的表
*/
private static final List<String> IGNORE_TENANT_TABLES = new ArrayList<>();
@Autowired
private MyContext apiContext;
/**
* 租戶Id
*
* @return
*/
@Override
public Expression getTenantId() {
// 從當(dāng)前系統(tǒng)上下文中取出當(dāng)前請(qǐng)求的服務(wù)商ID,通過(guò)解析器注入到SQL中。
Long tenantId = apiContext.getCurrentTenantId();
log.debug("當(dāng)前租戶為{}", tenantId);
if (tenantId == null) {
return new NullValue();
}
return new LongValue(tenantId);
}
/**
* 租戶字段名
*
* @return
*/
@Override
public String getTenantIdColumn() {
return SYSTEM_TENANT_ID;
}
/**
* 根據(jù)表名判斷是否進(jìn)行過(guò)濾
* 忽略掉一些表:如租戶表(sys_tenant)本身不需要執(zhí)行這樣的處理
*
* @param tableName
* @return
*/
@Override
public boolean doTableFilter(String tableName) {
return IGNORE_TENANT_TABLES.stream().anyMatch((e) -> e.equalsIgnoreCase(tableName));
}
}
MybatisPlus的配置
package com.xd.mybatisplusmultitenancy.config;
import com.baomidou.mybatisplus.core.parser.ISqlParser;
import com.baomidou.mybatisplus.extension.parsers.BlockAttackSqlParser;
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.extension.plugins.PerformanceInterceptor;
import com.baomidou.mybatisplus.extension.plugins.tenant.TenantHandler;
import com.baomidou.mybatisplus.extension.plugins.tenant.TenantSqlParser;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import java.util.ArrayList;
import java.util.List;
/**
* @Classname MybatisPlusConfig
* @Description TODO
* @Author Created by Lihaodong (alias:小東啊) lihaodongmail@163.com
* @Date 2019-08-09 22:44
* @Version 1.0
*/
@Configuration
@MapperScan("com.xd.mybatisplusmultitenancy.mapper")
public class MybatisPlusConfig {
@Autowired
private MyTenantHandler myTenantHandler;
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
// SQL解析處理攔截:增加租戶處理回調(diào)。
List<ISqlParser> sqlParserList = new ArrayList<>();
// 攻擊 SQL 阻斷解析器、加入解析鏈
sqlParserList.add(new BlockAttackSqlParser());
// 多租戶攔截
TenantSqlParser tenantSqlParser = new TenantSqlParser();
tenantSqlParser.setTenantHandler(myTenantHandler);
sqlParserList.add(tenantSqlParser);
paginationInterceptor.setSqlParserList(sqlParserList);
return paginationInterceptor;
}
/**
* 性能分析攔截器,不建議生產(chǎn)使用
* 用來(lái)觀察 SQL 執(zhí)行情況及執(zhí)行時(shí)長(zhǎng)
*/
@Bean(name = "performanceInterceptor")
public PerformanceInterceptor performanceInterceptor() {
return new PerformanceInterceptor();
}
}
自定義系統(tǒng)的上下文
package com.xd.mybatisplusmultitenancy.config;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @Classname ApiContext
* @Description 當(dāng)前系統(tǒng)的上下文
* @Author Created by Lihaodong (alias:小東啊) lihaodongmail@163.com
* @Date 2019-08-09 22:47
* @Version 1.0
*/
@Component
public class MyContext {
private static final String KEY_CURRENT_TENANT_ID = "KEY_CURRENT_PROVIDER_ID";
private static final Map<String, Object> M_CONTEXT = new ConcurrentHashMap<>();
public void setCurrentTenantId(Long tenantId) {
M_CONTEXT.put(KEY_CURRENT_TENANT_ID, tenantId);
}
public Long getCurrentTenantId() {
return (Long) M_CONTEXT.get(KEY_CURRENT_TENANT_ID);
}
}
Entity、Mapper 省略...

2. 單元測(cè)試
package com.xd.mybatisplusmultitenancy.test;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.xd.mybatisplusmultitenancy.MybatisPlusMultiTenancyApplication;
import com.xd.mybatisplusmultitenancy.config.MyContext;
import com.xd.mybatisplusmultitenancy.entity.User;
import com.xd.mybatisplusmultitenancy.mapper.UserMapper;
import lombok.extern.slf4j.Slf4j;
import org.junit.Assert;
import org.junit.Before;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.MethodSorters;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.sql.Wrapper;
/**
* @Classname MybatisPlusMultiTenancyApplicationTests
* @Description TODO
* @Author Created by Lihaodong (alias:小東啊) lihaodongmail@163.com
* @Date 2019-08-09 22:50
* @Version 1.0
*/
@Slf4j
@RunWith(SpringRunner.class)
@FixMethodOrder(MethodSorters.JVM)
@SpringBootTest(classes = MybatisPlusMultiTenancyApplication.class)
public class MybatisPlusMultiTenancyApplicationTests {
@Autowired
private MyContext apiContext;
@Autowired
private UserMapper userMapper;
/**
* 模擬當(dāng)前系統(tǒng)的多租戶Id
*/
@Before
public void before() {
// 在上下文中設(shè)置當(dāng)前多租戶id
apiContext.setCurrentTenantId(1L);
}
@Test
public void insert() {
// 新增數(shù)據(jù)
User user = new User().setName("小明");
//判斷一個(gè)條件是true還是false
Assert.assertTrue(userMapper.insert(user) > 0);
user = userMapper.selectById(user.getId());
log.info("插入數(shù)據(jù):{}", user);
// 判斷是否相等
Assert.assertEquals(apiContext.getCurrentTenantId(), user.getTenantId());
}
@Test
public void selectList() {
userMapper.selectList(null).forEach((e) -> {
log.info("查詢數(shù)據(jù){}", e);
Assert.assertEquals(apiContext.getCurrentTenantId(), e.getTenantId());
});
}
}
運(yùn)行結(jié)果
插入數(shù)據(jù)
2019-08-23 22:32:52.755 INFO 77902 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2019-08-23 22:32:53.210 INFO 77902 --- [ main] .MybatisPlusMultiTenancyApplicationTests : Started MybatisPlusMultiTenancyApplicationTests in 5.181 seconds (JVM running for 6.86)
2019-08-23 22:32:53.613 DEBUG 77902 --- [ main] c.x.m.config.MyTenantHandler : 當(dāng)前租戶為1
2019-08-23 22:32:53.614 DEBUG 77902 --- [ main] c.x.m.mapper.UserMapper.insert : ==> Preparing: INSERT INTO user (name, tenant_id) VALUES (?, 1)
2019-08-23 22:32:53.648 DEBUG 77902 --- [ main] c.x.m.mapper.UserMapper.insert : ==> Parameters: 小明(String)
2019-08-23 22:32:53.701 DEBUG 77902 --- [ main] c.x.m.mapper.UserMapper.insert : <== Updates: 1
Time:64 ms - ID:com.xd.mybatisplusmultitenancy.mapper.UserMapper.insert
Execute SQL:INSERT INTO user (name, tenant_id) VALUES ('小明', 1)2019-08-23 22:32:53.720 DEBUG 77902 --- [ main] c.x.m.config.MyTenantHandler : 當(dāng)前租戶為1
2019-08-23 22:32:53.722 DEBUG 77902 --- [ main] c.x.m.mapper.UserMapper.selectById : ==> Preparing: SELECT id, name, tenant_id FROM user WHERE user.tenant_id = 1 AND id = ?
2019-08-23 22:32:53.726 DEBUG 77902 --- [ main] c.x.m.mapper.UserMapper.selectById : ==> Parameters: 1(Long)
2019-08-23 22:32:53.745 DEBUG 77902 --- [ main] c.x.m.mapper.UserMapper.selectById : <== Total: 1
Time:20 ms - ID:com.xd.mybatisplusmultitenancy.mapper.UserMapper.selectById
Execute SQL:SELECT id, name, tenant_id FROM user WHERE user.tenant_id = 1 AND id = 12019-08-23 22:32:53.746 INFO 77902 --- [ main] .MybatisPlusMultiTenancyApplicationTests : 插入數(shù)據(jù):User(id=1, name=小明, tenantId=1)
2019-08-23 22:32:53.762 INFO 77902 --- [ Thread-2] o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService 'applicationTaskExecutor'
2019-08-23 22:32:53.764 INFO 77902 --- [ Thread-2] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated...
2019-08-23 22:32:53.777 INFO 77902 --- [ Thread-2] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed.
查詢數(shù)據(jù)
2019-08-23 22:34:26.700 INFO 77922 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2019-08-23 22:34:27.100 INFO 77922 --- [ main] .MybatisPlusMultiTenancyApplicationTests : Started MybatisPlusMultiTenancyApplicationTests in 4.521 seconds (JVM running for 6.268)
2019-08-23 22:34:27.412 DEBUG 77922 --- [ main] c.x.m.config.MyTenantHandler : 當(dāng)前租戶為1
2019-08-23 22:34:27.414 DEBUG 77922 --- [ main] c.x.m.mapper.UserMapper.selectList : ==> Preparing: SELECT id, name, tenant_id FROM user WHERE user.tenant_id = 1
2019-08-23 22:34:27.442 DEBUG 77922 --- [ main] c.x.m.mapper.UserMapper.selectList : ==> Parameters:
2019-08-23 22:34:27.464 DEBUG 77922 --- [ main] c.x.m.mapper.UserMapper.selectList : <== Total: 1
Time:22 ms - ID:com.xd.mybatisplusmultitenancy.mapper.UserMapper.selectList
Execute SQL:SELECT id, name, tenant_id FROM user WHERE user.tenant_id = 12019-08-23 22:34:27.467 INFO 77922 --- [ main] .MybatisPlusMultiTenancyApplicationTests : 查詢數(shù)據(jù)User(id=1, name=小明, tenantId=1)
2019-08-23 22:34:27.480 INFO 77922 --- [ Thread-2] o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService 'applicationTaskExecutor'
2019-08-23 22:34:27.482 INFO 77922 --- [ Thread-2] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated...
2019-08-23 22:34:27.492 INFO 77922 --- [ Thread-2] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed.
從打印的日志不難看出,目前這個(gè)方案還是比較完美的,僅需簡(jiǎn)單的配置,讓開發(fā)者極大方便的進(jìn)行開發(fā),同時(shí)又最大程度的保證了數(shù)據(jù)的安全性
源碼下載: https://github.com/LiHaodong888/SpringBootLearn
具體項(xiàng)目:https://gitee.com/li_haodong/pre

參考資料
https://www.cnblogs.com/pingfan21/p/7478242.html
https://segmentfault.com/a/1190000017197768
到此這篇關(guān)于SpringBoot集成Mybatis-Plus多租戶架構(gòu)實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)SpringBoot集成Mybatis-Plus多租戶內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java數(shù)據(jù)結(jié)構(gòu)與算法之棧(Stack)實(shí)現(xiàn)詳解
這篇文章主要為大家詳細(xì)介紹了Java數(shù)據(jù)結(jié)構(gòu)學(xué)習(xí)筆記第二篇,Java數(shù)據(jù)結(jié)構(gòu)與算法之棧Stack實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-09-09
IntelliJ IDEA多屏后窗口不顯示問(wèn)題解決方案
這篇文章主要介紹了IntelliJ IDEA多屏后窗口不顯示問(wèn)題解決方案,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-09-09
Java的stream流多個(gè)字段排序的實(shí)現(xiàn)
本文主要介紹了Java的stream流多個(gè)字段排序的實(shí)現(xiàn),主要是兩種方法,第一種是固定多個(gè)字段排序和第二種動(dòng)態(tài)字段進(jìn)行排序,具有一定的參考價(jià)值,感興趣的可以了解一下2023-10-10
Java中的異常處理(try,catch,finally,throw,throws)
本文主要介紹了Java中的異常處理,文章主要介紹的異常處理包括5個(gè)關(guān)鍵字try,catch,finally,throw,throws,更多詳細(xì)內(nèi)容需要的朋友可以參考一下2022-06-06
關(guān)于Mybatis-Plus字段策略與數(shù)據(jù)庫(kù)自動(dòng)更新時(shí)間的一些問(wèn)題
這篇文章主要介紹了關(guān)于Mybatis-Plus字段策略與數(shù)據(jù)庫(kù)自動(dòng)更新時(shí)間的一些問(wèn)題,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10
Spring中@PropertySource注解使用場(chǎng)景解析
這篇文章主要介紹了Spring中@PropertySource注解使用場(chǎng)景解析,@PropertySource注解就是Spring中提供的一個(gè)可以加載配置文件的注解,并且可以將配置文件中的內(nèi)容存放到Spring的環(huán)境變量中,需要的朋友可以參考下2023-11-11
SpringSecurity?用戶帳號(hào)已被鎖定的問(wèn)題及解決方法
這篇文章主要介紹了SpringSecurity?用戶帳號(hào)已被鎖定,本文給大家分享問(wèn)題原因及解決方式,需要的朋友可以參考下2023-12-12
讓Java后臺(tái)MySQL數(shù)據(jù)庫(kù)能夠支持emoji表情的方法
最近開發(fā)的iOS項(xiàng)目因?yàn)樾枰脩粑谋镜拇鎯?chǔ),自然就遇到了emoji等表情符號(hào)如何被mysql DB支持的問(wèn)題。下面這篇文章主要介紹了關(guān)于讓Java后臺(tái)MySQL數(shù)據(jù)庫(kù)能夠支持emoji表情的方法,需要的朋友可以參考下。2017-03-03

