多租户技术(英语:multi-tenancy technology)或称多重租赁技术,是一种软件架构技术,它是在探讨与实现如何于多用户的环境下共用相同的系统或程序组件,并且仍可确保各用户间数据的隔离性。
多租户技术可以实现多个租户之间共享系统实例,同时又可以实现租户的系统实例的个性化定制。通过使用多租户技术可以保证系统共性的部分被共享,个性的部分被单独隔离。通过在多个租户之间的资源复用,运营管理维护资源,有效节省开发应用的成本。
多租户技术的实现重点,在于不同租户间应用程序环境的隔离(application context isolation)以及数据的隔离(data isolation),以维持不同租户间应用程序不会相互干扰,同时数据的保密性也够强。
在MybatisPlusConfig类中添加多租户插件
@Configuration
@MapperScan("com.example.demo.*")
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//多租户插件
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
@Override
public Expression getTenantId() {
return new LongValue(1);
}
// 这是 default 方法,默认返回 false 表示所有表都需要拼多租户条件
@Override
public boolean ignoreTable(String tableName) {
return !"users".equalsIgnoreCase(tableName);
}
}));
return interceptor;
}
}
实体类Users
/**
* 用户实体对应表 users
*/
@Data
@Accessors(chain = true)
public class Users {
private Long id;
/**
* 租户 ID
*/
private Long tenantId;
private String name;
@TableField(exist = false)
private String addrName;
}
UsersMapper
/**
* MP 支持不需要 UsersMapper.xml 这个模块演示内置 CRUD 咱们就不要 XML 部分了
*/
public interface UsersMapper extends BaseMapper<Users> {
/**
* 自定义SQL:默认也会增加多租户条件
* 参考打印的SQL
* @return
*/
Integer myCount();
List<Users> getUserAndAddr(@Param("username") String username);
List<Users> getAddrAndUser(@Param("name") String name);
}
UsersMapper.xml
DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.example.demo.repository.UsersMapper">
<select id="myCount" resultType="java.lang.Integer">
select count(1) from users
select>
<select id="getUserAndAddr" resultType="com.example.demo.entity.Users">
select u.id, u.name, a.name as addr_name
from users u
left join user_addr a on a.user_id=u.id
<where>
<if test="username!=null">
u.name like concat(concat('%',#{username}),'%')
if>
where>
select>
<select id="getAddrAndUser" resultType="com.example.demo.entity.Users">
select a.name as addr_name, u.id, u.name
from user_addr a
left join users u on u.id=a.user_id
<where>
<if test="name!=null">
a.name like concat(concat('%',#{name}),'%')
if>
where>
select>
mapper>
单元测试TenantTest
@SpringBootTest
public class TenantTest {
@Resource
private UsersMapper mapper;
@Test
public void aInsert() {
Users user = new Users();
user.setName("一一");
Assertions.assertTrue(mapper.insert(user) > 0);
user = mapper.selectById(user.getId());
Assertions.assertTrue(1 == user.getTenantId());
}
@Test
public void bDelete() {
Assertions.assertTrue(mapper.deleteById(3L) > 0);
}
@Test
public void cUpdate() {
Assertions.assertTrue(mapper.updateById(new Users().setId(1L).setName("mp")) > 0);
}
@Test
public void dSelect() {
List<Users> userList = mapper.selectList(null);
userList.forEach(u -> Assertions.assertTrue(1 == u.getTenantId()));
}
/**
* 自定义SQL:默认也会增加多租户条件
* 参考打印的SQL
*/
@Test
public void manualSqlTenantFilterTest() {
System.out.println(mapper.myCount());
}
@Test
public void testTenantFilter(){
mapper.getAddrAndUser(null).forEach(System.out::println);
mapper.getAddrAndUser("add").forEach(System.out::println);
mapper.getUserAndAddr(null).forEach(System.out::println);
mapper.getUserAndAddr("J").forEach(System.out::println);
}
}
执行aInsert()方法,发现Sql语句自动拼接了租户过滤条件
数据库结果
描述: Sql执行时,动态的修改表名
简单业务场景: 日志或者其他数据量大的表,通过日期进行了水平分表,需要通过日期参数,动态的查询数据。分开来存储,表中的列名都是一样的,只是表名不同。
比如:log_201907、log_201908等等之类的
创建基础表
DROP TABLE IF EXISTS user_2021;
CREATE TABLE user_2021
(
id BIGINT(20) NOT NULL COMMENT '主键ID',
name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
age INT(11) NULL DEFAULT NULL COMMENT '年龄',
email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',
PRIMARY KEY (id)
);
DROP TABLE IF EXISTS user_2022;
CREATE TABLE user_2022
(
id BIGINT(20) NOT NULL COMMENT '主键ID',
name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
age INT(11) NULL DEFAULT NULL COMMENT '年龄',
email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',
PRIMARY KEY (id)
);
DELETE FROM user_2021;
INSERT INTO user_2021 (id, name, age, email) VALUES
(1, 'Jone', 18, '[email protected]');
DELETE FROM user_2022;
INSERT INTO user_2022 (id, name, age, email) VALUES
(1, 'Jack', 20, '[email protected]');
动态表名插件DynamicTableNameInnerInterceptor
@Configuration
@MapperScan("com.example.demo.*")
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//动态表名插件
DynamicTableNameInnerInterceptor dynamicTableNameInnerInterceptor = new DynamicTableNameInnerInterceptor();
dynamicTableNameInnerInterceptor.setTableNameHandler((sql, tableName) -> {
String year = "_2022";
int random = new Random().nextInt(10);
if (random % 2 == 1) {
year = "_2021";
}
return tableName + year;
});
interceptor.addInnerInterceptor(dynamicTableNameInnerInterceptor);
return interceptor;
}
}
单元测试
@Test
void test() {
// 自己去观察打印 SQL 目前随机访问 newuser_2021 newuser_2022 表
for (int i = 0; i < 6; i++) {
NewUser user = newUserMapper.selectById(1);
System.err.println(user.getName());
}
}
观察打印 SQL 目前随机访问 newuser_2021 newuser_2022 表,实现了动态访问表的功能。
项目源码下载
参考文章
https://baomidou.com/pages/aef2f2/#tenantlineinnerinterceptor
https://blog.csdn.net/qq_43437874/article/details/114836714
https://blog.csdn.net/weixin_38111957/article/details/101196130