背景
SaaS(Software-as-a-Service),软件即服务,通过网络提供软件服务。
多租户(multi-tenancy technology),多重租赁技术,一种软件架构技术,是在探讨与实现如何于多用户的环境下共用相同的系统或程序组件,并且仍可确保各用户间数据的隔离性。
多租户实现方式:1、独立数据库、2、共享数据库但隔离数据架构、3、共享数据库且共享数据架构。
MybatisPlus多租户插件:com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor,实现了共享数据库且共享数据架构,提供了租户处理器( TenantId 行级 ),租户之间共享数据库,共享数据架构,通多表字段(租户ID)进行数据逻辑隔离。
代码
pom.xml
<dependencies>
<!-- spring boot start-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
<!--mybatis-plus代码生成器-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.1</version>
</dependency>
<!-- pagehelper分页 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.13</version>
</dependency>
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>3.1</version>
</dependency>
<!-- Driver -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.22</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<!-- lombok插件-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
application.yml
server:
port: 8080
tomcat:
uri-encoding: utf-8
servlet:
context-path: /demo
#数据源配置
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/demo?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&tinyInt1isBit=false&allowMultiQueries=true&serverTimezone=GMT%2B8
username: root
password: ********
type: com.alibaba.druid.pool.DruidDataSource
druid:
initial-size: 20
min-idle: 20
max-active: 50
max-wait: 60000
time-between-eviction-runsMillis: 60000
min-evictable-idle-timeMillis: 300000
validation-query: select 'x' FROM DUAL
test-on-borrow: false
test-on-return: false
max-open-prepared-statements: 20
max-pool-prepared-statement-per-connection-size: 20
filters:
stat:
# 数据库类型
db-type: mysql
# 慢SQL
log-slow-sql: true
# 慢SQL执行时间
slow-sql-millis: 1000
merge-sql: true
initialization-mode: always
connection-properties: druid.stat.mergeSql=true;druid.stat.sloSqlMillis=5000
logging:
level:
#com.what21.demo: error
com.what21.demo.mapper: debug
配置类:
package com.what21.demo.config;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import com.what21.demo.config.mybatis.MyTenantLineHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
// 多租户: TenantLineInnerInterceptor
TenantLineInnerInterceptor tenantLineInnerInterceptor = new TenantLineInnerInterceptor();
TenantLineHandler tenantLineHandler = new MyTenantLineHandler();
tenantLineInnerInterceptor.setTenantLineHandler(tenantLineHandler);
mybatisPlusInterceptor.addInnerInterceptor(tenantLineInnerInterceptor);
return mybatisPlusInterceptor;
}
}
MyTenantLineHandler
package com.what21.demo.config.mybatis;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.what21.demo.service.TenantServiceUtils;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.StringValue;
@Slf4j
public class MyTenantLineHandler implements TenantLineHandler {
/**
* 获取租户ID实际应该从用户信息中获取
*
* @return
*/
@Override
public Expression getTenantId() {
// 模拟ID
String userTenantId = TenantServiceUtils.getTenantId();
return new StringValue(userTenantId);
}
/**
* 获取租户表字段 默认为tenant_id
*
* @return
*/
@Override
public String getTenantIdColumn() {
return "tenant_id";
}
/**
* 表过滤,返回true,表示当前表不进行租户过滤
*
* @param tableName 表名
* @return
*/
@Override
public boolean ignoreTable(String tableName) {
log.info("==========================ignoreTable");
// 排除user表
return "user".equalsIgnoreCase(tableName);
}
}
TenantServiceUtils
package com.what21.demo.service;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
public class TenantServiceUtils {
/**
* @return
*/
public static String getTenantIdByHttpServletRequest() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
if (request == null) {
return null;
}
try {
return "1";
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* @return
*/
public static String getTenantId() {
return "2";
}
}
实体类
package com.what21.demo.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.Version;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("t_order2")
public class Order implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
// 订单编号
private String code;
// 所属用户
private Integer userId;
// 总价钱
private BigDecimal totalPrice;
// 备注
private String remark;
// 创建时间
private Date createTime;
// @Version
private Long version;
// 租户ID
private String tenantId;
}
Mapper类
package com.what21.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.what21.demo.model.Order;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface OrderMapper extends BaseMapper<Order> {
}
CREATE TABLE `t_order2` (
`id` int(12) NOT NULL AUTO_INCREMENT,
`code` varchar(100) COLLATE utf8_unicode_ci DEFAULT NULL,
`user_id` int(12) DEFAULT NULL,
`total_price` decimal(10,2) DEFAULT NULL,
`remark` text COLLATE utf8_unicode_ci,
`create_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
`version` bigint(18) DEFAULT NULL,
`tenant_id` varchar(12) COLLATE utf8_unicode_ci DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3101 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
多租户测试类
package com.what21.demo.mapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.what21.demo.model.Order;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
@RunWith(SpringRunner.class)
@SpringBootTest
public class OrderMapperTest {
@Autowired
OrderMapper orderMapper;
@Test
public void testInsert() {
for (int i = 0; i < 100; i++) {
Order newOrder = new Order();
newOrder.setCode("order" + i);
newOrder.setUserId(1);
newOrder.setTotalPrice(new BigDecimal(i));
newOrder.setRemark("备注");
newOrder.setCreateTime(new Date());
newOrder.setVersion(0L);
// 如果不指定值,默认TenantId插入为:2<租户标识>
// newOrder.setTenantId(String.valueOf((i%5)));
int operate = orderMapper.insert(newOrder);
System.out.println(operate);
// INSERT INTO t_order2 (code, user_id, total_price, remark, create_time, version, tenant_id) VALUES (?, ?, ?, ?, ?, ?, '2')
}
}
@Test
public void testUpdate() {
// SELECT id, code, user_id, total_price, remark, create_time, version, tenant_id FROM t_order2 WHERE id = ? AND tenant_id = '2'
Order order = orderMapper.selectById(3001);
Order newOrder = new Order();
newOrder.setId(3001);
newOrder.setRemark("......");
// UPDATE t_order2 SET remark = ? WHERE tenant_id = '2' AND id = ?
orderMapper.updateById(newOrder);
}
@Test
public void testSelect() {
QueryWrapper warpper = new QueryWrapper();
warpper.eq("user_id",1);
List<Order> orderList = orderMapper.selectList(warpper);
orderList.forEach(System.out::println);
}
}