多租户基于Springboot+MybatisPlus实现使用一个数据库一个表 使用字段进行数据隔离
多租户实现方式
多租户在数据存储上主要存在三种方案,分别是:
1. 独立数据库
即一个租户一个数据库,这种方案的用户数据隔离级别最高,安全性最好,但成本较高。
优点:为不同的租户提供独立的数据库,有助于简化数据模型的扩展设计,满足不同租户的独特需求;如果出现故障,恢复数据比较简单。
缺点:增多了数据库的安装数量,随之带来维护成本和购置成本的增加。
2. 共享数据库,独立 Schema
也就是说 共同使用一个数据库 使用表进行数据隔离
多个或所有租户共享Database,但是每个租户一个Schema(也可叫做一个user)。底层库比如是:DB2、ORACLE等,一个数据库下可以有多个SCHEMA。
优点:为安全性要求较高的租户提供了一定程度的逻辑数据隔离,并不是完全隔离;每个数据库可支持更多的租户数量。
缺点:如果出现故障,数据恢复比较困难,因为恢复数据库将牵涉到其他租户的数据;
3. 共享数据库,共享 Schema,共享数据表
也就是说 共同使用一个数据库一个表 使用字段进行数据隔离
即租户共享同一个Database、同一个Schema,但在表中增加TenantID多租户的数据字段。这是共享程度最高、隔离级别最低的模式。
简单来讲,即每插入一条数据时都需要有一个客户的标识。这样才能在同一张表中区分出不同客户的数据,这也是我们系统目前用到的(tenant_id)
优点:三种方案比较,第三种方案的维护和购置成本最低,允许每个数据库支持的租户数量最多。
缺点:隔离级别最低,安全性最低,需要在设计开发时加大对安全的开发量;数据备份和恢复最困难,需要逐表逐条备份和还原。
项目依赖Springboot+Mybatisplus
<?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">
<parent>
<artifactId>cloud</artifactId>
<groupId>com.gton</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ManyUser</artifactId>
<description>Spring Boot 集成 Mybatis-Plus 多租户架构实战</description>
<properties>
<knife4j.version>3.0.3</knife4j.version>
<mybatisplus.verison>3.5.1</mybatisplus.verison>
<mysql-version>8.0.25</mysql-version>
<fastJson-version>2.0.18</fastJson-version>
</properties>
<dependencies>
<!--web项目驱动-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot-start-version}</version>
</dependency>
<!--Knife4j(增强Swagger)-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>${knife4j.version}</version>
</dependency>
<!--Mybatis-plus 代码生成器-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatisplus.verison}</version>
</dependency>
<!--Mysql数据库-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql-version}</version>
</dependency>
<!--lombok-实体类简化依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastJson-version}</version>
</dependency>
</dependencies>
</project>
配置文件
application.properties
#数据源
spring.datasource.url=jdbc:mysql://120.53.238.87:3366/cloud_market?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=guotong199114
# 应用名称
spring.application.name=more-user-use
# 启动环境
spring.profiles.active=mybatis
# 应用服务 WEB 访问端口
server.port=8889
#Springboot2.6以上需要手动设置
spring.mvc.pathmatch.matching-strategy=ant_path_matcher
# 配置数据库连接池
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.hikari.minimum-idle=3
spring.datasource.hikari.maximum-pool-size=10
# 不能小于30秒,否则默认回到1800秒
spring.datasource.hikari.max-lifetime=30000
spring.datasource.hikari.connection-test-query=SELECT 1
mybatis.pwd.key=d1104d7c3b616f0b
application-mybatis.yaml
mybatis-plus:
type-aliases-package: com.gton.user.entity
mapper-locations: classpath*:com/gton/user/mapper/xml/*Mapper.xml,classpath*:/mapper/**/*.xml
configuration:
map-underscore-to-camel-case: true #开启驼峰命名
cache-enabled: false #开启二级缓存
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 控制台日志
check-config-location: true # 检查xml是否存在
type-enums-package: com.gton.enumPackage #通用枚举开启
global-config:
db-config:
logic-not-delete-value: 1
logic-delete-field: isDel
logic-delete-value: 0
测试表
CREATE TABLE `tenant_auth_login_user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键-自增',
`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户名',
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '密码:AES加密',
`rule` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '权限',
`sex` varchar(5) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '性别',
`head_portrait` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '头像',
`phone` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '电话',
`address` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '地址',
`email` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '邮箱',
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '个人介绍',
`is_del` int NOT NULL COMMENT '辑删除(0-标识删除,1-标识可用)',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_time` datetime NOT NULL COMMENT '修改时间',
`tenant_id` bigint NOT NULL COMMENT '多租户下的租户ID',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci STATS_PERSISTENT=1 COMMENT='多租户表';
实现多租户:参考官方
https://baomidou.com/pages/aef2f2/#tenantlineinnerinterceptor
第一步 implements TenantLineHandler
package com.gton.user.handler;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.schema.Column;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* @description: 租户处理器 -主要实现mybatis-plus TenantLineHandler
* <p>
* 如果用了分页插件注意先 add TenantLineInnerInterceptor
* 再 add PaginationInnerInterceptor
* 用了分页插件必须设置 MybatisConfiguration#useDeprecatedExecutor = false
* @author: GuoTong
* @createTime: 2023-06-22 16:43
* @since JDK 1.8 OR 11
**/
@Slf4j
@Component
public class SysTenantHandlerImpl implements TenantLineHandler {
/**
* 多租户标识
*/
private static final String SYSTEM_TENANT_ID = "tenant_id";
/**
* 需要过滤的表
*/
private static final List<String> IGNORE_TENANT_TABLES = new ArrayList<>();
/**
* 获取租户 ID 值表达式,只支持单个 ID 值
* <p>
*
* @return 租户 ID 值表达式
*/
@Override
public Expression getTenantId() {
// 获取当前租户信息
String tenantId = TenantRequestContext.getTenantLocal();
String requestUser = StringUtils.defaultIfEmpty(tenantId, "1001");
return new LongValue(requestUser);
}
/**
* 获取租户字段名
* <p>
* 默认字段名叫: tenant_id
*
* @return 租户字段名
*/
@Override
public String getTenantIdColumn() {
return SYSTEM_TENANT_ID;
}
/**
* 根据表名判断是否忽略拼接多租户条件
* <p>
* 默认都要进行解析并拼接多租户条件
*
* @param tableName 表名
* @return 是否忽略, true:表示忽略,false:需要解析并拼接多租户条件
*/
@Override
public boolean ignoreTable(String tableName) {
return IGNORE_TENANT_TABLES.contains(tableName);
}
@Override
public boolean ignoreInsert(List<Column> columns, String tenantIdColumn) {
// 新增排除自己携带了这个多租户字段的新增
for (Column column : columns) {
if (column.getColumnName().equalsIgnoreCase(tenantIdColumn)) {
return true;
}
}
return false;
}
}
建立一个基础类,用于同一个线程上下文变量恒定
package com.gton.user.handler;
/**
* @description: 保存当前请求用户的的信息,
* 使用threadlocal来实现,
* 和当前请求线程绑定
* @author: GuoTong
* @createTime: 2023-06-22 16:59
* @since JDK 1.8 OR 11
**/
public class TenantRequestContext {
private static ThreadLocal<String> tenantLocal = new ThreadLocal<>();
public static void setTenantLocal(String tenantId) {
tenantLocal.set(tenantId);
}
public static String getTenantLocal() {
return tenantLocal.get();
}
public static void remove() {
tenantLocal.remove();
}
}
第二步拦截器拦截请求,获取请求头里面的租户标识放入线程上下文
package com.gton.user.handler;
import com.gton.user.handler.TenantRequestContext;
import com.mysql.cj.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @description: 拦截器主要是获取请求头中的租户id,
* 然后放到上下文中,
* 供mybatisPlus获取
* @author: GuoTong
* @createTime: 2023-06-22 17:02
* @since JDK 1.8 OR 11
**/
@Slf4j
public class TenantUserInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String userId = request.getHeader("tenant_id");
if (!StringUtils.isNullOrEmpty(userId)) {
// 当前上下文的线程私有域注入多租户信息
TenantRequestContext.setTenantLocal(userId);
log.info("当前租户ID:" + userId);
}
return HandlerInterceptor.super.preHandle(request, response, handler);
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 当前上下文的线程私有域释放多租户信息
TenantRequestContext.remove();
}
}
注册拦截器到Spring容器中
/**
* @description: SpringBoot-Web配置
* @author: GuoTong
* @createTime: 2021-10-05 15:37
* @since JDK 1.8 OR 11
**/
@Configuration
public class SpringBootConfig implements WebMvcConfigurer {
/**
* Description: 添加全局跨域CORS处理
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
// 设置允许跨域的路径
registry.addMapping("/**")
//设置允许跨域请求的域名
.allowedOrigins("http://127.0.0.1:8787")
// 是否允许证书
.allowCredentials(true)
// 设置允许的方法
.allowedMethods("GET", "POST", "DELETE", "PUT")
// 设置允许的header属性
.allowedHeaders("*")
// 跨域允许时间
.maxAge(3600);
}
/**
* Description: 静态资源过滤
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
//ClassPath:/Static/** 静态资源释放
registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
//释放swagger
registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
//释放webjars
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
/**
* Description: 过滤器
*
* @param registry
* @author: GuoTong
* @date: 2023-06-03 12:32:39
* @return:void
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new TenantUserInterceptor()).addPathPatterns("/**")
.excludePathPatterns("/doc.html")
.excludePathPatterns("/swagger-resources/**")
.excludePathPatterns("/webjars/**")
.excludePathPatterns("/v2/**")
.excludePathPatterns("/favicon.ico")
.excludePathPatterns("/sso/**")
.excludePathPatterns("/swagger-ui.html/**");
}
}
注册mybatisplus的多租户实现到 MybatisPlusInterceptor
package com.gton.user.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.gton.user.handler.EasySqlInjector;
import com.gton.user.handler.SysTenantHandlerImpl;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* @description: Mybatis相关组件配置
* @author: GuoTong
* @createTime: 2022-11-25 15:33
* @since JDK 1.8 OR 11
**/
@Configuration
public class MybatisConfig {
@Value("${spring.jackson.date-format:yyyy-MM-dd HH:mm:ss}")
private String pattern;
/**
* Description: 新的分页插件
*
* @author: GuoTong
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加多租户插件
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new SysTenantHandlerImpl()));
// 添加乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
// 向Mybatis过滤器链中添加分页拦截器
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
/**
* Description: 批量插入优化
*
* @author: GuoTong
*/
@Bean
public EasySqlInjector sqlInjector() {
return new EasySqlInjector();
}
/**
* Description: localDateTime 序列化器
*
* @author: GuoTong
* @return:
*/
@Bean
public LocalDateTimeSerializer localDateTimeSerializer() {
return new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(pattern));
}
/**
* Description: localDateTime 反序列化器
*
* @author: GuoTong
* @return:
*/
@Bean
public LocalDateTimeDeserializer localDateTimeDeserializer() {
return new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(pattern));
}
/**
* Description: Json序列化JDK8新时间APILocalDateTime
*
* @author: GuoTong
* @date: 2022-12-05 16:20:01
*/
@Bean
public Jackson2ObjectMapperBuilderCustomizer customizer() {
return builder -> {
//返回时间数据序列化
builder.serializerByType(LocalDateTime.class, localDateTimeSerializer());
//接收时间数据反序列化
builder.deserializerByType(LocalDateTime.class, localDateTimeDeserializer());
builder.simpleDateFormat(pattern);
};
}
}
测试Controller
/**
* 多租户表(TenantAuthLoginUser)表控制层
*
* @author 郭童
* @since 2023-06-22 16:28:10
*/
@RestController
@RequestMapping("tenantAuthLoginUser")
@SwaggerScanClass
public class TenantAuthLoginUserController {
/**
* 服务对象
*/
@Autowired
private TenantAuthLoginUserService tenantAuthLoginUserService;
@Value("${mybatis.pwd.key:d1104d7c3b616f0b}")
private String mybatiskey;
/**
* 分页查询数据
*
* @param limitRequest 查询实体
* @return 所有数据
*/
@PostMapping("/queryLimit")
public Resp<BaseLimitResponse<TenantAuthLoginUser>> queryPage(@RequestBody BaseLimitRequest<TenantAuthLoginUser> limitRequest) {
// 分页查询
IPage<TenantAuthLoginUser> page = this.tenantAuthLoginUserService.queryLimitPage(limitRequest);
// 封装返回结果集
BaseLimitResponse<TenantAuthLoginUser> data = BaseLimitResponse.getInstance(page.getRecords(), page.getTotal(), page.getPages(), limitRequest.getPageIndex(), limitRequest.getPageSize());
return Resp.Ok(data);
}
/**
* 通过主键查询单条数据
*
* @param id 主键
* @return 单条数据
*/
@GetMapping("/queryOne/{id}")
public Resp<TenantAuthLoginUser> selectOne(@PathVariable("id") Serializable id) {
return Resp.Ok(this.tenantAuthLoginUserService.getById(id));
}
/**
* 新增数据
*
* @param tenantAuthLoginUser 实体对象
* @return 新增结果
*/
@PostMapping("/save")
public Resp<String> insert(@RequestBody TenantAuthLoginUser tenantAuthLoginUser) {
boolean save = false;
String executeMsg = ContextCommonMsg.USER_NAME_EXITS;
;
try {
String username = tenantAuthLoginUser.getUsername();
Long count = tenantAuthLoginUserService.lambdaQuery().eq(StringUtils.isNotEmpty(username), TenantAuthLoginUser::getUsername, username).count();
if (count >= 1) {
return Resp.error(executeMsg);
}
tenantAuthLoginUser.setPassword(AES.encrypt(tenantAuthLoginUser.getPassword(), mybatiskey));
save = this.tenantAuthLoginUserService.save(tenantAuthLoginUser);
executeMsg = "新增成功,id 是:" + tenantAuthLoginUser.getId();
} catch (Exception e) {
executeMsg = e.getMessage();
}
return save ? Resp.Ok(executeMsg) : Resp.error(executeMsg);
}
}
省略单表的CRUD的三层架构,各种Mybatisx或者EasyCode都可以全自动生成
测试数据
INSERT INTO `tenant_auth_login_user`(`id`, `username`, `password`, `rule`, `sex`, `head_portrait`, `phone`, `address`, `email`, `description`, `is_del`, `create_time`, `update_time`, `tenant_id`) VALUES (1671817343047143426, '全球最强', 'JRaZunLuzVfNLNfCpe/Ahg==', 'ADMIN', '中立', 'http://localhost:8889/', '110-7654321', '重庆市神魂村天之痕路一号', 'guotong@qq.com', '黑暗万岁', 1, '2023-06-22 17:48:10', '2023-06-22 17:48:10', 1100);
INSERT INTO `tenant_auth_login_user`(`id`, `username`, `password`, `rule`, `sex`, `head_portrait`, `phone`, `address`, `email`, `description`, `is_del`, `create_time`, `update_time`, `tenant_id`) VALUES (1671817697516163073, '地表最强', 'JRaZunLuzVfNLNfCpe/Ahg==', 'ADMIN', '中立', 'http://localhost:8889/', '110-7654321', '天之痕路一号', 'guotong@qq.com', '光明万岁', 1, '2023-06-22 17:49:35', '2023-06-22 17:49:35', 1001);
Mybatisplus帮我们在增删改查的所有操作都给拼接上携带租户条件。。一劳永逸,当你不需要使用多租户的条件时,TenantLineHandler实现类里
/**
* 根据表名判断是否忽略拼接多租户条件
* <p>
* 默认都要进行解析并拼接多租户条件
*
* @param tableName 表名
* @return 是否忽略, true:表示忽略,false:需要解析并拼接多租户条件
*/
@Override
public boolean ignoreTable(String tableName) {
return IGNORE_TENANT_TABLES.contains(tableName);
}
@Override
public boolean ignoreInsert(List<Column> columns, String tenantIdColumn) {
// 新增排除自己携带了这个多租户字段的新增
for (Column column : columns) {
if (column.getColumnName().equalsIgnoreCase(tenantIdColumn)) {
return true;
}
}
return false;
}
swagger测试接口新增;如果参数传了租户值就使用当前租户ID,否则使用默认租户ID完成新增
swagger测试接口查询(请求头里设置租户信息就使用该租户信息查询,否则就使用默认租户ID实现查询)
多租户基于Springboot+MybatisPlus实现使用一个数据库一个表 使用字段进行数据隔离的更多相关文章
- SQL Server 游标运用:查看一个数据库所有表大小信息(Sizes of All Tables in a Database)
原文:SQL Server 游标运用:查看一个数据库所有表大小信息(Sizes of All Tables in a Database) 一.本文所涉及的内容(Contents) 本文所涉及的内容(C ...
- 通过 SQL Server 视图访问另一个数据库服务器表的方法
今天项目经理跑过来对我大吼大叫说什么之前安排让我做一大堆接口为什么没做,我直接火了,之前明明没有这个事情…… 不过事情还要解决,好在两个项目都是用的sqlserver,可以通过跨数据库视图来快速解决问 ...
- sql数据库如何在数据库里面把其中一个数据库的表复制到另一个数据库里面
在sqlserver数据库里面,我们肯定有这样一个情况,假如我用的是SQL2008,如何把数据库里面的整个表以及表内数据复制到另外一个表中.那应该如何操作??有两种方法,我们一起来看一下 复制表结构: ...
- SpringBoot Mybatis-Plus 整合 dynamic-datasource-spring-boot-starter 对数据库进行读写分离
准备工作 对 MySql 进行主从搭建 引入 dynamic-datasource-spring-boot-starter 坐标 引入 druid-spring-boot-starter 坐标 对应框 ...
- ORACLE查询当前资产状态,和另一个数据库联查,(查询重复数据中第一条),子查询作为字段查询
背景:ORACLE查询当前资产状态,包含资产信息(表1),资产维修状态(表2),资产报废状态(表3) 如下: 资产信息:
- python操作mysql数据库读取一个数据库的表写入另一个数据库
写这个肯定是工作需要了,不啰嗦,直接说事 我现在有两台主机,一台是公司主机,一台是客户主机,要求把公司主机上的三个表同步到客户主机上的数据库 注意是同步,首先就得考虑用linux定时任务或者主从复制, ...
- 使用navicat把一个数据库的表导入到另外一个数据库
第一步:右击数据库名,选择数据传输 第二步:全选要导的数据库表 第三步:选择目标中的数据库,然后开始就可以了
- Springboot项目启动后自动创建多表关联的数据库与表的方案
文/朱季谦 在一些项目开发当中,存在这样一种需求,即开发完成的项目,在第一次部署启动时,需能自行构建系统需要的数据库及其对应的数据库表. 若要解决这类需求,其实现在已有不少开源框架都能实现自动生成数据 ...
- SpringBoot+MybatisPlus+Mysql+Sharding-JDBC分库分表实践
一.序言 在实际业务中,单表数据增长较快,很容易达到数据瓶颈,比如单表百万级别数据量.当数据量继续增长时,数据的查询性能即使有索引的帮助下也不尽如意,这时可以引入数据分库分表技术. 本文将基于Spri ...
- saas系统多租户数据隔离的实现(一)数据隔离方案
0. 前言 前几天跟朋友聚会的时候,朋友说他们公司准备自己搞一套saas系统,以实现多个第三方平台的业务接入需求.聊完以后,实在手痒难耐,于是花了两天时间自己实现了两个saas系统多租户数据隔离实现方 ...
随机推荐
- 【Python入门教程】Python常用表格函数&操作(xlrd、xlwt、openpyxl、xlwings)
在我们使用Python时,避免不了与Excel打交道.同样Python的三方库和代码的简洁性也为我们处理大数据提供了便利.今天给大家介绍一下常用的处理表格的函数,同时还有一些常用的 ...
- Python正则表达式完全指南
本篇文章将深入探讨python的一项强大工具:正则表达式.正则表达式是一个强大的文本处理工具,可以用来匹配,搜索,替换和解析文本.我们将逐步展示如何在Python中使用正则表达式,包括其基本语法,常见 ...
- 【python基础】input函数
1.初识input函数 大多数程序都旨在解决最终用户的问题,为此通常需要从用户那里获取一些信息.例如假设有人要判断自己是否到了投票的年龄,要编写回答这个问题的程序,就需要知道用户的年龄,这样才能给出答 ...
- [ARM 汇编]进阶篇—存储访问指令—2.3.3 栈操作指令
栈是一种特殊的数据结构,其特点是后进先出(LIFO,Last In First Out).在 ARM 汇编中,栈通常用于保存函数调用时的寄存器状态.局部变量和返回地址等.本节将详细介绍 ARM 汇编中 ...
- 数据库中的可视化和探索性:MongoDB的数据可视化和探索性工具
目录 1. 引言 2. 技术原理及概念 2.1 基本概念解释 2.2 技术原理介绍 2.3 相关技术比较 3. 实现步骤与流程 3.1 准备工作:环境配置与依赖安装 3.2 核心模块实现 3.3 集成 ...
- DLang 与 C 语言交互
DLang 与 C 语言交互 很难受的是,这部分的文档太少了,根本没有 教程向 的文章.所以我写了此文以做分享. 本文原址链接(防止机器搬运):https://www.cnblogs.com/jeef ...
- 利用代码生成工具快速生成基于SqlSugar框架的Winform界面项目
我们接触一个新事物的时候,如果一个事物能够给我们带来非常直观的感官认识,那么我们就很容易接受,反之可能需要很长时间的潜移默化的了解认识才能接受.万物化繁为简,透过本质看表象,往往也是一个认知迭代深入的 ...
- 关于Java已死,看看国外开发者怎么说的
博主在浏览 medium 社区时,发现了一篇点赞量 1.5k 的文章,名称叫<Java is Dead - 5 Misconceptions of developers that still t ...
- 消灭非稳态噪音的利器 - AI 降噪
摘要:轻量级神经网络降噪方法,解析 ZegoAIDenoise 的算法实现! 文|即构引擎开发团队 一.轻量级神经网络降噪--ZegoAIDenoise 当下,用户在进行音频通话时常常置身于各种不同的 ...
- 浮点指令之找main函数
环境 vs2019 编译选项x86(32位) debug版本 float指令练习 //c++源码 #include<stdio.h> int main(int argc,char* arg ...