本文就不多逼逼,直接进入正题。
多租户技术(Multi-TenancyTechnology)又称多重租赁技术,简称SaaS,是一种软件架构技术,是实现如何在多用户环境下 (此处的多用户一般是面向企业用户)共用相同的系统或程序组件,并且可确保各用户间数据的隔离性。简单讲: 在一台服务器上运行单个应用实例,它为多个租户(客户)提供服务。从定义中我们可以理解:多租户是一种架 构,目的是为了让多用户环境下使用同一套程序,且保证用户间数据隔离。那么重点就很浅显易懂了,多租户的重 点就是同一套程序下实现多用户数据的隔离
目前基于多租户的数据库设计方案通常有如下三种:
1、独立数据库 共享数据库
2、独立 Schema 共享数据库
3、共享数据库、共享数据表
即一个租户一个数据库。
为不同的租户提供独立的数据库,用户数据隔离级别最高,安全性最好,有助于简化数据模型的扩展设计,满足不同租户的独特需求;如果出现故障,恢复数据比较简单。
数据库维护成本和购置成本的大大增加。
即多个或所有租户共享Database,每个租户一个Schema。

为安全性要求较高的租户提供了一定程度的逻辑数据隔离,并不是完全隔离;每个数据库可以支持更多的租户数量。
如果出现故障,数据恢复比较困难,因为恢复数据库将牵扯到其他租户的数据;
如果需要跨租户统计数据,存在一定困难。 这种方案是方案一的变种。只需要安装一份数据库服务,通过不同的Schema对不同租户的数据进行隔离。由于数 据库服务是共享的,所以成本相对低廉。
即租户共享同一个Database、同一个Schema,但在表中通过tenant_id字段区分租户的数据,表明该记录是属于哪个租户的。这是共享程度最高、隔离级别最低的模式。
所有租户使用同一套数据库,所以成本低廉。
隔离级别最低,安全性最低,需要在设计开发时加大对安全的开发量;这种方案和基于传统应用的数据库设计并没有任何区别,但是由于所有租户使用相同的数据库表,所以需要做好对 每个租户数据的隔离安全性处理,这就增加了系统设计和数据管理方面的复杂程度。 数据备份和恢复最困难,需要逐表逐条备份和还原。
如果希望以最少的服务器为最多的租户提供服务,并且租户接受以牺牲隔离级别换取降低成本,这种方案最适合。
本文选择的是方案三!如果是自己从零开始进行开发,需要在每条 sql 上加上 tenant_id 条件。那开发成本特别大。但我们使用的是 Mybatis Plus,那就不需要如此复杂了,框架已经集成多租户使用。
CREATE TABLE `test_tenant` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`account` varchar(32) DEFAULT NULL,
`email` varchar(64) DEFAULT NULL,
`tenant_id` int(10) unsigned DEFAULT NULL COMMENT '租户id',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
复制代码
insert 语句
INSERT INTO `test`.`test_tenant`(`id`, `account`, `email`, `tenant_id`) VALUES (1, 'cxyxj', 'cxyxj.qq.com', 0);
INSERT INTO `test`.`test_tenant`(`id`, `account`, `email`, `tenant_id`) VALUES (2, 'awesome', 'awesome@163.com', 1);
INSERT INTO `test`.`test_tenant`(`id`, `account`, `email`, `tenant_id`) VALUES (3, 'gongj', 'gongj@163.com', 2);
复制代码
注意关键字段tenant_id。
搭建 Boot项目,加入以下依赖:
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<mybatis-plus.version>3.5.0</mybatis-plus.version>
</properties>
<dependencies>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
复制代码
@Data
public class TestTenant {
@TableId(type = IdType.AUTO)
private Integer id;
private String account;
private String email;
private Integer tenantId;
}
复制代码
我们的主键类型为 int,所以需要修改主键策略,修改为自增,默认使用雪花算法生成全局唯一id,长度为19 位。
public interface TenantMapper extends BaseMapper<TestTenant> {
}
复制代码
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
@Override
public Expression getTenantId() {
//获得当前登录用户的租户id
return new LongValue(1111);
}
}));
return interceptor;
}
}
复制代码
在 Mybatis Plus 中,一切插件的主体是 InnerInterceptor。 目前已有的功能(官网地址):
本文使用到的是 TenantLineInnerInterceptor。在我们的代码中,使用了 TenantLineInnerInterceptor 类的有参构造方法。入参为 TenantLineHandler对象。这是比较重要的对象,比如:某一些表不需要拼接多租户条件、多租户的字段名是什么。都是在这个对象中规定。
public interface TenantLineHandler {
// 获得租户ID值 本文写死了 111
Expression getTenantId();
// 数据库字段 默认为 tenant_id
default String getTenantIdColumn() {
return "tenant_id";
}
// 需要忽略拼接条件的表名
// 方法默认返回 false 表示所有表都需要拼多租户条件
default boolean ignoreTable(String tableName) {
return false;
}
// 这个方法在之前版本是没有的!已给出租户列的 insert 不再拼接条件。使用用户给出的值。
// 针对比较特殊的场景,比如:异步添加时,获取不到登录人的租户ID,则给默认租户ID
default boolean ignoreInsert(List<Column> columns, String tenantIdColumn) {
return columns.stream().map(Column::getColumnName).anyMatch((i) -> {
return i.equalsIgnoreCase(tenantIdColumn);
});
}
}
复制代码
server:
port: 1998
spring:
datasource:
url: jdbc:mysql:/127.0.0.1:3306/test?useSSL=false&useUnicode=true&characterEncoding=utf-8
username: root
password: xxx
driver-class-name: com.mysql.jdbc.Driver
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
复制代码
@SpringBootTest
public class MybatisPlusApplicationTests {
@Autowired
TenantMapper tenantMapper;
@Test
public void testSelect() {
List<TestTenant> testTenants = tenantMapper.selectList(null);
testTenants.forEach(System.out::println);
}
}
复制代码
关注控制台打印的 sql 语句,在 where 语句后面拼接了 test_tenant.tenant_id = 1111的条件。这说明我们的租户隔离达到效果,并且很轻松容易的实现了。
那我们再来看看其他语句!
@Test
public void testOther() {
System.out.println("测试新增=====");
TestTenant testTenant = new TestTenant();
testTenant.setAccount("hhhh");
testTenant.setEmail("100093");
tenantMapper.insert(testTenant);
System.out.println("测试修改=====");
testTenant.setEmail("@164.com");
tenantMapper.updateById(testTenant);
System.out.println("测试删除=====");
tenantMapper.deleteById(testTenant.getId());
}
复制代码
测试新增=====
==> Preparing: INSERT INTO test_tenant (account, email, tenant_id)
VALUES (?, ?, 1111)
==> Parameters: hhhh(String), 100093(String)
<== Updates: 1
复制代码
测试修改=====
==> Preparing: UPDATE test_tenant SET account = ?, email = ? WHERE
test_tenant.tenant_id = 1111 AND id = ?
==> Parameters: hhhh(String), @164.com(String), 4(Integer)
<== Updates: 1
复制代码
测试删除=====
==> Preparing: DELETE FROM test_tenant WHERE test_tenant.tenant_id = 1111 AND id = ?
==> Parameters: 4(Integer)
<== Updates: 1
复制代码
可以得知,当配置了 TenantLineInnerInterceptor插件后,我们的 CRUR SQL 都拼接了我们所指定的字段作为 where 条件。
在实际的开发中,肯定不会如此的一帆风顺。肯定会有一些比较特殊的逻辑。
总有一些表是比较特殊的。表中压根就没租户id字段,那这怎么处理呢? 我们只需要重写 TenantLineHandler 类中的ignoreTable方法即可。
/**
* 需要忽略拼接多租户条件的表名
*/
@Value("#{'${mybatis-plus.configuration.ignore-tenant-tables:}'.split(',')}")
private List<String> ignoreTenantTables;
// 该 default 方法 默认返回 false 表示所有表都需要拼多租户条件
// 如果有部分 sql 不需要加上租户ID条件
// 可以使用 @InterceptorIgnore(tenantLine = "true") 标注在 Mapper 接口的方法上
// 而 @SqlParser(filter = true) 在 mybatis-plus 3.4 版本中标记为过时
@Override
public boolean ignoreTable(String tableName) {
return ignoreTenantTables.stream().anyMatch((e) -> e.equalsIgnoreCase(tableName));
}
复制代码
在配置文件中将需要忽略的表名进行配置。
mybatis-plus:
configuration:
ignore-tenant-tables: test_tenant
复制代码
@Test
public void testSelect() {
List<TestTenant> testTenants = tenantMapper.selectList(null);
testTenants.forEach(System.out::println);
}
复制代码

