前言什么是多租户 多租户技术(MultiTenancyTechnology)又称多重租赁技术,简称SaaS,是一种软件架构技术,是实现如何在多用户环境下(此处的多用户一般是面向企业用户)共用相同的系统或程序组件,并且可确保各用户间数据的隔离性。简单讲:在一台服务器上运行单个应用实例,它为多个租户(客户)提供服务。从定义中我们可以理解:多租户是一种架构,目的是为了让多用户环境下使用同一套程序,且保证用户间数据隔离。那么重点就很浅显易懂了,多租户的重点就是同一套程序下实现多用户数据的隔离隔离方案 目前基于多租户的数据库设计方案通常有如下三种:1、独立数据库共享数据库2、独立Schema共享数据库3、共享数据库、共享数据表独立数据库 即一个租户一个数据库。优点 为不同的租户提供独立的数据库,用户数据隔离级别最高,安全性最好,有助于简化数据模型的扩展设计,满足不同租户的独特需求;如果出现故障,恢复数据比较简单。缺点 数据库维护成本和购置成本的大大增加。共享数据库,独立Schema 即多个或所有租户共享Database,每个租户一个Schema。什么是Schemaoracle数据库:在oracle中一个数据库可以具有多个用户,那么一个用户一般对应一个Schema,表都是建立在Schema中的,(可以简单的理解:在oracle中一个用户一套数据库表) mysql数据库:mysql数据中的schema比较特殊,并不是数据库的下一级,而是等同于数据库。比如执行createschematest和执行createdatabasetest效果是一模一样的优点 为安全性要求较高的租户提供了一定程度的逻辑数据隔离,并不是完全隔离;每个数据库可以支持更多的租户数量。缺点 如果出现故障,数据恢复比较困难,因为恢复数据库将牵扯到其他租户的数据; 如果需要跨租户统计数据,存在一定困难。这种方案是方案一的变种。只需要安装一份数据库服务,通过不同的Schema对不同租户的数据进行隔离。由于数据库服务是共享的,所以成本相对低廉。共享数据库、共享数据表 即租户共享同一个Database、同一个Schema,但在表中通过tenantid字段区分租户的数据,表明该记录是属于哪个租户的。这是共享程度最高、隔离级别最低的模式。优点 所有租户使用同一套数据库,所以成本低廉。缺点 隔离级别最低,安全性最低,需要在设计开发时加大对安全的开发量;这种方案和基于传统应用的数据库设计并没有任何区别,但是由于所有租户使用相同的数据库表,所以需要做好对每个租户数据的隔离安全性处理,这就增加了系统设计和数据管理方面的复杂程度。数据备份和恢复最困难,需要逐表逐条备份和还原。 如果希望以最少的服务器为最多的租户提供服务,并且租户接受以牺牲隔离级别换取降低成本,这种方案最适合。集成 本文选择的是方案三!如果是自己从零开始进行开发,需要在每条sql上加上tenantid条件。那开发成本特别大。但我们使用的是MybatisPlus,那就不需要如此复杂了,框架已经集成多租户使用。创建表CREATETABLEtesttenant(idint(11)unsignedNOTNULLAUTOINCREMENT,accountvarchar(32)DEFAULTNULL,emailvarchar(64)DEFAULTNULL,tenantidint(10)unsignedDEFAULTNULLCOMMENT租户id,PRIMARYKEY(id))ENGINEInnoDBDEFAULTCHARSETutf8;复制代码 insert语句INSERTINTOtest。testtenant(id,account,email,tenantid)VALUES(1,cxyxj,cxyxj。qq。com,0);INSERTINTOtest。testtenant(id,account,email,tenantid)VALUES(2,awesome,awesome163。com,1);INSERTINTOtest。testtenant(id,account,email,tenantid)VALUES(3,gongj,gongj163。com,2);复制代码 注意关键字段tenantid。搭建项目依赖 搭建Boot项目,加入以下依赖:propertiesmaven。compiler。source8maven。compiler。sourcemaven。compiler。target8maven。compiler。targetmybatisplus。version3。5。0mybatisplus。versionpropertiesdependenciesdependencygroupIdcom。baomidougroupIdmybatisplusbootstarterartifactIdversion{mybatisplus。version}versiondependencydependencygroupIdorg。springframework。bootgroupIdspringbootstarterwebartifactIddependencydependencygroupIdmysqlgroupIdmysqlconnectorjavaartifactIdversion5。1。47versionscoperuntimescopedependencydependencygroupIdorg。projectlombokgroupIdlombokartifactIddependencydependencygroupIdorg。springframework。bootgroupIdspringbootstartertestartifactIdscopetestscopeexclusionsexclusiongroupIdorg。junit。vintagegroupIdjunitvintageengineartifactIdexclusionexclusionsdependencydependencies复制代码实体DatapublicclassTestTenant{TableId(typeIdType。AUTO)privateIntegerid;privateStringaccount;privateStringemail;privateIntegertenantId;}复制代码 我们的主键类型为int,所以需要修改主键策略,修改为自增,默认使用雪花算法生成全局唯一id,长度为19位。mapper接口publicinterfaceTenantMapperextendsBaseMapperTestTenant{}复制代码MybatisPlusConfigimportcom。baomidou。mybatisplus。extension。plugins。MybatisPlusInterceptor;importcom。baomidou。mybatisplus。extension。plugins。handler。TenantLineHandler;importcom。baomidou。mybatisplus。extension。plugins。inner。TenantLineInnerInterceptor;importnet。sf。jsqlparser。expression。Expression;importnet。sf。jsqlparser。expression。LongValue;importorg。springframework。beans。factory。annotation。Value;importorg。springframework。context。annotation。Bean;importorg。springframework。context。annotation。Configuration;importjava。util。List;ConfigurationpublicclassMybatisPlusConfig{BeanpublicMybatisPlusInterceptormybatisPlusInterceptor(){MybatisPlusInterceptorinterceptornewMybatisPlusInterceptor();interceptor。addInnerInterceptor(newTenantLineInnerInterceptor(newTenantLineHandler(){OverridepublicExpressiongetTenantId(){获得当前登录用户的租户idreturnnewLongValue(1111);}}));returninterceptor;}}复制代码 在MybatisPlus中,一切插件的主体是InnerInterceptor。目前已有的功能(官网地址):自动分页:PaginationInnerInterceptor多租户:TenantLineInnerInterceptor动态表名:DynamicTableNameInnerInterceptor乐观锁:OptimisticLockerInnerInterceptorsql性能规范:IllegalSQLInnerInterceptor防止全表更新与删除:BlockAttackInnerInterceptor 本文使用到的是TenantLineInnerInterceptor。在我们的代码中,使用了TenantLineInnerInterceptor类的有参构造方法。入参为TenantLineHandler对象。这是比较重要的对象,比如:某一些表不需要拼接多租户条件、多租户的字段名是什么。都是在这个对象中规定。publicinterfaceTenantLineHandler{获得租户ID值本文写死了111ExpressiongetTenantId();数据库字段默认为tenantiddefaultStringgetTenantIdColumn(){returntenantid;}需要忽略拼接条件的表名方法默认返回false表示所有表都需要拼多租户条件defaultbooleanignoreTable(StringtableName){returnfalse;}这个方法在之前版本是没有的!已给出租户列的insert不再拼接条件。使用用户给出的值。针对比较特殊的场景,比如:异步添加时,获取不到登录人的租户ID,则给默认租户IDdefaultbooleanignoreInsert(ListColumncolumns,StringtenantIdColumn){returncolumns。stream()。map(Column::getColumnName)。anyMatch((i){returni。equalsIgnoreCase(tenantIdColumn);});}}复制代码配置文件server:port:1998spring:datasource:url:jdbc:mysql:127。0。0。1:3306test?useSSLfalseuseUnicodetruecharacterEncodingutf8username:rootpassword:xxxdriverclassname:com。mysql。jdbc。Drivermybatisplus:configuration:logimpl:org。apache。ibatis。logging。stdout。StdOutImpl复制代码测试SpringBootTestpublicclassMybatisPlusApplicationTests{AutowiredTenantMappertenantMapper;TestpublicvoidtestSelect(){ListTestTenanttestTenantstenantMapper。selectList(null);testTenants。forEach(System。out::println);}}复制代码 关注控制台打印的sql语句,在where语句后面拼接了testtenant。tenantid1111的条件。这说明我们的租户隔离达到效果,并且很轻松容易的实现了。测试增修删语句 那我们再来看看其他语句!TestpublicvoidtestOther(){System。out。println(测试新增);TestTenanttestTenantnewTestTenant();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:INSERTINTOtesttenant(account,email,tenantid)VALUES(?,?,1111)Parameters:hhhh(String),100093(String)Updates:1复制代码测试修改测试修改Preparing:UPDATEtesttenantSETaccount?,email?WHEREtesttenant。tenantid1111ANDid?Parameters:hhhh(String),164。com(String),4(Integer)Updates:1复制代码测试删除测试删除Preparing:DELETEFROMtesttenantWHEREtesttenant。tenantid1111ANDid?Parameters:4(Integer)Updates:1复制代码 可以得知,当配置了TenantLineInnerInterceptor插件后,我们的CRURSQL都拼接了我们所指定的字段作为where条件。特殊处理 在实际的开发中,肯定不会如此的一帆风顺。肯定会有一些比较特殊的逻辑。某表不需要拼接租户条件 总有一些表是比较特殊的。表中压根就没租户id字段,那这怎么处理呢?我们只需要重写TenantLineHandler类中的ignoreTable方法即可。需要忽略拼接多租户条件的表名Value({{mybatisplus。configuration。ignoretenanttables:}。split(,)})privateListStringignoreTenantTables;该default方法默认返回false表示所有表都需要拼多租户条件如果有部分sql不需要加上租户ID条件可以使用InterceptorIgnore(tenantLinetrue)标注在Mapper接口的方法上而SqlParser(filtertrue)在mybatisplus3。4版本中标记为过时OverridepublicbooleanignoreTable(StringtableName){returnignoreTenantTables。stream()。anyMatch((e)e。equalsIgnoreCase(tableName));}复制代码 在配置文件中将需要忽略的表名进行配置。mybatisplus:configuration:ignoretenanttables:testtenant复制代码测试TestpublicvoidtestSelect(){ListTestTenanttestTenantstenantMapper。selectList(null);testTenants。forEach(System。out::println);}复制代码 可以看到这一次的查询语句中并没有拼接多租户条件。某一条sql不需要拼接 对于一些拥有租户id字段的表,在某一些场景中,比如:我想获得表中所有数据,不想让它拼接条件。那应该怎么做?注意:这种都是自己自定义sql语句。 我们只需要在自定义的方法上标注一个注解InterceptorIgnore。这是官方提供的。 该注解作用于xxMapper。java方法之上,各属性代表对应的插件,各属性不给值则默认为false,设置为true表示忽略拦截。自定义sqlSelect(SELECTid,account,email,tenantidFROMtesttenant)InterceptorIgnore(tenantLinetrue)ListTestTenantlistAll();复制代码测试TestpublicvoidlistAll(){ListTestTenanttestTenantstenantMapper。listAll();testTenants。forEach(System。out::println);}复制代码 额外知识点 TenantLineHandler类中,还有一个方法没有介绍,那就是ignoreInsert方法。这个方法的作用就是如果你在进行insert时,我们手动给了租户id字段,则框架不再自动拼接。我们来看看效果吧!TestpublicvoidtestInsert(){System。out。println(测试新增);TestTenanttestTenantnewTestTenant();testTenant。setAccount(hhhh);testTenant。setEmail(100093);testTenant。setTenantId(11232323);tenantMapper。insert(testTenant);}复制代码 可以看到sql中tenantid的值,取的是我们指定的值。我们看看源码是怎么处理的!逻辑在processInsert方法中。 如果需要插入的列中,包含知道的租户列,则不进行多租户处理。 如果还想对update、deletesql也进行这种特殊的处理,只需要重写对应的方法processUpdate、processDelete。