精华内容
下载资源
问答
  • 在上一篇“浅析多租户在 Java 平台和某些 PaaS 上的实现”中我们谈到了应用层面的多租户架构,涉及到 PaaS、JVM、OS 等,与之相应的是数据层也有多租户的支持。数据层的多租户综述多租户(Multi Tenancy/Tenant)是一...

    在上一篇“浅析多租户在 Java 平台和某些 PaaS 上的实现”中我们谈到了应用层面的多租户架构,涉及到 PaaS、JVM、OS 等,与之相应的是数据层也有多租户的支持。

    数据层的多租户综述

    多租户(Multi Tenancy/Tenant)是一种软件架构,其定义是:

    在一台服务器上运行单个应用实例,它为多个租户提供服务。

    在SaaS实施过程中,有一个显著的考量点,就是如何对应用数据进行设计,以支持多租户,而这种设计的思路,是要在数据的共享、安全隔离和性能间取得平衡。

    传 统的应用,仅仅服务于单个租户,数据库多部署在企业内部网络环境,对于数据拥有者来说,这些数据是自己“私有”的,它符合自己所定义的全部安全标准。而在 云计算时代,随着应用本身被放到云端,导致数据层也经常被公开化,但租户对数据安全性的要求,并不因之下降。同时,多租户应用在租户数量增多的情况下,会 比单租户应用面临更多的性能压力。本文即对这个主题进行探讨:多租户在数据层的框架如何在共享、安全与性能间进行取舍,同时了解一下市面上一些常见的数据 厂商怎样实现这部分内容。

    常见的三种模式

    在 MSDN 的这篇文章 Multi-Tenant Data Architecture 中,系统的总结了数据层的三种多租户架构:

    独立数据库

    共享数据库、独立 Schema

    共享数据库、共享 Schema、共享数据表

    独立数据库是一个租户独享一个数据库实例,它提供了最强的分离度,租户的数据彼此物理不可见,备份与恢复都很灵活;共享数据库、独立 Schema 将每个租户关联到同一个数据库的不同 Schema,租户间数据彼此逻辑不可见,上层应用程序的实现和独立数据库一样简单,但备份恢复稍显复杂; 最后一种模式则是租户数据在数据表级别实现共享,它提供了最低的成本,但引入了额外的编程复杂性(程序的数据访问需要用 tenantId 来区分不同租户),备份与恢复也更复杂。这三种模式的特点可以用一张图来概括:

    图 1. 三种部署模式的异同

    上图所总结的是一般性的结论,而在常规场景下需要综合考虑才能决定那种方式是合适的。例如,在占用成本上,认为独立数据库会高,共享模式较低。但如果考虑到大租户潜在的数据扩展需求,有时也许会有相反的成本耗用结论。

    而多租户采用的选择,主要是成本原因,对于多数场景而言,共享度越高,软硬件资源的利用效率更好,成本也更低。但同时也要解决好租户资源共享和隔离带来的安全与性能、扩展性等问题。毕竟,也有客户无法满意于将数据与其他租户放在共享资源中。

    目前市面上各类数据厂商在多租户的支持上,大抵都是遵循上文所述的这几类模式,或者混合了几种策略,这部分内容将在下面介绍。

    JPA Provider

    JSR 338 定义了 JPA 规范 2.1,但如我们已经了解到的,Oracle 把多租户的多数特性推迟到了 Java EE 8 中。尽管这些曾经在 JavaOne 大会中有过演示,但无论是在 JPA 2.0(JSR 317)还是 2.1 规范中,都依然没有明文提及多租户。不过这并不妨碍一些 JPA provider 在这部分领域的实现,Hibernate 和 EclipseLink 已提供了全部或部分的多租户数据层的解决方案。

    Hibernate 是当今最为流行的开源的对象关系映射(ORM)实现,并能很好地和 Spring 等框架集成,目前 Hibernate 支持多租户的独立数据库和独立 Schema 模式。EclipseLink 也是企业级数据持久层JPA标准的参考实现,对最新 JPA2.1 完整支持,在目前 JPA 标准尚未引入多租户概念之际,已对多租户支持完好,其前身是诞生已久、功能丰富的对象关系映射工具 Oracle TopLink。因此本文采用 Hibernate 和 EclipseLink 对多租户数据层进行分析。

    Hibernate

    Hibernate 是一个开放源代码的对象/关系映射框架和查询服务。它对 JDBC 进行了轻量级的对象封装,负责从 Java 类映射到数据库表,并从 Java 数据类型映射到 SQL 数据类型。在 4.0 版本 Hibenate 开始支持多租户架构——对不同租户使用独立数据库或独立 Sechma,并计划在 5.0 中支持共享数据表模式。

    在 Hibernate 4.0 中的多租户模式有三种,通过 hibernate.multiTenancy 属性有下面几种配置:

    NONE:非多租户,为默认值。

    SCHEMA:一个租户一个 Schema。

    DATABASE:一个租户一个 database。

    DISCRIMINATOR:租户共享数据表。计划在 Hibernate5 中实现。

    模式1:独立数据库

    如果是独立数据库,每个租户的数据保存在物理上独立的数据库实例。JDBC 连接将指向具体的每个数据库,一个租户对应一个数据库实例。在 Hibernate 中,这种模式可以通过实现 MultiTenantConnectionProvider 接口或继承 AbstractMultiTenantConnectionProvider 类等方式来实现。三种模式中它的共享性最低,因此本文重点讨论以下两种模式。

    模式 2:共享数据库,独立 Schema

    对于共享数据库,独立 Schema。所有的租户共享一个数据库实例,但是他们拥有独立的 Schema 或 Catalog,本文将以多租户酒店管理系统为案例说明 Hibernate 对多租户的支持和用使用方法。

    图 2. guest 表结构

    这是酒店客户信息表,我们仅以此表对这种模式进行说明,使用相同的表结构在 MySQL 中创建 DATABASE hotel_1 和 hotel_2。基于 Schema 的多租户模式,需要在 Hibernate 配置文件 Hibernate.cfg.xml 中设置 hibernate.multiTenancy 等相关属性。

    清单 1. 配置文件 Hibernate.cfg.xml

    jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8

    root

    com.mysql.jdbc.Driver

    org.hibernate.dialect.MySQLInnoDBDialect

    false

    false

    false

    SCHEMA

    hotel.dao.hibernate.TenantIdResolver

    hotel.dao.hibernate.SchemaBasedMultiTenantConnectionProvider

    属性规定了一个合约,以使 Hibernate 能够解析出应用当前的 tenantId,该类必须实现 CurrentTenantIdentifierResolver 接口,通常我们可以从登录信息中获得 tenatId。

    清单 2. 获取当前 tenantId

    public class TenantIdResolver implements CurrentTenantIdentifierResolver {

    public String resolveCurrentTenantIdentifier() {

    return Login.getTenantId();

    }

    }

    < hibernate.multi_tenant_connection_provider> 属性指定了 ConnectionProvider,即 Hibernate 需要知道如何以租户特有的方式获取数据连接,SchemaBasedMultiTenantConnectionProvider 类实现了MultiTenantConnectionProvider 接口,根据 tenantIdentifier 获得相应的连接。在实际应用中,可结合使用 JNDI DataSource 技术获取连接以提高性能。

    清单 3. 以租户特有的方式获取数据库连接

    public class SchemaBasedMultiTenantConnectionProvider

    implements MultiTenantConnectionProvider, Stoppable,

    Configurable, ServiceRegistryAwareService {

    private final DriverManagerConnectionProviderImpl connectionProvider

    = new DriverManagerConnectionProviderImpl();

    @Override

    public Connection getConnection(String tenantIdentifier) throws SQLException {

    final Connection connection = connectionProvider.getConnection();

    connection.createStatement().execute("USE " + tenantIdentifier);

    return connection;

    }

    @Override

    public void releaseConnection(String tenantIdentifier, Connection connection)

    throws SQLException {

    connection.createStatement().execute("USE test");

    connectionProvider.closeConnection(connection);

    }

    ……

    }

    与表 guest 对应的 POJO 类 Guest,其中主要是一些 getter 和 setter方法。

    清单 4. POJO 类 Guest

    @Table(name = "guest")

    public class Guest {

    private Integer id;

    private String name;

    private String telephone;

    private String address;

    private String email;

    //getters and setters

    ……

    }

    我们使用 ServiceSchemaBasedMain.java 来进行测试,并假设了一些数据以方便演示,如当有不同租户的管理员登录后分别进行添加客户的操作。

    清单 5. 测试类 ServiceSchemaBasedMain

    public class ServiceSchemaBasedMain {

    public static void main(String[] args) {

    Session session = null;

    Guest guest =null;

    List list = null;

    Transaction tx = null;

    System.out.println("======== 租户 hotel_1 ========");

    Login.setTenantId("hotel_1");

    session = sessionFactory.openSession();

    tx = session.beginTransaction();

    guest = new Guest();

    guest.setName("张三");

    guest.setTelephone("56785678");

    guest.setAddress("上海市张扬路88号");

    guest.setEmail("zhangsan@gmail.com");

    session.saveOrUpdate(guest);

    list = session.createCriteria(Guest.class).list();

    for (Guest gue : list) {

    System.out.println(gue.toString());

    }

    tx.commit();

    session.close();

    System.out.println("======== 租户 hotel_2 ========");

    Login.setTenantId("hotel_2");

    session = sessionFactory.openSession();

    tx = session.beginTransaction();

    guest = new Guest();

    guest.setName("李四");

    guest.setTelephone("23452345");

    guest.setAddress("上海市南京路100号");

    guest.setEmail("lisi@gmail.com");

    session.saveOrUpdate(guest);

    list = session.createCriteria(Guest.class).list();

    for (Guest gue : list) {

    System.out.println(gue.toString());

    }

    tx.commit();

    session.close();

    }

    }

    清单 6. 运行程序 ServiceSchemaBasedMain 的输出

    ======== 租户 hotel_1 ========

    Guest [id=1, name=Victor, telephone=56008888, address=上海科苑路399号, email=vic@gmail.com]

    Guest [id=2, name=Jacky, telephone=66668822, address=上海金科路28号, email=jacky@sina.com]

    Guest [id=3, name=张三, telephone=56785678, address=上海市张扬路88号, email=zhangsan@gmail.com]

    ======== 租户 hotel_2 ========

    Guest [id=1, name=Anton, telephone=33355566, address=上海南京路8号, email=anton@gmail.com]

    Guest [id=2, name=Gus, telephone=33355566, address=北京大道3号, email=gus@yahoo.com]

    Guest [id=3, name=李四, telephone=23452345, address=上海市南京路100号, email=lisi@gmail.com]

    模式3:共享数据库、共享 Schema、共享数据表

    在这种情况下,所有租户共享数据表存放数据,不同租户的数据通过 tenant_id 鉴别器来区分。但目前的 Hibernate 4 还不支持这个多租户鉴别器策略,要在 5.0 才支持。但我们是否有可选的替代方案呢?答案是使用 Hibernate Filter.

    为了区分多个租户,我在 Schema 的每个数据表需要添加一个字段 tenant_id 以判定数据是属于哪个租户的。

    图 3. 共享 Schema、共享数据表案例 E-R 图

    根据上图在 MySQL 中创建 DATABASE hotel。

    我们在 OR-Mapping 配置文件中使用了 Filter,以便在进行数据查询时,会根据 tenant_id 自动查询出该租户所拥有的数据。

    清单 7. 对象关系映射文件 Room.hbm.xml

    接下来我们在 HibernateUtil 类中通过 ThreadLocal 存放和获取 Hibernate Session,并将用户登录信息中的 tenantId 设置为 tenantFilterParam 的参数值。

    清单 8. 获取 Hibernate Session 的工具类 HibernateUtil

    不过 Filter 只是有助于我们读取数据时显示地忽略掉 tenantId,但在进行数据插入的时候,我们还是不得不显式设置相应 tenantId 才能进行持久化。这种状况只能在 Hibernate5 版本中得到根本改变。

    清单 9. 运行程序 HotelServiceMain 输出

    多租户下的 Hibernate 缓存

    基于独立 Schema 模式的多租户实现,其数据表无需额外的 tenant_id。通过 ConnectionProvider 来取得所需的 JDBC 连接,对其来说一级缓存(Session 级别的缓存)是安全的可用的,一级缓存对事物级别的数据进行缓存,一旦事物结束,缓存也即失效。但是该模式下的二级缓存是不安全的,因为多个 Schema 的数据库的主键可能会是同一个值,这样就使得 Hibernate 无法正常使用二级缓存来存放对象。例如:在 hotel_1 的 guest 表中有个 id 为 1 的数据,同时在 hotel_2 的 guest 表中也有一个 id 为 1 的数据。通常我会根据 id 来覆盖类的 hashCode() 方法,这样如果使用二级缓存,就无法区别 hotel_1 的 guest 和 hote_2 的 guest。

    在共享数据表的模式下的缓存, 可以同时使用 Hibernate的一级缓存和二级缓存, 因为在共享的数据表中,主键是唯一的,数据表中的每条记录属于对应的租户,在二级缓存中的对象也具有唯一性。Hibernate 分别为 EhCache、OSCache、SwarmCache 和 JBossCache 等缓存插件提供了内置的 CacheProvider 实现,读者可以根据需要选择合理的缓存,修改 Hibernate 配置文件设置并启用它,以提高多租户应用的性能。

    EclipseLink

    EclipseLink 是 Eclipse 基金会管理下的开源持久层服务项目,为 Java 开发人员与各种数据服务(比如:数据库、web services、对象XML映射(OXM)、企业信息系统(EIS)等)交互提供了一个可扩展框架,目前支持的持久层标准中包括:

    Java Persistence API (JPA)

    Java Architecture for XML Binding (JAXB)

    Java Connector Architecture (JCA)

    Service Data Objects (SDO)

    EclipseLink 前身是 Oracle TopLink, 2007年 Oracle 将后者绝大部分捐献给了 Eclipse 基金会,次年 EclipseLink 被 Sun 挑选成为 JPA 2.0 的参考实现。

    注: 目前 EclipseLink2.5 完全支持 2013 年发布的 JPA2.1(JSR 338) 。

    在完整实现 JPA 标准之外,针对 SaaS 环境,在多租户的隔离方面 EclipseLink 提供了很好的支持以及灵活地解决方案。

    应用程序隔离

    隔离的容器/应用服务器

    共享容器/应用服务器的应用程序隔离

    同一应用程序内的共享缓存但隔离的 entity manager factory

    共享的 entity manager factory 但每隔离的 entity manager

    数据隔离

    隔离的数据库

    隔离的Schema/表空间

    隔离的表

    共享表但隔离的行

    查询过滤

    Oracle Virtual Private Database (VPD)

    对于多租户数据源隔离主要有以下方案

    Single-Table Multi-tenancy,依靠租户区分列(tenant discriminator columns)来隔离表的行,实现多租户共享表。

    Table-Per-Tenant Multi-tenancy,依靠表的租户区分(table tenant discriminator)来隔离表,实现一租户一个表,大体类似于上文的共享数据库独立Schema模式。

    Virtual Private Database(VPD ) Multi-tenancy,依靠 Oracle VPD 自身的安全访问策略(基于动态SQL where子句特性),实现多租户共享表。

    本节重点介绍多租户在 EclipseLink 中的共享数据表和一租户一个表的实现方法,并也以酒店多租户应用的例子展现共享数据表方案的具体实践。

    EclipseLink Annotation @Multitenant

    与 @Entity 或 @MappedSuperclass 一起使用,表明它们在一个应用程序中被多租户共享, 如清单 10。

    清单10. @Multitenant

    @Entity

    @Table(name="room")

    @Multitenant

    ...

    publicclass Room {

    }

    表 1. Multitenant 包含两个属性

    Annotation 属性描述缺省值

    boolean includeCriteria

    是否将租户限定应用到 select、update、delete 操作上

    true

    MultitenantType value

    多租户策略,SINGLE_TABLE, TABLE_PER_TENANT, VPD.

    SINGLE_TABLE

    共享数据表(SINGLE_TABLE)

    Metadata配置

    依靠租户区分列修饰符 @TenantDiscriminatorColumn 实现。

    清单11. @TenantDiscriminatorColumn

    @Entity

    @Table(name="hotel_guest")

    @Multitenant(SINGLE_TABLE)

    @TenantDiscriminatorColumn(name="tenant_id", contextProperty="tenant.id")

    publicclass HotelGuest {

    }

    或者在EclipseLink描述文件orm.xml定义对象与表映射时进行限制,两者是等价的。

    清单12. orm.xml

    ...

    属性配置

    租户区分列定义好后,在运行时环境需要配置具体属性值,以确定当前操作环境所属的租户。

    三种方式的属性配置,按优先生效顺序排序如下

    EntityManager(EM)

    EntityManagerFactory(EMF)

    Application context (when in a Java EE container)

    例如 EntityManagerFactory 可以间接通过在 persistence.xml 中配置持久化单元(Persistence Unit)或直接传属性参数给初始化时 EntityManagerFactory。

    清单 13. 配置 persistence.xml

    ...

    ...

    或者

    清单 14. 初始化 EntityManagerFactory

    按共享粒度可以作如下区分,

    EntityManagerFactory 级别

    用户需要通过 eclipselink.session-name 提供独立的会话名,确保每个租户占有独立的会话和缓存。

    清单 15. 为 EntityManagerFactory 配置会话名

    共享的 EntityManagerFactory 级别

    EntityManagerFactory 的默认模式, 此级别缺省配置为独立二级缓存(L2 cache), 即每个 mutlitenant 实体缓存设置为 ISOLATED,用户也可设置 eclipselink.multitenant.tenants-share-cache 属性为真以共享,此时多租户 Entity 缓存设置为 PROTECTED。

    这种级别下,一个活动的 EntityManager 不能更换 tenantId。

    EntityManager 级别

    这种级别下,共享 session,共享 L2 cache, 用户需要自己设置缓存策略,以设置哪些租户信息是不能在二级缓存共享的。

    清单 16. 设置缓存

    同样,一个活动的EntityManager不能更换tenant ID。

    几点说明:

    每个表的区分列可以有任意多个,使用修饰符 TenantDiscriminatorColumns。

    清单 17. 多个分区列

    @TenantDiscriminatorColumns({

    @TenantDiscriminatorColumn(name="tenant_id", contextProperty="tenant.id"),

    @TenantDiscriminatorColumn(name = "guest_id", contextProperty="guest.id")

    })

    租户区分列的名字和对应的上下文属性名可以取任意值,由应用程序开发者设定。

    生成的 Schema 可以也可以不包含租户区分列,如 tenant_id 或 guest_id。

    租户区分列可以映射到实体对象也可以不。

    注意:当映射的时候,实体对象相应的属性必须标记为只读(insertable=false, updatable=false),这种限制使得区分列不能作为实体表的 identifier。

    TenantDiscriminatorColumn被以下 EntityManager 的操作和查询支持:

    persist,find,refresh,named queries,update all,delete all 。

    一租户一表(TABLE_PER_TENANT )

    这种多租户类型使每个租户的数据可以占据专属它自己的一个或多个表,多租户间的这些表可以共享相同 Schema 也可使用不同的,前者使用前缀(prefix)或后缀(suffix)命名模式的表的租户区分符,后者使用租户专属的 Schema 名来定义表的租户区分符。

    Metadata配置

    依靠数据表的租户区分修饰符 @TenantTableDiscriminator 实现

    清单 18.

    @Entity

    @Table(name=“CAR”)

    @Multitenant(TABLE_PER_TENANT)

    @TenantTableDiscriminator(type=SCHEMA, contextProperty="eclipselink-tenant.id")

    public class Car{

    }

    清单 19.

    如前所述,TenantTableDiscriminatorType有 3 种类型:SCHEMA、SUFFIX 和 PREFIX。

    属性配置

    与另外两种多租户类型一样,默认情况下,多租户共享EMF,如不想共享 EMF,可以通过配置 PersistenceUnitProperties.MULTITENANT_SHARED_EMF 以及 PersistenceUnitProperties.SESSION_NAME 实现。

    清单 20.

    或在 persistence.xml 配置属性。

    酒店多租户应用实例(EclipseLink 共享(单)表)

    数据库 Schema 和测试数据与上文 Hibernate 实现相同,关于对象关系映射(OR mapping)的配置均采用 JPA 和 EclipseLink 定义的 Java Annotation 描述。

    关于几个基本操作

    添加一个对象实例, 利用EntityManager.persist()

    清单 21. 添加

    public void save(T t) {

    em.persist(t);

    }

    public void saveBulk(List bulk) {

    for(T t:bulk){

    em.persist(t);

    }

    }

    更新一个对象实例, 利用EntityManager.merge()

    清单 22. 更新

    public void update(T t){

    em.merge(t);

    }

    查询, 利用EntityManager的NamedQuery,

    清单 23. 多条件多结果查询

    若用 JPQL 实现则示例如下:

    清单 24. JPQL NamedQuery 定义

    部分测试数据如下(MySQL):

    hotel_admin

    hotel_guest

    room

    运行附件 MT_Test_Hotels.zip 中的测试代码(请参照 readme)来看看多租户的一些典型场景。

    清单 25. 运行测试代码

    能得到输出片段如下:

    清单 26. 输出

    通过共享表的测试数据以及运行结果可以看到,对于多个不同的租户(hotel_admin),在添加、查找、更新操作没有显示声明租户标识的情况下,EntityManager 可以根据自身的租户属性配置

    实现租户分离。在本实例,EntityManager 初始化时利用到 hotel_admin 登录后的会话上下文进行租户判断,这里不再赘述。

    注:上文中提及的全部源码都可以在附件中找到。

    其它方面的考虑

    数据备份

    独立数据库和独立Sechma的模式,为每个租户备份数据比较容易,因为他们存放在不同的数据表中,只需对整个数据库或整个Schema进行备份。

    在 共享数据表的模式下,可以将所有租户的数据一起备份,但是若要为某一个租户或按租户分开进行数据备份,就会比较麻烦。通常需要另外写sql脚本根据 tenant_id来取得对应的数据然后再备份,但是要按租户来导入的话依然比较麻烦,所以必要时还是需要备份所有并为以后导入方便。

    性能

    独立数据库:性能高,但价格也高,需要占用资源多,不能共享,性价比低。

    共享数据库,独立 Schema:性能中等,但价格合适,部分共享,性价比中等。

    共享数据库,共享 Schema,共享数据表:性能中等(可利用 Cache 可以提高性能),但价格便宜,完全共享,性价比高。如果在某些表中有大量的数据,可能会对所有租户产生性能影响。

    对于共享数据库的情况下,如果因为太多的最终用户同时访问数据库而导致应用程序性能问题,可以考虑数据表分区等数据库端的优化方案。

    经济考虑

    为了支持多租户应用,共享模式的应用程序往往比使用独立数据库模式的应用程序相对复杂,因为开发一个共享的架构,导致在应用设计上得花较大的努力,因而初始成本会较高。然而,共享模式的应用在运营成本上往往要低一些,每个租户所花的费用也会比较低。

    结束语

    多租户数据层方案的选择是一个综合的考量过程,包括成本、数据隔离与保护、维护、容灾、性能等。但无论怎样选择,OR-Mapping 框架对多租户的支持将极大的解放开发人员的工作,从而可以更多专注于应用逻辑。最后我们以一个 Hibernate 和 EclipseLink 的比较来结束本文。

    表 2. Hibernate 与 EclipeLink 对多租户支持的比较

    HibernateEclipseLink

    独立数据库

    支持,通过实现 MultiTenantConnectionProvider 接口以连接独立的数据库

    支持,为每个租户配置独立的 EntityManagerFactory

    共享数据库,独立 Schema

    支持,通过实现 MultiTenantConnectionProvider 接口以切换 Schema

    支持,使用 TABLE_PER_TENANT MultitenantType 以及 SCHEMA TenantTableDiscriminatorType

    共享数据库,共享 Schema,共享数据表

    展开全文
  • 多租户设计与实践探索

    千次阅读 2021-07-18 10:42:25
    从中台架构的理念提出到现在,经过了3年的实践,行业内从一些大厂开始纷纷将自身的架构逐步改造成中台架构或者基于中台模式的架构进行规划,到今天来看,从中台架构出发,衍生出各种与之相关的模式,如sass等,其...

    前言

    从中台架构的理念提出到现在,经过了3年多的实践,行业内从一些大厂开始纷纷将自身的架构逐步改造成中台架构或者基于中台模式的架构进行规划,到今天来看,从中台架构出发,衍生出各种与之相关的模式,如sass等,其底层技术架构的规划,仍然和中台是一脉相承的

    为什么需要中台

    关于这一点,可以参考阿里内部人士撰写的一本《企业IT架构转型之道 阿里巴巴中台战略思想与架构实战》,有非常详细的解读

    个人也有幸参与过2个小型的中台化产品的建设与改造,从实践上来说,产品逐步向中台(这里指代技术中台,不同的公司有自己的中台,比如业务中台,数据中台等)靠拢的好处在于,业务更加聚焦,不再像之前那样各种定制化的需求一大堆,兵来将挡水来土掩,最后弄得毫无章法,技术上来说,偏向基础而底层的微服务可以逐步抽象并下沉,使得公司的技术体系更加完备,比如数据存储服务,中间件服务,数据统计分析服务等,从而技术人员有更多的精力去研究如何在提升系统的高并发等性能方面进行发力,而对于上层应用来说,有了这些基础的底层服务的支撑,快速开发新的基于中台的应用也就变得更高效

    租户由来

    在这里插入图片描述

    想必很多同学对电商类系统一定不陌生吧,就像上图展示的这种架构,一般的电商系统或者类电商(医疗电商,宠物商城,团购电商等)模式,都有一个前台系统和后台系统,前台系统是流量的主要来源,而且随着硬件技术的发展,前台的入口也越来越多,图中展示的来源包括,网页,app,Ipad,甚至一些广告屏幕,小程序等

    前台页面中产生的数据来源于各类渠道用户的交互点击行为,比如某个用户产生了一笔订单数据,这些进入订单表的数据,对于公司来说,最终都是要走财务流程的,为了财务核算数据,就需要一个后台系统,专门处理订单表的数据,这只是其中一个非常简单的场景

    再比如说,当系统处于流量高峰期时,比如像双11那样的大促,为了确保核心服务可用,可以考虑关闭一些无关紧要的服务,为了能够快速的关闭前台服务展示的各种菜单入口,总不能直接去服务器上面把这个服务给kill掉吧,通常各类的服务都可以通过后台系统统一配置管理,根据需要进行快速的启停等操作

    从以上描述来看,这个后管系统对于整个系统的作用是不言而喻的

    在上面的后管系统中,为了管理前台页面的数据,经常需要从后管系统中对数据进行管理,是不是所有人都可以登录后管系统进行操作呢?

    答案是否定的

    通常来说,系统的超级管理员只有一个,即admin,为了分担admin的压力,通常可以通过admin创建出一些2级管理员,授予这些2级管理员一定的角色,然后就可以通过这些2级管理员来管理各种菜单、资源、用户等数据权限了

    到这里,就要引出租户的概念了,从上面的后管系统开始,可以发现,后管的主要目的是为了统一管理一些前台数据,和前台甚至后台操作相关的权限等等

    一旦当前台的应用架构演变为中台架构之后,需要对各种数据、资源权限相关的操作就更多了,本文以小编参与过的一个中台产品,基于sass架构的中台产品为例,来探讨下租户的设计与实践

    我们知道,所有中台建设的目的都是为了业务快速且低成本创新,绝大部分的企业基于中台都会开发大量的业务应用,一般基于业务中台的架构如下图:

    在这里插入图片描述
    上图为某业务中台架构模式,在该业务中台下,可以划分成不同的维度,可以确定的是,业务中台承载的应用是多样的,输出的能力也是多样的,但不管今后有多少新的应用加入到中台中,有一点可以确定的是,任何一个使用中台的账户,需要被授予一定的权限,才能具有访问中台下各个子模块应用的能力

    这就像大家的支付宝账号,不仅可以访问支付宝,还可以访问淘宝、天猫、阿里云等等阿里生态的几乎所有产品,因为你的支付宝账号对应的背后的应用,理论上都是阿里大中台下的一个个具体的应用而已

    在如此的背景下,我们亟需一个统一管理用户、授权等数据的应用或者入口,对访问中台中各个应用的资源细化到更具体的层面,这就是对应的权限配置

    在这里插入图片描述

    以上就是用户中心简易功能图,即用户中心最基本的管理用户,管理权限的能力,但是在中台模式下,整个缺少远远不够的,为什么呢?

    这种类似简单的用户管理后台一样的系统,在小规模的几个微服务应用之下,还是可以支撑勉强应付,但对于中台来说,尤其是要商业化的中台产品,必然要考虑的一个问题就是,你的前台应用要拿出去卖钱的,针对不同的客户,对于用户数据的管理必然有不同的需求,有的客户希望自建数据库,有的想上公有云,有的想玩私有云,有的甚至想混合云,并且能够打通各种云之间的数据通道

    更有一种极端情况,用户希望有一种方式可以使用你的中台应用达到数据上的隔离,如果将用户的需求理解为 “数据空间”的话,那么就需要你的用户中心具备数据隔离的能力,即在不同的数据空间下,用户能够管理的应用资源也是不同的

    这里就可以将这个 “数据空间”理解为租户

    何为租户

    租户可以当作一个逻辑上的概念进行理解,相信有购买过阿里云或者腾讯云等服务器的小伙伴有过深刻的体会,其实你购买的云服务器或者云服务,本质上来说,购买完成之后,你就是服务提供商的一个租户,你是在租用他们的服务,而服务器提供商从他们那里给你分配了一个虚拟机或者服务器空间,你可以在这个独立的空间上进行各种操作,安装应用,跑mysql服务等

    也就是说,你是作为一个租户在有限的空间内使用服务提供商的资源,而这个空间内你操作过程中产生的一切数据都归属于你自己这个租户

    从中台架构上来说,租户可以表示为使用中台应用的第三方,为了管理自身的用户数据,合理的为其单位下的各类用户分配不同的应用资源权限等操作,需要一个独立的逻辑上的隔离空间,来达到目的

    单独户和多租户

    在这里插入图片描述

    单租户下,平台中只有一个租户,即使用平台应用的只有一个租户,所有租户相关的管理数据都在这一个租户中完成

    优点:

    • 单租户系统独立享有所有资源,用户不隔离,数据独享
    • 支持自定义所有功能,具备较大灵活性,后期定制化功能时预留空间大
    • 支持高可用,高并发,分布式部署

    缺点:

    • 维护成本高,上百套系统需投入大量的运维及开发
    • 资源浪费极大,如果用户需要多个租户,则需要横向扩张服务器资源
    • 存在私有云、公有云、混合云部署,对数据抽离及分析存对技术提出了很大的挑战

    在这里插入图片描述

    多租户是相对但租户来说的,多租户主要从逻辑上将租户进行了区分,实际操作时,可以通过 tenant_id字段标识与之相关的数据表

    优点:

    • 租户之间资源可以方便的实现共享
    • 用户隔离,数据隔离(逻辑上)
    • 后期可以方便的对数据进行提取分析,也可以方便的通过tenant_id对数据库、数据表做拆分

    缺点:

    • 支持有限定制化功能开发
    • 数据隔离上存在一定的安全隐患(逻辑隔离)
    • 对混合云的支持不够好

    租户的主要功能

    这是本篇探讨的一个很重要的话题,上面谈到了租户的由来,我们大致了解到,租户其实要完成的工作其实和后管中的内容差不多,只是从设计上来说,比单纯的后管系统要更复杂,而且可以支持使用平台的单位更加灵活的对用户、资源等数据进行分配,更重要的是,要提供数据隔离空间

    在这里插入图片描述
    这仅仅是用户中心的基础功能,为了能够达到租户级别的数据控制,我们还需要有一个可以配置管理租户的地方,我们可以理解为 “租户管理”模块

    小编在实际产品中,租户管理是作为单独的微服务应用划分出来的,这样有个好处,就是可以集中管理所有的租户,即你在租户管理中,可以创建租户,删除租户,以及为租户分配用户数据

    想必这么一描述之后,大家心里对租户管理服务应该有个大致的模样了,试想,如果没有租户管理的话,若还想满足用户对于租户的需求,那就只能是单租户,即默认为每个使用本系统的单位为一个租户,这样来说,从后期更长远的需求规划上,就很难再满足用户从单租户切换为多租户

    在这里插入图片描述

    实际的实施流程大概是下面这样的

    在客户购买我们整个标准产品后(包括业务中台、或部分基础应用),首先我们在顶级租户中预置了一个root账户,通过该账户我能够创建更多租户,并为租户实例化应用,在实例化应用的同时,为该租户生成在该应用实例下的租户管理员。

    租户管理员能够登入本租户进行租户下的全局的权限管理,例如:他能在该租户下创建用户,并设置该用户能够登录的应用;他能为租户下的任一应用实例创建角色,并把该角色分配租户下的用户

    也就是说,租户其实和用户中心的功能是相辅相成的,用户中心的功能是基础,租户相当于是隔离了一块块区域,在该区域下,管理自己的数据,入用户、权限、用户组等等

    当然在实际操作中,远远比上面描述的要复杂,举例来说,如何让中台下的各个应用都能快速接入租户这一套体系呢?

    这是一个业务问题,也是一个技术问题,本篇的最后,以实践中的经验来简单探讨下

    在这里插入图片描述

    以上图为例,为一个sass应用使用admin用户默认登录之后的界面展现,上面我们简单提到过,应用的分配可以在租户下进行分配

    通常来说,对于初次安装的使用单位,会默认分配基本的应用,这个可以在租户应用侧在程序初始化的安装的时候,根据商务要求,通过读取配置中心的配置,将基础的应用初始化注册到应用相关的数据表中

    为了方便后续新接入的应用可以继续注册进去,从租户侧的微服务来说,提供了更灵活的方式来实现应用注册,包括:restFul接口,dubbo接口,当然也可以通过租户的界面进行配置

    注册之前,比如当调用租户侧的dubbo接口注册时候,需要携带一定的参数,除了必要的参数之外,有一个参数非常必要,就是全局的apiKey,对于apiKey这个想必大家并不陌生,这个apiKey的产生在于用户创建那一刻就产生了,而注册应用时,为了确定租户信息,还需要携带一个tenant_id的参数

    其他的和租户相关的操作,租户侧也提供了各类基于apiKey和tenant_id的dubbo服务接口,供各个应用的微服务模块进行调用,比如注册权限,新建用户组等

    多租户展望

    在实际实践过程中,由于需要真实对接客户方的需求,针对一些大的客户方,比如像某某油公司这样的大金主,由于总部到分部人员众多,对于租户的使用也提出了越来越多的需求,举例来说,客户方希望对租户的类型进行区分,也可以理解为等级,不同等级的租户具备不同的操作权限,即不再是单纯的在各个租户下进行自己租户下用户、资源、权限等数据管理,还能对租户自身进行分级管理,其目的之一,在于加强总部(顶级租户)对下级租户的管控

    第二个需求是,客户希望所有的用户数据只有一个入口和出口,即租户管理中,在本文上篇描述的,用户信息一般来源于顶级租户,即admin这个账户产生的用户数据,然后各个租户可以登录之后管理自己的用户数据(用户的增删改查,及基于用户的资源权限分配),但是客户希望所有的用户只能从admin顶级租户往各个租户做分配,即各个租户不再单独产生用户数据,而是由顶级租户生产,然后分发到各个租户,然后各个租户再管理这部分的用户数据

    这么做的考虑在于总部对分部对用户数据的集中管理

    当然,这也说明,基于多租户的业务,还有很多值得探究的问题,毕竟在大环境下互联网安全的话题越来越被重视的情况下涉及到数据安全的问题,所以对于租户的探索,还需要更多丰富的场景来完善我们的认知

    本篇到此结束,最后感谢观看!

    展开全文
  • 多租户方案设计

    千次阅读 2021-02-03 15:13:38
    独立数据库 独立服务实例 优点:独立部署,数据服务... 优点: 维护和购置成本最低,允许每个数据库支持的租户数量最多。 缺点: 隔离级别最低,安全性最低,需要在设计开发时加大对安全的开发量 数据备份和恢复最困

    独立数据库 独立服务实例

        优点:独立部署,数据服务隔离度高,业务定制扩展性高,如果出现故障,恢复数据比较简单
        缺点:运维成本高
    

    独立数据库 共享服务实例

        优点:数据隔离度高,服务共享减少维护成本,扩展性中等
    	缺点:数据库独立部署成本高,服务实例共享对高可用性要求高
    

    共享数据库 共享服务实例

        优点:  维护和购置成本最低,允许每个数据库支持的租户数量最多。
    	缺点:  隔离级别最低,安全性最低,需要在设计开发时加大对安全的开发量
                数据备份和恢复最困难,需要逐表逐条备份和还原;
                如果希望以最少的服务器为最多的租户提供服务,并且租户接受以牺牲隔离级别换取降低成本,这种方案最适合
    

    独立数据库(数据库代理方案如mycat) 共享服务实例

        优点:数据库隔离度高,开发者只需要关注代理服务, 扩展性中等
    	缺点:数据库独立部署成本高,使用数据库代理增加维护成本,对数据库代理中间件高可用要求高,对服务实例高可用性要求高
    

    独立数据库 共享服务实例 技术实现

    1. 建立租户中心服务及数据库

      1. 租户管理

        1. 租户对应的数据库连接信息
      2. 用户管理

        1. 关联租户
      3. 租户及用户表结构

      DROP TABLE IF EXISTS `tenant`;
      CREATE TABLE `tenant`  (
        `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
        `tenant_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '租户名称',
        `tenant_database` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '数据库名称',
        `tenant_database_url` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '数据库连接',
        `tenant_database_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '数据库登录用户',
        `tenant_database_password` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '数据库密码',
        PRIMARY KEY (`id`) USING BTREE
      ) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '租户信息' ROW_FORMAT = Dynamic;
      
      DROP TABLE IF EXISTS `user_info`;
      CREATE TABLE `user_info`  (
        `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
        `user_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '用户名',
        `password` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '密码',
        `tenant_id` bigint(20) NOT NULL COMMENT '租户id',
        PRIMARY KEY (`id`) USING BTREE
      ) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '用户表' ROW_FORMAT = Dynamic;
      
    2. 流程如下
      image

      1. 用户请请求租户中心 登录, 返回token 并将token和用户信息绑定至redis
      2. 用户携带token请求业务服务,aop拦截,环绕通知中根据token从redis中取出用户及租户信息,判断切换数据源。
    3. 切换数据源相关技术

      1. 使用AbstractRoutingDataSource 抽象路由数据源 切换 数据源
      2. 重写AbstractRoutingDataSource类,并实例化此类。注入DataSourceTransactionManager及SqlSessionFactory
      3. 使用一个ThreadLocal来管理当前线程路由键,aop设置路由键

    demo示例代码

    独立数据库(数据库代理方案如mycat) 共享服务实例

    1. 租户表设计同上,租户表除库名以外的连接信息可以删除

    2. 流程如下
      image

      1. 用户请请求租户中心 登录, 返回token 并将token和用户信息绑定至redis
      2. 用户携带token请求业务服务,在sql发送至mycat前拦截sql并增强sql,使用mycat注释指定相应的schema
        //  /*!mycat:schema=test_01*/ sql ;
        sql = "/*!mycat:schema="+userInfo.getTenantDatabase()+"*/" + sql;
        
    3. mycat配置

      1. schema.xml
      <?xml version="1.0"?>
      <!DOCTYPE mycat:schema SYSTEM "schema.dtd">
      <mycat:schema xmlns:mycat="http://io.mycat/">
      
      		<!--schema start-->
              <!--A业务库-->
              <schema name="lzq" checkSQLschema="false" sqlMaxLimit="100">
                      <table name="business"  dataNode="tenant_lzq"/>
              </schema>
      
              <!--B业务库-->
              <schema name="wy" checkSQLschema="false" sqlMaxLimit="100">
                      <table name="business"  dataNode="tenant_wy"/>
              </schema>
      
              <!--租户中心业务库-->
              <schema name="tenant" checkSQLschema="false" sqlMaxLimit="100">
                      <table name="user_info"  dataNode="tenant_centre"/>
      				<table name="tenant"  dataNode="tenant_centre"/>
              </schema>
      		<!--schema end-->
      
      
      
      
      
      		<!--dataNode start-->
              <dataNode name="tenant_lzq" dataHost="tenant_lzq_host" database="tenant_lzq" />
      
              <dataNode name="tenant_wy" dataHost="tenant_wy_host" database="tenant_wy" />
      
              <dataNode name="tenant_centre" dataHost="tenant_centre_host" database="tenant_centre" />
      		<!--dataNode end-->
      		
      		
      		
      
      		<!--dataHost start-->
              <dataHost name="tenant_lzq_host" maxCon="1000" minCon="10" balance="1"
                                writeType="0" dbType="mysql" dbDriver="native" switchType="1"  slaveThreshold="100">
                      <heartbeat>select user()</heartbeat>
                      <writeHost host="hostM1" url="101.37.152.195:3306" user="root" password="lzq199528">
                              <readHost host="hostS2" url="101.37.152.195:3306" user="root" password="lzq199528" />
                      </writeHost>
              </dataHost>
      
              <dataHost name="tenant_wy_host" maxCon="1000" minCon="10" balance="1"
                                writeType="0" dbType="mysql" dbDriver="native" switchType="1"  slaveThreshold="100">
                      <heartbeat>select user()</heartbeat>
                      <writeHost host="hostM1" url="101.37.152.195:3306" user="root" password="lzq199528">
                              <readHost host="hostS2" url="101.37.152.195:3306" user="root" password="lzq199528" />
                      </writeHost>
              </dataHost>
      
              <dataHost name="tenant_centre_host" maxCon="1000" minCon="10" balance="1"
                                writeType="0" dbType="mysql" dbDriver="native" switchType="1"  slaveThreshold="100">
                      <heartbeat>select user()</heartbeat>
                      <writeHost host="hostM1" url="101.37.152.195:3306" user="root" password="lzq199528">
                              <readHost host="hostS2" url="101.37.152.195:3306" user="root" password="lzq199528" />
                      </writeHost>
              </dataHost>
      		<!--dataHost end-->
      		
      </mycat:schema>
      
      
      1. server.xml
      <?xml version="1.0" encoding="UTF-8"?>
      <!-- - - Licensed under the Apache License, Version 2.0 (the "License"); 
           	- you may not use this file except in compliance with the License. - You 
      	may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 
      	- - Unless required by applicable law or agreed to in writing, software - 
      	distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT 
      	WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the 
      	License for the specific language governing permissions and - limitations 
      	under the License. -->
      <!DOCTYPE mycat:server SYSTEM "server.dtd">
      <mycat:server xmlns:mycat="http://io.mycat/">
      	<system>
      	<property name="useSqlStat">0</property>  <!-- 1为开启实时统计、0为关闭 -->
      	<property name="useGlobleTableCheck">0</property>  <!-- 1为开启全加班一致性检测、0为关闭 -->
      		<property name="sequnceHandlerType">0</property>
      		<property name="processorBufferPoolType">0</property>
      		<!--默认是65535 64K 用于sql解析时最大文本长度 -->
      		<!--<property name="maxStringLiteralLength">65535</property>-->
      		<!--<property name="sequnceHandlerType">0</property>-->
      		<!--<property name="backSocketNoDelay">1</property>-->
      		<!--<property name="frontSocketNoDelay">1</property>-->
      		<!--<property name="processorExecutor">16</property>-->
      		<!--
      		     			<property name="serverPort">8066</property> <property name="managerPort">9066</property> 
      			<property name="idleTimeout">300000</property> <property name="bindIp">0.0.0.0</property> 
      			<property name="frontWriteQueueSize">4096</property> <property name="processors">32</property> -->
      		<!--分布式事务开关,0为不过滤分布式事务,1为过滤分布式事务(如果分布式事务内只涉及全局表,则不过滤),2为不过滤分布式事务,但是记录分布式事务日志-->
      		<property name="handleDistributedTransactions">0</property>
      			<!--
      			     			off heap for merge/order/group/limit      1开启   0关闭
      		-->
      		<property name="useOffHeapForMerge">1</property>
      		<!--
      		     			单位为m
      		-->
      		<property name="memoryPageSize">1m</property>
      		<!--
      		     			单位为k
      		-->
      		<property name="spillsFileBufferSize">1k</property>
      		<property name="useStreamOutput">0</property>
      		<!--
      		     			单位为m
      		-->
      		<property name="systemReserveMemorySize">384m</property>
      		<!--是否采用zookeeper协调切换  -->
      		<property name="useZKSwitch">true</property>
      	</system>
      	<!--mycat用户信息 账号root 密码 lzq199528 -->
      	<user name="root">
      		<property name="password">lzq199528</property>
      		<!--对应schemas.xml文件的schemas标签的name-->
      		<property name="schemas">lzq,wy,tenant</property> 
      	</user>
      </mycat:server>
      
      
    4. 租户表中的 数据库名 存储 mycat schema名

    5. 写一个sql拦截器,在sql发送给mycat前 增加指定schema 注解

    6. sql拦截器代码

      package mycat.multi.tenancy.conf;
      
      
      import lombok.extern.slf4j.Slf4j;
      import mycat.multi.tenancy.domain.UserInfo;
      import mycat.multi.tenancy.service.impl.UserServiceImpl;
      import org.apache.ibatis.executor.statement.StatementHandler;
      import org.apache.ibatis.mapping.BoundSql;
      import org.apache.ibatis.mapping.MappedStatement;
      import org.apache.ibatis.plugin.*;
      import org.apache.ibatis.reflection.DefaultReflectorFactory;
      import org.apache.ibatis.reflection.MetaObject;
      import org.apache.ibatis.reflection.SystemMetaObject;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.stereotype.Component;
      import org.springframework.util.StringUtils;
      
      import javax.servlet.http.HttpServletRequest;
      import java.lang.reflect.Field;
      import java.sql.Connection;
      import java.util.Properties;
      
      /**
       * sql拦截器,通过mybatis提供的Interceptor接口实现
       * @author liuzhiqiang
       */
      @Slf4j
      @Component
      @Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
      public class MySqlInterceptor implements Interceptor {
      
          @Autowired
          private HttpServletRequest request;
      
          /**
           * 拦截sql
           *
           * @param invocation
           */
          @Override
          public Object intercept(Invocation invocation) throws Throwable {
              StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
      
              // 通过MetaObject优雅访问对象的属性,这里是访问statementHandler的属性;:MetaObject是Mybatis提供的一个用于方便、
              // 优雅访问对象属性的对象,通过它可以简化代码、不需要try/catch各种reflect异常,同时它支持对JavaBean、Collection、Map三种类型对象的操作。
              MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,
                      new DefaultReflectorFactory());
      
              // 先拦截到RoutingStatementHandler,里面有个StatementHandler类型的delegate变量,其实现类是BaseStatementHandler,然后就到BaseStatementHandler的成员变量mappedStatement
              MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
      
              // id为执行的mapper方法的全路径名,如com.cq.UserMapper.insertUser, 便于后续使用反射
              String id = mappedStatement.getId();
              // sql语句类型 select、delete、insert、update
              String sqlCommandType = mappedStatement.getSqlCommandType().toString();
      
              BoundSql boundSql = statementHandler.getBoundSql();
              // 获取到原始sql语句
              String sql = boundSql.getSql().toLowerCase();
              log.info("SQL:{}", sql);
      
      
              // 增强sql
              // 通过反射,拦截方法上带有自定义@InterceptAnnotation注解的方法,并增强sql
              String mSql = sqlAnnotationEnhance(id, sqlCommandType, sql);
              // 直接增强sql
      //      mSql = sql + " limit 2";
      
              //通过反射修改sql语句
              Field field = boundSql.getClass().getDeclaredField("sql");
              field.setAccessible(true);
              field.set(boundSql, mSql);
              log.info("增强后的SQL:{}", mSql);
              return invocation.proceed();
          }
      
          /**
           * 通过反射,拦截方法上带有自定义@InterceptAnnotation注解的方法,并增强sql
           * @param id 方法全路径
           * @param sqlCommandType sql类型
           * @param sql 所执行的sql语句
           */
          private String sqlAnnotationEnhance(String id, String sqlCommandType, String sql) throws ClassNotFoundException {
              // 通过类全路径获取Class对象
              Class<?> classType = Class.forName(id.substring(0, id.lastIndexOf(".")));
              // 获取当前所拦截的方法名称
              String mName = id.substring(id.lastIndexOf(".") + 1);
      
              String token = request.getHeader("Authorization");
              if (!StringUtils.isEmpty(token)) {
                  // 获取user信息 也可以登陆时存到redis中,从redis中取
                  UserInfo userInfo = UserServiceImpl.map.get(token);
                  if (userInfo != null) {
                      sql = "/*!mycat:schema="+userInfo.getTenantDatabase()+"*/" + sql;
                  }
              }
              log.info("sql====" + sql);
              return sql;
          }
      
          @Override
          public Object plugin(Object target) {
              if (target instanceof StatementHandler) {
                  return Plugin.wrap(target, this);
              } else {
                  return target;
              }
          }
      
          @Override
          public void setProperties(Properties properties) {
          }
      }
      
      

    demo示例代码

    mycat官方多租户解决方案

    SAAS多租户案例

        SAAS多租户的案例是Mycat粉丝的创新性应用案例之一,思路巧妙并且实现方式简单。
        SAAS应用中,不同租户的数据是需要进行相互隔离的,比较常用的一种方式是不同的租户采用不同的Database存放业务数据,常规的做法是应用程序中根据租户ID连接到相应的Database,通常是需要启动多个应用实例,每个租户一个,
        但这种模式消耗的资源比较多,而且不容易管理,还需要开发额外的功能,以对应租户和部署的应用实例。
        在Mycat出现以后,有人利用Mycat的SQL拦截功能,巧妙的实现了SAAS多租户特
        性,传统应用仅做少量的改动,就直接进化为多租户的SAAS应用,下面的内容是Mycat用户提供的具体细节:
        单租户就是传统的给每个租户独立部署一套web + db 。由于租户越来越多,整个web部分的机器和运维成本都非常高,因此需要改进到所有租户共享一套web的模式(db部分暂不改变)。
    基于此需求,我们对单租户的程序做了简单的改造实现web多租户共享。具体改造如下:
    1.web部分修改:
          a.在用户登录时,在线程变量(ThreadLocal)中记录租户的id
          b.修改 jdbc的实现:在提交sql时,从ThreadLocal中获取租户id,  添加sql 注释,把租户的schema 放到注释中。例如:/*!mycat :  schema = test_01 */ sql ;
    2.在db前面建立proxy层,代理所有web过来的数据库请求。proxy层是用mycat实现的,web提交的sql过来时在注释中指定schema, proxy层根据指定的schema 转发sql请求。
    此方案有几个关键点:
        - ThreadLocal变量的巧妙使用,与Hibernate的事务管理器一样的机制,线程的一个ThreadLocal变量中保留当前线程涉及到的数据库连接、事务状态等信息
        ,当Service的某个事务托管的业务方法被调用时,Hibernate自动完成数据库连接的建立或重用过程,当此方法结束时,自动回收数据库连接以及提交事务
        。在这里,操作数据库的线程中以ThreadLocal变量方式放入当前用户的Id以及对应的数据库Schema(Database),则此线程随后的整个调用方法堆栈中的任何一个点都能获取到用户对应的Schema,包括在JDBC的驱动程序中。
        - Mycat的SQL拦截机制,Mycat提供了强大的SQL注解机制,可以用来影响SQL的路由,用户可以灵活扩展。在此方案中,:/*!mycat : schema = test_01 */ 这个注解就表明此SQL将在test_01这个Schema(Database)中执行
        - 改造MySQL JDBC 驱动,MySQL JDBC驱动是开源的项目,在这里实现对SQL的拦截改造,比在程序里实现,要更加安全和可靠
    

    image

    2.1 SAAS多租户案例

    mycat官方文档

    展开全文
  • 数据层的多租户浅谈

    千次阅读 2021-01-18 18:58:58
    数据层的多租户浅谈刘 盛彬, 任 乐天, 和 陈 争云2013 年 12 月 26 日发布在上一篇“浅析多租户在 Java 平台和某些 PaaS 上的实现”中我们谈到了应用层面的多租户架构,涉及到 PaaS、JVM、OS等,与之相应的是数据层...

    数据层的多租户浅谈

    刘 盛彬, 任 乐天, 和 陈 争云

    2013 年 12 月 26 日发布

    在上一篇“浅析多租户在 Java 平台和某些 PaaS 上的实现”中我们谈到了应用层面的多租户架构,涉及到 PaaS、JVM、OS

    等,与之相应的是数据层也有多租户的支持。

    数据层的多租户综述

    多租户(Multi Tenancy/Tenant)是一种软件架构,其定义是:

    在一台服务器上运行单个应用实例,它为多个租户提供服务。

    在SaaS实施过程中,有一个显著的考量点,就是如何对应用数据进行设计,以支持多租户,而这种设计的思路,是要在数据的共享、安全隔离和性能间取得平衡。

    传统的应用,仅仅服务于单个租户,数据库多部署在企业内部网络环境,对于数据拥有者来说,这些数据是自己“私有”的,它符合自己所定义的全部安全标准。而在云计算时代,随着应用本身被放到云端,导致数据层也经常被公开化,但租户对数据安全性的要求,并不因之下降。同时,多租户应用在租户数量增多的情况下,会比单租户应用面临更多的性能压力。本文即对这个主题进行探讨:多租户在数据层的框架如何在共享、安全与性能间进行取舍,同时了解一下市面上一些常见的数据厂商怎样实现这部分内容。

    常见的三种模式

    在 MSDN 的这篇文章 Multi-Tenant Data Architecture 中,系统的总结了数据层的三种多租户架构:独立数据库

    共享数据库、独立 Schema

    共享数据库、共享 Schema、共享数据表

    独立数据库是一个租户独享一个数据库实例,它提供了最强的分离度,租户的数据彼此物理不可见,备份与恢复都很灵活;共享数据库、独立 Schema

    将每个租户关联到同一个数据库的不同 Schema,租户间数据彼此逻辑不可见,上层应用程序的实现和独立数据库一样简单,但备份恢复稍显复杂;

    最后一种模式则是租户数据在数据表级别实现共享,它提供了最低的成本,但引入了额外的编程复杂性(程序的数据访问需要用 tenantId

    来区分不同租户),备份与恢复也更复杂。这三种模式的特点可以用一张图来概括:

    图 1. 三种部署模式的异同

    baf80bd00a3799e982d0ba2c726f7d68.png

    baf80bd00a3799e982d0ba2c726f7d68.png

    上图所总结的是一般性的结论,而在常规场景下需要综合考虑才能决定那种方式是合适的。例如,在占用成本上,认为独立数据库会高,共享模式较低。但如果考虑到大租户潜在的数据扩展需求,有时也许会有相反的成本耗用结论。

    而多租户采用的选择,主要是成本原因,对于多数场景而言,共享度越高,软硬件资源的利用效率更好,成本也更低。但同时也要解决好租户资源共享和隔离带来的安全与性能、扩展性等问题。毕竟,也有客户无法满意于将数据与其他租户放在共享资源中。

    目前市面上各类数据厂商在多租户的支持上,大抵都是遵循上文所述的这几类模式,或者混合了几种策略,这部分内容将在下面介绍。

    JPA Provider

    JSR 338 定义了 JPA 规范 2.1,但如我们已经了解到的,Oracle 把多租户的多数特性推迟到了 Java EE 8 中。尽管这些曾经在 JavaOne

    大会中有过演示,但无论是在 JPA 2.0(JSR 317)还是 2.1 规范中,都依然没有明文提及多租户。不过这并不妨碍一些 JPA provider

    在这部分领域的实现,Hibernate 和 EclipseLink 已提供了全部或部分的多租户数据层的解决方案。

    Hibernate 是当今最为流行的开源的对象关系映射(ORM)实现,并能很好地和 Spring 等框架集成,目前 Hibernate 支持多租户的独立数据库和独立

    Schema 模式。EclipseLink 也是企业级数据持久层JPA标准的参考实现,对最新 JPA2.1 完整支持,在目前 JPA

    标准尚未引入多租户概念之际,已对多租户支持完好,其前身是诞生已久、功能丰富的对象关系映射工具 Oracle TopLink。因此本文采用 Hibernate 和

    EclipseLink 对多租户数据层进行分析。

    Hibernate

    Hibernate 是一个开放源代码的对象/关系映射框架和查询服务。它对 JDBC 进行了轻量级的对象封装,负责从 Java 类映射到数据库表,并从 Java

    数据类型映射到 SQL 数据类型。在 4.0 版本 Hibenate 开始支持多租户架构——对不同租户使用独立数据库或独立 Sechma,并计划在 5.0

    中支持共享数据表模式。

    在 Hibernate 4.0 中的多租户模式有三种,通过 hibernate.multiTenancy 属性有下面几种配置:NONE:非多租户,为默认值。

    SCHEMA:一个租户一个 Schema。

    DATABASE:一个租户一个 database。

    DISCRIMINATOR:租户共享数据表。计划在 Hibernate5 中实现。

    模式1:独立数据库

    如果是独立数据库,每个租户的数据保存在物理上独立的数据库实例。JDBC 连接将指向具体的每个数据库,一个租户对应一个数据库实例。在 Hibernate

    中,这种模式可以通过实现 MultiTenantConnectionProvider 接口或继承

    AbstractMultiTenantConnectionProvider 类等方式来实现。三种模式中它的共享性最低,因此本文重点讨论以下两种模式。

    模式 2:共享数据库,独立 Schema

    对于共享数据库,独立 Schema。所有的租户共享一个数据库实例,但是他们拥有独立的 Schema 或 Catalog,本文将以多租户酒店管理系统为案例说明

    Hibernate 对多租户的支持和用使用方法。

    图 2. guest 表结构

    2427f524b95c176e98c6a4c976ee9f58.png

    这是酒店客户信息表,我们仅以此表对这种模式进行说明,使用相同的表结构在 MySQL 中创建 DATABASE hotel_1 和 hotel_2。基于 Schema

    的多租户模式,需要在 Hibernate 配置文件 Hibernate.cfg.xml 中设置 hibernate.multiTenancy 等相关属性。

    清单 1. 配置文件

    Hibernate.cfg.xml

    jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8

    root

    com.mysql.jdbc.Driver

    org.hibernate.dialect.MySQLInnoDBDialect

    false

    false

    false

    SCHEMA

    hotel.dao.hibernate.TenantIdResolver

    hotel.dao.hibernate.SchemaBasedMultiTenantConnectionProvider

    属性规定了一个合约,以使 Hibernate 能够解析出应用当前的

    tenantId,该类必须实现 CurrentTenantIdentifierResolver 接口,通常我们可以从登录信息中获得 tenatId。

    清单 2. 获取当前

    tenantIdpublic class TenantIdResolver implements CurrentTenantIdentifierResolver {

    public String resolveCurrentTenantIdentifier() {

    return Login.getTenantId();

    }

    }

    < hibernate.multi_tenant_connection_provider> 属性指定了 ConnectionProvider,即

    Hibernate 需要知道如何以租户特有的方式获取数据连接,SchemaBasedMultiTenantConnectionProvider

    类实现了MultiTenantConnectionProvider 接口,根据 tenantIdentifier 获得相应的连接。在实际应用中,可结合使用 JNDI

    DataSource 技术获取连接以提高性能。

    清单 3.

    以租户特有的方式获取数据库连接public class SchemaBasedMultiTenantConnectionProvider

    implements MultiTenantConnectionProvider, Stoppable,

    Configurable, ServiceRegistryAwareService {

    private final DriverManagerConnectionProviderImpl connectionProvider

    = new DriverManagerConnectionProviderImpl();

    @Override

    public Connection getConnection(String tenantIdentifier) throws SQLException {

    final Connection connection = connectionProvider.getConnection();

    connection.createStatement().execute("USE " + tenantIdentifier);

    return connection;

    }

    @Override

    public void releaseConnection(String tenantIdentifier, Connection connection)

    throws SQLException {

    connection.createStatement().execute("USE test");

    connectionProvider.closeConnection(connection);

    }

    ……

    }

    与表 guest 对应的 POJO 类 Guest,其中主要是一些 getter 和 setter方法。

    清单 4. POJO 类

    Guest@Table(name = "guest")

    public class Guest {

    private Integer id;

    private String name;

    private String telephone;

    private String address;

    private String email;

    //getters and setters

    ……

    }

    我们使用 ServiceSchemaBasedMain.java 来进行测试,并假设了一些数据以方便演示,如当有不同租户的管理员登录后分别进行添加客户的操作。

    清单 5. 测试类

    ServiceSchemaBasedMainpublic class ServiceSchemaBasedMain {

    public static void main(String[] args) {

    Session session = null;

    Guest guest =null;

    List list = null;

    Transaction tx = null;

    System.out.println("======== 租户 hotel_1 ========");

    Login.setTenantId("hotel_1");

    session = sessionFactory.openSession();

    tx = session.beginTransaction();

    guest = new Guest();

    guest.setName("张三");

    guest.setTelephone("56785678");

    guest.setAddress("上海市张扬路88号");

    guest.setEmail("zhangsan@gmail.com");

    session.saveOrUpdate(guest);

    list = session.createCriteria(Guest.class).list();

    for (Guest gue : list) {

    System.out.println(gue.toString());

    }

    tx.commit();

    session.close();

    System.out.println("======== 租户 hotel_2 ========");

    Login.setTenantId("hotel_2");

    session = sessionFactory.openSession();

    tx = session.beginTransaction();

    guest = new Guest();

    guest.setName("李四");

    guest.setTelephone("23452345");

    guest.setAddress("上海市南京路100号");

    guest.setEmail("lisi@gmail.com");

    session.saveOrUpdate(guest);

    list = session.createCriteria(Guest.class).list();

    for (Guest gue : list) {

    System.out.println(gue.toString());

    }

    tx.commit();

    session.close();

    }

    }

    清单 6. 运行程序 ServiceSchemaBasedMain

    的输出======== 租户 hotel_1 ========

    Guest [id=1, name=Victor, telephone=56008888, address=上海科苑路399号, email=vic@gmail.com]

    Guest [id=2, name=Jacky, telephone=66668822, address=上海金科路28号, email=jacky@sina.com]

    Guest [id=3, name=张三, telephone=56785678, address=上海市张扬路88号, email=zhangsan@gmail.com]

    ======== 租户 hotel_2 ========

    Guest [id=1, name=Anton, telephone=33355566, address=上海南京路8号, email=anton@gmail.com]

    Guest [id=2, name=Gus, telephone=33355566, address=北京大道3号, email=gus@yahoo.com]

    Guest [id=3, name=李四, telephone=23452345, address=上海市南京路100号, email=lisi@gmail.com]

    模式3:共享数据库、共享 Schema、共享数据表

    在这种情况下,所有租户共享数据表存放数据,不同租户的数据通过 tenant_id 鉴别器来区分。但目前的 Hibernate 4 还不支持这个多租户鉴别器策略,要在

    5.0 才支持。但我们是否有可选的替代方案呢?答案是使用 Hibernate Filter.

    为了区分多个租户,我在 Schema 的每个数据表需要添加一个字段 tenant_id 以判定数据是属于哪个租户的。

    图 3. 共享 Schema、共享数据表案例 E-R 图

    e8d0b142fe33910fefbd20470329b6a7.png

    e8d0b142fe33910fefbd20470329b6a7.png

    根据上图在 MySQL 中创建 DATABASE hotel。

    我们在 OR-Mapping 配置文件中使用了 Filter,以便在进行数据查询时,会根据 tenant_id 自动查询出该租户所拥有的数据。

    清单 7. 对象关系映射文件

    Room.hbm.xml

    接下来我们在 HibernateUtil 类中通过 ThreadLocal 存放和获取 Hibernate Session,并将用户登录信息中的 tenantId 设置为

    tenantFilterParam 的参数值。

    清单 8. 获取 Hibernate Session 的工具类

    HibernateUtilpublic class HibernateUtil {

    public static final ThreadLocal session = new ThreadLocal();

    public static Session currentSession() throws HibernateException {

    Session s = session.get();

    if (s == null) {

    s = sessionFactory.openSession();

    String tenantId = LoginContext.getTenantId();

    s.enableFilter("tenantFilter").setParameter("tenantFilterParam", tenantId);

    session.set(s);

    }

    return s;

    }

    }

    不过 Filter 只是有助于我们读取数据时显示地忽略掉 tenantId,但在进行数据插入的时候,我们还是不得不显式设置相应 tenantId

    才能进行持久化。这种状况只能在 Hibernate5 版本中得到根本改变。

    清单 9. 运行程序 HotelServiceMain

    输出======当前可用房间列表======

    Room [ID=1, 房间编号=R1011, 位置=南楼, 床数=2, 状态=Free, 租户=新亚酒店]

    Room [ID=7, 房间编号=R7011, 位置=南楼, 床数=2, 状态=Free, 租户=新亚酒店]

    ======预订后======

    RentHistory [ID=1, 开始时间=2013-10-09 12:42:15.0, 结束时间=2013-10-12 12:42:15.0, Room ID=1, 房间编号=R1011, 房间状态=Booked, 金额=0.0元, 客人=Victor]

    ======结账后======

    RentHistory [ID=1, 开始时间=2013-10-09 12:42:15.0, 结束时间=2013-10-12 12:42:15.0, Room ID=1, 房间编号=R1011, 房间状态=Free, 金额=300.0元, 客人=Victor]

    多租户下的 Hibernate 缓存

    基于独立 Schema 模式的多租户实现,其数据表无需额外的 tenant_id。通过 ConnectionProvider 来取得所需的 JDBC

    连接,对其来说一级缓存(Session

    级别的缓存)是安全的可用的,一级缓存对事物级别的数据进行缓存,一旦事物结束,缓存也即失效。但是该模式下的二级缓存是不安全的,因为多个 Schema

    的数据库的主键可能会是同一个值,这样就使得 Hibernate 无法正常使用二级缓存来存放对象。例如:在 hotel_1 的 guest 表中有个 id 为 1

    的数据,同时在 hotel_2 的 guest 表中也有一个 id 为 1 的数据。通常我会根据 id 来覆盖类的 hashCode()

    方法,这样如果使用二级缓存,就无法区别 hotel_1 的 guest 和 hote_2 的 guest。

    在共享数据表的模式下的缓存, 可以同时使用 Hibernate的一级缓存和二级缓存,

    因为在共享的数据表中,主键是唯一的,数据表中的每条记录属于对应的租户,在二级缓存中的对象也具有唯一性。Hibernate 分别为

    EhCache、OSCache、SwarmCache 和 JBossCache 等缓存插件提供了内置的 CacheProvider

    实现,读者可以根据需要选择合理的缓存,修改 Hibernate 配置文件设置并启用它,以提高多租户应用的性能。

    EclipseLink

    EclipseLink 是 Eclipse 基金会管理下的开源持久层服务项目,为 Java 开发人员与各种数据服务(比如:数据库、web

    services、对象XML映射(OXM)、企业信息系统(EIS)等)交互提供了一个可扩展框架,目前支持的持久层标准中包括:Java Persistence API (JPA)

    Java Architecture for XML Binding (JAXB)

    Java Connector Architecture (JCA)

    Service Data Objects (SDO)

    EclipseLink 前身是 Oracle TopLink, 2007年 Oracle 将后者绝大部分捐献给了 Eclipse 基金会,次年 EclipseLink 被

    Sun 挑选成为 JPA 2.0 的参考实现。

    注: 目前 EclipseLink2.5 完全支持 2013 年发布的 JPA2.1(JSR 338) 。

    在完整实现 JPA 标准之外,针对 SaaS 环境,在多租户的隔离方面 EclipseLink 提供了很好的支持以及灵活地解决方案。

    应用程序隔离隔离的容器/应用服务器

    共享容器/应用服务器的应用程序隔离

    同一应用程序内的共享缓存但隔离的 entity manager factory

    共享的 entity manager factory 但每隔离的 entity manager

    数据隔离隔离的数据库

    隔离的Schema/表空间

    隔离的表

    共享表但隔离的行

    查询过滤

    Oracle Virtual Private Database (VPD)

    对于多租户数据源隔离主要有以下方案Single-Table Multi-tenancy,依靠租户区分列(tenant discriminator

    columns)来隔离表的行,实现多租户共享表。

    Table-Per-Tenant Multi-tenancy,依靠表的租户区分(table tenant

    discriminator)来隔离表,实现一租户一个表,大体类似于上文的共享数据库独立Schema模式。

    Virtual Private Database(VPD ) Multi-tenancy,依靠 Oracle VPD

    自身的安全访问策略(基于动态SQL where子句特性),实现多租户共享表。

    本节重点介绍多租户在 EclipseLink 中的共享数据表和一租户一个表的实现方法,并也以酒店多租户应用的例子展现共享数据表方案的具体实践。

    EclipseLink Annotation @Multitenant

    与 @Entity 或 @MappedSuperclass 一起使用,表明它们在一个应用程序中被多租户共享, 如清单 10。

    清单10.

    @Multitenant@Entity

    @Table(name="room")

    @Multitenant

    ...

    publicclass Room {

    }

    表 1. Multitenant 包含两个属性Annotation 属性描述缺省值boolean includeCriteria是否将租户限定应用到 select、update、delete 操作上true

    MultitenantType value多租户策略,SINGLE_TABLE,

    TABLE_PER_TENANT, VPD.SINGLE_TABLE

    共享数据表(SINGLE_TABLE)Metadata配置

    依靠租户区分列修饰符 @TenantDiscriminatorColumn 实现。

    清单11.

    @TenantDiscriminatorColumn@Entity

    @Table(name="hotel_guest")

    @Multitenant(SINGLE_TABLE)

    @TenantDiscriminatorColumn(name="tenant_id", contextProperty="tenant.id")

    publicclass HotelGuest {

    }

    或者在EclipseLink描述文件orm.xml定义对象与表映射时进行限制,两者是等价的。

    清单12.

    orm.xml

    ...

    属性配置

    租户区分列定义好后,在运行时环境需要配置具体属性值,以确定当前操作环境所属的租户。

    三种方式的属性配置,按优先生效顺序排序如下EntityManager(EM)

    EntityManagerFactory(EMF)

    Application context (when in a Java EE container)

    例如 EntityManagerFactory 可以间接通过在 persistence.xml 中配置持久化单元(Persistence

    Unit)或直接传属性参数给初始化时 EntityManagerFactory。

    清单 13. 配置

    persistence.xml

    ...

    ...

    或者

    清单 14. 初始化

    EntityManagerFactoryHashMap properties = new HashMap();

    properties.put("tenant_id", "人力资源部");

    ...

    EntityManager em = Persistence.createEntityManagerFactory("multi-tenant", properties).createEntityManager();

    按共享粒度可以作如下区分,EntityManagerFactory 级别

    用户需要通过 eclipselink.session-name 提供独立的会话名,确保每个租户占有独立的会话和缓存。

    清单 15. 为 EntityManagerFactory

    配置会话名HashMap properties = new HashMap();

    properties.put("tenant_id", "开发部");

    properties.put("eclipselink.session-name", "multi-tenant-dev");

    ...

    EntityManager em = Persistence.createEntityManagerFactory("multi-tenant", properties).createEntityManager();共享的 EntityManagerFactory 级别

    EntityManagerFactory 的默认模式, 此级别缺省配置为独立二级缓存(L2 cache), 即每个 mutlitenant 实体缓存设置为

    ISOLATED,用户也可设置 eclipselink.multitenant.tenants-share-cache 属性为真以共享,此时多租户 Entity

    缓存设置为 PROTECTED。

    这种级别下,一个活动的 EntityManager 不能更换 tenantId。EntityManager 级别

    这种级别下,共享 session,共享 L2 cache, 用户需要自己设置缓存策略,以设置哪些租户信息是不能在二级缓存共享的。

    清单 16.

    设置缓存HashMap tenantProperties = new HashMap();

    tenantProperties.put("tenant_id", "人力资源部");

    HashMap cacheProperties = new HashMap();

    cacheProperties.put("eclipselink.cache.shared.Employee", "false");

    cacheProperties.put("eclipselink.cache.size.Address", "10");

    cacheProperties.put("eclipselink.cache.type.Contract", "NONE");

    ...

    EntityManager em = Persistence.createEntityManagerFactory("multi-tenant", cacheProperties).createEntityManager(tenantProperties);

    同样,一个活动的EntityManager不能更换tenant ID。

    几点说明:每个表的区分列可以有任意多个,使用修饰符 TenantDiscriminatorColumns。

    清单 17.

    多个分区列@TenantDiscriminatorColumns({

    @TenantDiscriminatorColumn(name="tenant_id", contextProperty="tenant.id"),

    @TenantDiscriminatorColumn(name = "guest_id", contextProperty="guest.id")

    })租户区分列的名字和对应的上下文属性名可以取任意值,由应用程序开发者设定。

    生成的 Schema 可以也可以不包含租户区分列,如 tenant_id 或 guest_id。

    租户区分列可以映射到实体对象也可以不。

    注意:当映射的时候,实体对象相应的属性必须标记为只读(insertable=false, updatable=false),这种限制使得区分列不能作为实体表的

    identifier。TenantDiscriminatorColumn被以下 EntityManager 的操作和查询支持:

    persist,find,refresh,named queries,update all,delete all 。

    一租户一表(TABLE_PER_TENANT )

    这种多租户类型使每个租户的数据可以占据专属它自己的一个或多个表,多租户间的这些表可以共享相同 Schema

    也可使用不同的,前者使用前缀(prefix)或后缀(suffix)命名模式的表的租户区分符,后者使用租户专属的 Schema 名来定义表的租户区分符。Metadata配置

    依靠数据表的租户区分修饰符 @TenantTableDiscriminator 实现

    清单

    18.@Entity

    @Table(name=“CAR”)

    @Multitenant(TABLE_PER_TENANT)

    @TenantTableDiscriminator(type=SCHEMA, contextProperty="eclipselink-tenant.id")

    public class Car{

    }

    清单

    19.

    如前所述,TenantTableDiscriminatorType有 3 种类型:SCHEMA、SUFFIX 和 PREFIX。属性配置

    与另外两种多租户类型一样,默认情况下,多租户共享EMF,如不想共享 EMF,可以通过配置

    PersistenceUnitProperties.MULTITENANT_SHARED_EMF 以及

    PersistenceUnitProperties.SESSION_NAME 实现。

    清单

    20.// Shared EMF

    EntityManager em = createEntityManager(MULTI_TENANT_PU);

    em.getTransaction().begin();

    em.setProperty(EntityManagerProperties.MULTITENANT_PROPERTY_DEFAULT, "RLT");

    // Non shared EMF

    HashMap properties = new HashMap();

    properties.put(PersistenceUnitProperties.MULTITENANT_SHARED_EMF, "false");

    properties.put(PersistenceUnitProperties.SESSION_NAME, "non-shared-emf-for-rlt");

    properties.put(PersistenceUnitProperties.MULTITENANT_PROPERTY_DEFAULT, "RLT");

    ...

    EntityManager em = Persistence.createEntityManagerFactory("multi-tenant-pu", properties).createEntityManager();

    或在 persistence.xml 配置属性。

    酒店多租户应用实例(EclipseLink 共享(单)表)

    数据库 Schema 和测试数据与上文 Hibernate 实现相同,关于对象关系映射(OR mapping)的配置均采用 JPA 和 EclipseLink 定义的

    Java Annotation 描述。

    关于几个基本操作添加一个对象实例, 利用EntityManager.persist()

    清单 21.

    添加public void save(T t) {

    em.persist(t);

    }

    public void saveBulk(List bulk) {

    for(T t:bulk){

    em.persist(t);

    }

    }更新一个对象实例, 利用EntityManager.merge()

    清单 22.

    更新public void update(T t){

    em.merge(t);

    }查询, 利用EntityManager的NamedQuery,

    清单 23.

    多条件多结果查询protected List queryResultList(String queryName, Class clazz, String[] argNames, Object[] argValues){

    TypedQuery query = em.createNamedQuery(queryName, clazz);

    if(argNames != null && argValues != null){

    if(argNames.length != argValues.length){

    return null;

    }

    for(int i = 0; i < argNames.length; i++){

    query.setParameter(argNames[i], argValues[i]);

    }

    }

    return query.getResultList();

    }

    若用 JPQL 实现则示例如下:

    清单 24. JPQL NamedQuery

    定义@Entity

    @Table(name = "rent_history")

    @Multitenant

    @TenantDiscriminatorColumn(name="tenant_id", contextProperty="tenant.id")

    @NamedQueries({

    @NamedQuery(

    name="find_renthistory_by_hotel_guest_name",

    query="select h from RentHistory h, HotelGuest g where h.hotelGuestId=g.id and g.name=:hotelGuestName order by h.createTime DESC"

    ),

    })

    public class RentHistory implements Serializable {

    }

    部分测试数据如下(MySQL):

    hotel_admin

    33b39a360a2234a7eb4c3cf42efe30da.png

    33b39a360a2234a7eb4c3cf42efe30da.png

    hotel_guest

    af652a6f6dc6a387fcf56d6fa3f12c8d.png

    af652a6f6dc6a387fcf56d6fa3f12c8d.png

    room

    d0e0c66c1261ec40acdade03c67e5ef0.png

    d0e0c66c1261ec40acdade03c67e5ef0.png

    运行附件 MT_Test_Hotels.zip 中的测试代码(请参照 readme)来看看多租户的一些典型场景。

    清单 25.

    运行测试代码java -classpath build/classes/;D:/workspace/eclipselink/jlib/jpa/javax.persistence_2.1.0

    .v201304241213.jar;D:/workspace/eclipselink/jlib/eclipselink.jar;D:/workspace/mysql/mysql-connector-java-5.1.26/mysql-connector-java-5.1.26-bin.jar mtsample.hotel.test.TestHotelAdmin

    能得到输出片段如下:

    清单 26.

    输出*****登录*****

    用户名:letian

    密码:

    letian 已登录.

    ...

    ...

    *****查房*****

    1.获取可用房间

    Room [id=3, categoryId=3, createTime=2013-09-22 23:13:11.0, position=南楼, serialNumber=R2011, status=Free]

    Room [id=4, categoryId=4, createTime=2013-09-24 09:07:02.0, position=北楼, serialNumber=R2012, status=Free]

    ...

    ...

    *****入住*****

    请输入房间号:R2012

    请输入房客编号:4

    请输入入住时间(yyyy.MM.dd H:mm):2014.02.14 14:02

    请输入退房时间(yyyy.MM.dd H:mm):2014.02.17 11:00

    ...

    ...

    *****查房*****

    1.获取可用房间

    Room [id=3, categoryId=3, createTime=2013-09-26 13:02:49.0, position=南楼, serialNumber=R2011, status=Free]

    ...

    ...

    *****退房*****

    请输入房间号:R2012

    请输入退房时间(yyyy.MM.dd H:mm):2014.02.15 5:20

    ...

    ...

    1.获取所有入住历史信息

    RentHistory [id=3, amount=260.0, createTime=2013-09-26 13:07:48.0, endTime=Sat Feb 15 05:20:00 CST 2014, hotelGuestId=4, roomId=4, startTime=Fri Feb 14 14:02:00

    CST 2014, ]

    RentHistory [id=1, amount=1040.0, createTime=2013-09-17 13:28:00.0, endTime=Fri Sep 20 11:28:00 CST 2013, hotelGuestId=3, roomId=3, startTime=Wed Sep 18 13:28:00 CST 2013, ]

    ...

    ...

    *****查房*****

    1.获取可用房间

    Room [id=3, categoryId=3, createTime=2013-09-26 13:02:49.0, position=南楼, serialNumber=R2011, status=Free]

    Room [id=4, categoryId=4, createTime=2013-09-26 13:07:48.0, position=北楼, serialNumber=R2012, status=Free]

    通过共享表的测试数据以及运行结果可以看到,对于多个不同的租户(hotel_admin),在添加、查找、更新操作没有显示声明租户标识的情况下,EntityManager

    可以根据自身的租户属性配置

    实现租户分离。在本实例,EntityManager 初始化时利用到 hotel_admin 登录后的会话上下文进行租户判断,这里不再赘述。

    注:上文中提及的全部源码都可以在附件中找到。

    其它方面的考虑

    数据备份

    独立数据库和独立Sechma的模式,为每个租户备份数据比较容易,因为他们存放在不同的数据表中,只需对整个数据库或整个Schema进行备份。

    在共享数据表的模式下,可以将所有租户的数据一起备份,但是若要为某一个租户或按租户分开进行数据备份,就会比较麻烦。通常需要另外写sql脚本根据tenant_id来取得对应的数据然后再备份,但是要按租户来导入的话依然比较麻烦,所以必要时还是需要备份所有并为以后导入方便。

    性能

    独立数据库:性能高,但价格也高,需要占用资源多,不能共享,性价比低。

    共享数据库,独立 Schema:性能中等,但价格合适,部分共享,性价比中等。

    共享数据库,共享 Schema,共享数据表:性能中等(可利用 Cache

    可以提高性能),但价格便宜,完全共享,性价比高。如果在某些表中有大量的数据,可能会对所有租户产生性能影响。

    对于共享数据库的情况下,如果因为太多的最终用户同时访问数据库而导致应用程序性能问题,可以考虑数据表分区等数据库端的优化方案。

    经济考虑

    为了支持多租户应用,共享模式的应用程序往往比使用独立数据库模式的应用程序相对复杂,因为开发一个共享的架构,导致在应用设计上得花较大的努力,因而初始成本会较高。然而,共享模式的应用在运营成本上往往要低一些,每个租户所花的费用也会比较低。

    结束语

    多租户数据层方案的选择是一个综合的考量过程,包括成本、数据隔离与保护、维护、容灾、性能等。但无论怎样选择,OR-Mapping

    框架对多租户的支持将极大的解放开发人员的工作,从而可以更多专注于应用逻辑。最后我们以一个 Hibernate 和 EclipseLink 的比较来结束本文。

    表 2. Hibernate 与 EclipeLink 对多租户支持的比较HibernateEclipseLink独立数据库支持,通过实现 MultiTenantConnectionProvider

    接口以连接独立的数据库支持,为每个租户配置独立的 EntityManagerFactory

    共享数据库,独立 Schema支持,通过实现

    MultiTenantConnectionProvider 接口以切换 Schema支持,使用

    TABLE_PER_TENANT MultitenantType 以及 SCHEMA TenantTableDiscriminatorType

    共享数据库,共享 Schema,共享数据表多租户 Discriminator 计划在

    Hibernate 5.0 支持支持, 使用 SINGLE_TABLE MultitenantType 以及

    TenantDiscriminatorColumn

    下载资源代码示例 (Multi-tenancy-attachment.zip | 272k)

    相关主题

    展开全文
  • Java 多租户:配置选项、租户生命周期和所使用的隔离性IBM SDK Java Technology Edition(第 7 版的第 1 个发行版)中多租户实现的深入研究Gavin Rolleston 和 Michael Dawson2014 年 10 月 09 日发布获取 IBM SDK ...
  • 多租户适配 很多产品只有专属化版本,需要从产品底层进行尽量少的改造,满足上云之后多租户的数据、缓存、定时任务等隔离 多租户适配条目 条目名称 适配方案 持久层适配 支持schema和字段隔离两种方案 ...
  • 1 对多租户的理解多租户定义:多租户技术或称多重租赁技术,简称SaaS,是一种软件架构技术,是实现如何在多用户环境下(此处的多用户一般是面向企业用户)共用相同的系统或程序组件,并且可确保各用户间数据的隔离性。...
  • 什么是多租户多租户带来的好处: 多租户的几种模式 下面我们讲讲如何实现模式二的多租户 如何实现多租户? 项目中的技术方案: 实现步骤: 什么是多租户多租户技术(英语:multi-tenancy technology...
  • 我们知道,Oracle多租户选件是数据库原生支持的云管理能力,而不是像其它厂商采用云管理平台来实现多租户的功能,使得Oracle多租户选件可实现更高的资源利用率,更好的性能,更强的稳定性,和更简便的运维,从而广受...
  • 很多人对于一些SaaS技术问题还是知之甚少,例如企业在进行SaaS企业管理软件选型时,仍不了解“多租户”与“单租户”是什么意思,二者之间的区别更是一头雾水。企业管理者需要明白这两种SaaS架构的特点,才能更多地从...
  • 多租户系统架构

    2021-02-08 04:12:56
    多租户系统架构一种多租户系统架构背景:去年的时候,因为某些特殊原因,有幸带了一个组,参与了B2B平台的开发。说是B2B平台,因为这套程序开发完了后,可以拿给多个客户使用。客户可以搭建一套具有京东商城风格,...
  • 而与其名称后缀c,也就是cloud相对应,Oracle Database 12c引入了最为亮眼的多租户架构。由于最近工作环境中已经开始用12c,所以将一些知识总结一下。本文内容是有关12c的多租户架构的一些概念,来自Oracle Database...
  • 关于多租户架构

    2021-01-28 14:54:29
    多租户架构能使Oracle数据库可用作多租户容器数据库(CDB)一个多租户容器数据库(CDB)包含0个或多个自定义的可插拔数据库(PDBs)。PDB是一个包含schemas,schema objects,nonschema objects的集合。在12c之前,所有的...
  • 在基于Mybatis-plus实现多租户架构中,介绍了在多租户项目中如果要开启一个子线程,那么需要手动进行RequestAttributes的子线程共享。如果应用场景较少的话可能也不是特复杂,但是如果场景数量上来了,还是很容易...
  • 监控 - Thanos - 多租户

    千次阅读 2021-05-13 12:51:39
    监控 - Thanos - 多租户 文章目录监控 - Thanos - 多租户前言一、Thanos是什么?二、无租户或硬租户配置1.配置Prometheus多租户或软租户总结 前言 假设我们必须为两个团队Team Fruit和Team Veggie提供集中的度量...
  • 项目背景项目中SaaS服务需要提供多租户基础功能,通过访问域名区分不同的客户,进而隔离数据源,即一个租户一个数据库。AbstractRoutingDataSourcespring中对切换数据源提供了动态设置方法,通过...
  • seata与多租户 多租户模式为一个租户一个DataSource 隔离性比较好. 同时对业务拆分了微服务后就带来了分布式事务问题. 可以使用seata来解决这类问题.测试过程中对AT模式没有什么问题. 但是在TCC模式下commit/cache会...
  • 多租户技术

    千次阅读 2021-02-01 16:55:33
    1 多租户概念 多租户技术( Multi Tenancy Technology )又称多重租赁技术,用于实现如何在多用户的环境下共用相同的系统或程序组件,并且仍可确保各用户间数据的隔离性。 具体的多租户技术有多种,数据库通常有以下...
  • 一、引言小编先解释一下什么叫多租户,什么场景下使用多租户多租户是一种软件架构技术,在多用户的环境下,共有同一套系统,并且要注意数据之间的隔离性。举个实际例子:小编曾经开发过一套支付宝程序,这套程序...
  • 1、Redis多租户平台的设计方案 Redis多租户平台的设计图如下: Redis多租户平台分为三部分,分别是Redis管理后台、Redis的客户端、Redis集群。 对于Redis集群的部署,我们没有做持久化,通过一主一从数据冗余来保证...
  • 多租户介绍2.计算资源:cpu管控3.计算资源:内存管控4.存储资源:磁盘空间正文一、什么是多租户?所谓多租户:就是我们一套集群内,可以有多个用户使用,比如说云上,一个集群中有多个用户,大家都有自己的业务要...
  • 前言 saas 软件即服务 现在的软件...多租户更多跟云计算在一起,因为你有的客户需求大,付费多,那它分配的计算资源和功能更多,比如有自己独立的应用实例,数据库,硬盘空间等。 这个跟云计算的概念就差不多,云...
  • 云计算之多租户理解

    2021-08-11 19:31:10
    真正意义上的SaaS一定是多租户的,但是多租户根据隔离程度的不同又分为不同模式。 多租户根据隔离程度和共享程度分为三种模型,其实就是在共享程度与隔离程度的权衡选择。 共享程度越高,租户成本自然越低,技术实现...
  • 要将传统的私有化部署的软件重构成支持 SaaS 模式,多租户是一个迈不过去的坎,首先需要将系统改造成多租户模式,然后再逐步实现计费、系统监控、用户行为分析等功能。 我觉得多租户的设计应该分为三个层面来进行...
  • springboot搭建SaaS多租户动态数据源

    千次阅读 2020-12-19 23:54:10
    ③共享数据库,共享数据架构 即租户共享同一个数据库、同一个Schema,但在表中增加TenantID多租户的数据字段。这是共享程度最高、隔离级别最低的模式。 优点: 三种方案比较,第三种方案的维护和购置成本最低,允许...
  • 下面由Laravel教程栏目给人人引见在几分钟内使Laravel运用具有多租户功用的要领,愿望对须要的朋侪有所协助! 在本教程中,我们将运用 Tenancy for Laravel package让Laravel 运用完成多租户。它是一个多租户软件包...
  • 多租户是CDH里面非常重要的一部分,从一开始配置KDC到集成KDC,服务使用过程中都有可能会遇到各种各样的问题;下面我举例说下我当时遇过的问题,希望能帮助到大家服务启动错误KDC服务配置完成安装完成,CDH集成过程...
  • springboot多租户设计

    2021-01-19 12:43:35
    3.4 动态获取jdbcTemplate 这里注入了一个datasourceProvider,首先从request的header中获取租户的标志,然后通过datasourceProvider的selectDataSource方法获取数据源。 3.5 继续跟进 这里通过一个map存储着所有的...
  • 当我们在看Loki的架构文档时,社区都会宣称Loki是一个可以支持多租户模式下运行的日志系统,但我们再想进一步了解时,它却含蓄的表示Loki开启多租户只需要满足两个条件:配置文件中添加au...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 45,200
精华内容 18,080
关键字:

多租户