可以看到这一次的查询语句中并没有拼接多租户条件。
对于一些拥有租户id字段的表,在某一些场景中,比如:我想获得表中所有数据,不想让它拼接条件。那应该怎么做?注意:这种都是自己自定义 sql 语句。
我们只需要在自定义的方法上标注一个注解 @InterceptorIgnore。这是官方提供的。

该注解作用于 xxMapper.java 方法之上,各属性代表对应的插件,各属性不给值则默认为 false,设置为 true 表示忽略拦截。
@Select("SELECT id, account, email, tenant_id FROM test_tenant")
@InterceptorIgnore(tenantLine = "true")
List<TestTenant> listAll();
复制代码
@Test
public void listAll() {
List<TestTenant> testTenants = tenantMapper.listAll();
testTenants.forEach(System.out::println);
}
复制代码
TenantLineHandler 类中,还有一个方法没有介绍,那就是 ignoreInsert方法。这个方法的作用就是如果你在进行 insert 时,我们手动给了租户id字段,则框架不再自动拼接。我们来看看效果吧!
@Test
public void testInsert() {
System.out.println("测试新增=====");
TestTenant testTenant = new TestTenant();
testTenant.setAccount("hhhh");
testTenant.setEmail("100093");
testTenant.setTenantId(11232323);
tenantMapper.insert(testTenant);
}
复制代码
可以看到 sql 中 tenant_id 的值,取的是我们指定的值。 我们看看源码是怎么处理的!逻辑在 processInsert方法中。

如果需要插入的列中,包含知道的租户列,则不进行多租户处理。
如果还想对 update、delete sql 也进行这种特殊的处理,只需要重写对应的方法 processUpdate、processDelete。
我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div
我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看rubyzip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d
类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc
很好奇,就使用rubyonrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提
假设我做了一个模块如下:m=Module.newdoclassCendend三个问题:除了对m的引用之外,还有什么方法可以访问C和m中的其他内容?我可以在创建匿名模块后为其命名吗(就像我输入“module...”一样)?如何在使用完匿名模块后将其删除,使其定义的常量不再存在? 最佳答案 三个答案:是的,使用ObjectSpace.此代码使c引用你的类(class)C不引用m:c=nilObjectSpace.each_object{|obj|c=objif(Class===objandobj.name=~/::C$/)}当然这取决于
我正在尝试使用ruby和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我
关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。
我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t
我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h
我想为Heroku构建一个Rails3应用程序。他们使用Postgres作为他们的数据库,所以我通过MacPorts安装了postgres9.0。现在我需要一个postgresgem并且共识是出于性能原因你想要pggem。但是我对我得到的错误感到非常困惑当我尝试在rvm下通过geminstall安装pg时。我已经非常明确地指定了所有postgres目录的位置可以找到但仍然无法完成安装:$envARCHFLAGS='-archx86_64'geminstallpg--\--with-pg-config=/opt/local/var/db/postgresql90/defaultdb/po