4种多租户数据库设计方案对比及思考,一文全讲透


前言

多租户是SaaS(Software-as-a-Service)下的一个概念,意思为软件即服务,即通过网络提供软件服务。

SaaS平台供应商将应用软件统一部署在自己的服务器上,客户可以根据工作的实际需求,通过互联网向厂商定购所需的应用软件服务,按定购的服务多少和时间长短向厂商支付费用,并通过互联网获得SaaS平台供应商提供的服务。SaaS服务尤其利于一些中小企业,以低成本实现自己的软件需求。

在这里插入图片描述

就如企业微信,它就是一个典型的多租户系统。每在企业微信上注册一个企业,也就是多租户下创建一个租户。企业微信提供各种插件、服务、第三方支持供租户购买,拓展系统功能,可以说是天花板的存在,他们的产品设计是很值得我们参考与学习的。

我本人也是对各种多租户开源项目进行过研究,参与开发过不少类型的多租户系统,对多租户系统的设计有一些自己的见解,本期就来跟大家分享一下关于多租户DB的设计方案。


一、设计方案

多租户对于用户来说,最主要的一点就在于数据隔离,不可以说,我登了A用户的号,但是看到了B用户的数据。

因此,多租户的数据库设计方案和代码实现就相当有必要考虑了。

当下,开发者们普遍接受的多租户设计方案,常见的大概就四种:

  • 所有租户使用同一数据源下同一数据库下共同数据表(单数据源单数据库单数据表)
  • 所有租户使用同一数据源下同一数据库下不同数据表(单数据源单数据库多数据表)
  • 所有租户使用同一数据源下不同数据库下不同数据表(单数据源多数据库多数据表)
  • 所有租户使用不同数据源下不同数据库下不同数据表(多数据源多数据库多数据表)

二、方案剖析

注:对于本节,我们主要采用开源的mysql数据库来分析设计

  • 方案一:单数据源单数据库单数据表

这种方案是我目前见过的最普遍的设计方案。

该系统只有一个数据库,所有租户共用数据表。在每一个数据表中增加一列租户ID,用以区分租户的数据。增删查改时,一定要带上租户ID,否则就会操作到其他租户的数据。因此,这里的设计一定要重点考虑!

一个重要的点:我们要保证的就是一定不要忘记带上租户ID。一个很好的方案就是通过AOP的方案,隐式的为我们的每一个SQL带上这个租户ID。

我个人是更喜欢使用MyBatis来操作数据库的。它提供了插件的机制,我们可以通过拦截它提供的四大组件的某些对象,某些方法,来操作SQL,动态的为我们的SQL拼接上租户ID字段。

当然,MyBatis-Plus高版本提供了更加方便的拦截器,并且已经将多租户插件放入JAR包,我们只需稍加实现,并将该插件加入到MyBatis的拦截器链中,就可以不用再显式的拼接租户ID字段了,降低了出错的概率。

因为篇幅有限,关于MyBatis和MyBatis-Plus插件这里就不做太多知识延伸,如果有兴趣可以留言我专门写一期MyBatis插件的文章。

这里我放一下MyBatis-Plus关于多租户的主要代码 ⬇

@Configuration
@MapperScan("com.baomidou.mybatisplus.samples.tenant.mapper")
public class MybatisPlusConfig {

    /**
     * 新多租户插件配置,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存万一出现问题
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
            @Override
            public Expression getTenantId() {
                return new LongValue(1);
            }

            // 这是 default 方法,默认返回 false 表示所有表都需要拼多租户条件
            @Override
            public boolean ignoreTable(String tableName) {
                return !"user".equalsIgnoreCase(tableName);
            }
        }));
        // 如果用了分页插件注意先 add TenantLineInnerInterceptor 再 add PaginationInnerInterceptor
        // 用了分页插件必须设置 MybatisConfiguration#useDeprecatedExecutor = false
        // interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return interceptor;
    }

      // @Bean
      // public ConfigurationCustomizer configurationCustomizer() {
      // return configuration -> configuration.setUseDeprecatedExecutor(false);
      // }
}

也就是说,我们只需要根据我们的业务,重写getTenantId()方法和ignoreTable()方法即可。

ignoreTable()其实没啥说的,主要还是getTenantId()这个方法。最方便的方法当然是使用ThreadLocal存储当前线程用户的租户信息,然后直接从里面取出即可。

如果使用了SpringSecurity,用户信息和租户信息也可以存他的内置ThreadLocal中。

一般来说,开源框架都会提供一个SecurityUtil工具,可以直接拿到当前线程的用户信息和租户信息。当然,也可以自己实现,这并不困难。

有了这个思路,其他的数据库也可以使用类似的方法实现隐式租户ID赋值,这里我就不再举例。

  • 方案二:单数据源单数据库多数据表

这个方案是我之前项目所使用的,架构师是一个十几年经验的大牛,我觉得这个思路也是很好的,我可以详细的说一下:

这个项目使用的是MongoDB数据库,MongoDB使用的是类似于Json的Bson语法,实现了类似SQL的功能,使用Bson进行增删查改,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。

它的好处就是,不用太过于关心数据库的建表,因为它的存储类似于一行一行的json对象,插入一条数据即可理解为建表。表增加一行或者减少一行这种变化也不需要去修改数据表的结构。其次,表名也可以自己来指定。

项目的设计方案是,**所有租户的数据都放在同一数据库中,不同租户的数据表使用后缀来区分。**比如租户A的用户表叫user_001,用户B的用户表叫user_002。这样,就做到了相对于方案一而言更可靠的设计。

缺点也很明显,这种数据库难以实现连表查询,只能进行多次单表查询,对事务的控制基本为0。

一般来说,对于小场景而言,这种架构的设计已经完全够用了。服务器和数据库都使用了阿里云的云服务器和云数据库,数据的可靠性由他们来保证,我们只需要专心写代码即可,发生错误的概率也是非常低的。就算真的以极小的概率发生了数据问题,对于当时项目的规模和成本考虑来说,查一下审计日志,手动做一下数据的处理就可以解决了。

实际上,这个项目还是一个电商项目,当时同时支持了3个租户同时使用,并没有出现很大的问题。因此,在我们的设计中,也要根据场景进行架构的选择,没必要追求过于超前的设计,毕竟越稳定越花钱,成本决定了很多因素,通过对这个项目的研究和与领导的沟通,我也收获颇多。

接下来,我们回头再说一下MySQL的设计方案,毕竟MySQL免费,并且支持事务和多表查询,所以它永远是我们的最先考虑的。

对MySQL而言,新建租户时需要建表,每次建表,都要使用DDL建新租户所使用的所有表。我自己的做法是,定义建表的DDL,当需要创建租户时,使用MyBatis执行SQL脚本。

我自己的实现方案是在XML里面定义SQL语句,项目启动时,解析XML到配置,存为Key-Value形式,并提供一个执行SQL的工具,当需要创建租户时,调用SQL工具并传入配置参数,即可实现自动建表。

下面给出我的具体实现:

Step 1

提供一个XML,记录SQL的对应关系,这里我没有写DTD约束。如果怕出错,可以自己加上,也可以在里面定义标签,标注需要替换的键值。

我这里做的比较简单,**后期使用对应的键值替换SQL中的 x x x 元素 ∗ ∗ ,比如我这里后期使 用 0 01 替换这个 {xxx}元素**,比如我这里后期使用_001替换这个 xxx元素,比如我这里后期使001替换这个{tenant_id},这样就实现了动态生成创建新租户表的DDL。

<?xml version="1.0" encoding="UTF-8"?>
<document>
    <statement>
        <name>create-tenant</name>
        <script>

            DROP TABLE IF EXISTS `sys_user${tenant_id}`;
            CREATE TABLE `sys_user${tenant_id}`
            (
            `user_id`         bigint(20) UNSIGNED                                           NOT NULL AUTO_INCREMENT COMMENT '用户编号',
            `account_user_id` bigint(20) UNSIGNED                                           NOT NULL COMMENT '账户用户编号',
            `nickname`        varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '昵称',
            `real_name`       varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '姓名',
            `phone`           char(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci     NOT NULL COMMENT '手机号',
            `avatar`          varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户头像',
            `email`           varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户邮箱',
            `gender`          tinyint(4) UNSIGNED                                           NOT NULL COMMENT '性别[enum]',
            `department_id`   bigint(20) UNSIGNED                                           NOT NULL COMMENT '部门编号',
            `enable`          tinyint(4) UNSIGNED                                           NOT NULL COMMENT '启用禁用[enum]',
            `last_login_time` datetime(0)                                                   NOT NULL COMMENT '最后登录时间',
            `last_login_ip`   varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '最后登录IP',
            `create_time`     datetime(0)                                                   NOT NULL COMMENT '创建时间',
            `update_time`     datetime(0)                                                   NOT NULL COMMENT '更新时间',
            `create_user_id`  bigint(20) UNSIGNED                                           NOT NULL COMMENT '创建人编号',
            `update_user_id`  bigint(20) UNSIGNED                                           NOT NULL COMMENT '更新人编号',
            `is_deleted`      tinyint(4) UNSIGNED                                           NOT NULL DEFAULT 0 COMMENT '逻辑删除[enum]',
            PRIMARY KEY (`user_id`) USING BTREE
            ) ENGINE = InnoDB
            AUTO_INCREMENT = 2
            CHARACTER SET = utf8mb4
            COLLATE = utf8mb4_general_ci
            ROW_FORMAT = Dynamic;

          ......
          
        </script>
    </statement>
</document>

Step 2

编写配置类,解析XML到JAVA,使用hashMap保存SQL的对应关系。这里我使用的是dom4j,当然你也可以选择其他解析器。


/**
 * @author 谷子毅
 * @date 2022/4/2
 */
@Component
public class ExecSqlConfig {

    private final Map<String, String> sqlContainer = new HashMap<>();

    private ExecSqlConfig() throws Exception {
        // 1.创建Reader对象
        SAXReader reader = new SAXReader();
        // 2.加载xml
        InputStream inputStream = new ClassPathResource("sql-script.xml").getInputStream();
        Document document = reader.read(inputStream);
        // 3.获取根节点
        Element root = document.getRootElement();

        // 4.遍历每个statement
        List<Element> statements = root.elements("statement");
        for (Element statement : statements) {
            String name = null;
            String sql = null;
            List<Element> elements = statement.elements();
            // 5.拿到name和script加载到内存中管理
            for (Element element : elements) {
                if ("name".equals(element.getName())) {
                    name = element.getText();
                } else if ("script".equals(element.getName())) {
                    sql = element.getText();
                }
            }
            sqlContainer.put(name, sql);
        }
    }

    public String get(String name) {
        return sqlContainer.get(name);
    }
}

Step 3

编写SQL的执行器工具类,具体的用法就是,通过key从hashMap中取出对应DDL类型的SQL模板,对模版中的替换值进行替换,然后就生成了可以执行的SQL,使用MyBatis执行即可。


/**
 * sql脚本执行工具
 * @author 谷子毅
 * @date 2022/4/2
 */
public class ExecSqlUtil {

    private static final ExecSqlConfig EXEC_SQL_CONFIG = SpringContextHolder.getBean(ExecSqlConfig.class);

    private static final DataSource DATA_SOURCE = SpringContextHolder.getBean(DataSource.class);

    @SneakyThrows
    public static void execSql(String name, Map<String, String> replaceMap) {
        // 获取SQL脚本模板
        String sql = EXEC_SQL_CONFIG.get(name);

        // 替换模板变量
        for (Map.Entry<String, String> entity : replaceMap.entrySet()) {
            sql = sql.replace(entity.getKey(), entity.getValue());
        }
        ScriptRunner scriptRunner = new ScriptRunner(DATA_SOURCE.getConnection());

        // 执行SQL
        scriptRunner.runScript(new StringReader(sql));
    }

}

Step 4

最后,在创建租户的地方,使用SQL执行工具类执行SQL即可。

public void addTenant(TenantInsertDTO dto) {
  
    // 在租户表中新增租户信息
    ...... 

    // 执行sql语句创建新租户的数据库表
    ExecSqlUtil.execSql(SqlStatement.CREATE_TENANT, Collections.singletonMap("${tenant_id}", dto.getTenantId()));
  
    // 向新建的数据表中插入租户的一些初始数据
    ...... 

以上即为建表方案,不过需要注意,如果租户表有变化,所有的租户表都需要同时进行相应修改,否则就会出现问题。

说完建表,那下一步需要关心的还是增删查改的问题。这个问题同样也可以使用MyBatis的插件解决,MyBatis-Plus同样给我们写好了代码放到了JAR包中,也就是动态表名插件。

这里我也只放一下MyBatis-Plus关于动态表名插件的主要代码 ⬇


@Configuration
@MapperScan("com.baomidou.mybatisplus.samples.dytablename.mapper")
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        DynamicTableNameInnerInterceptor dynamicTableNameInnerInterceptor = new DynamicTableNameInnerInterceptor();
        dynamicTableNameInnerInterceptor.setTableNameHandler((sql, tableName) -> {
            // 获取参数方法
            Map<String, Object> paramMap = RequestDataHelper.getRequestData();
            paramMap.forEach((k, v) -> System.err.println(k + "----" + v));

            String year = "_2018";
            int random = new Random().nextInt(10);
            if (random % 2 == 1) {
                year = "_2019";
            }
            return tableName + year;
        });
        interceptor.addInnerInterceptor(dynamicTableNameInnerInterceptor);
        // 3.4.3.2 作废该方式
        // dynamicTableNameInnerInterceptor.setTableNameHandlerMap(map);
        return interceptor;
    }
}

它这里的动态表名还可以实现类似于分表的操作,比如按时间分表,2021年的数据放到order_001_2021,2022年的数据放到order_001_2011。

我们这里主要讨论的还是关于租户的分表,我们将代码稍加改造,即为:

dynamicTableNameInnerInterceptor.setTableNameHandler((sql, tableName) -> {
    if(IgnoreTables.contains(tableName)) {
      return tableName;
    }
    String tenantId = SecurityUtil.getTenantId();
    return tableName + "_" + tenantId;
});

如果当前操作的表名在忽略表名集合中,则不拼接租户ID,否则所有的表名后面都会自动的被插件拼接上租户ID。

如原SQL的查询select * from user就会变成select * from user_001。

  • 方案三:单数据源多数据库多数据表

方案三则是将租户数据彻底隔离,给每个租户创建一个数据库。基本上和方案二类似,关于好处和坏处我们后面统一讨论,还是先说一下这个实现方案。

同样的,新建租户的时候需要建库建表,那么我们是可以直接继续使用方案二的,将SQL模板稍加改造即可,经过上一方案的代码剖析,这个应该没有难度。

<?xml version="1.0" encoding="UTF-8"?>
<document>
    <statement>
        <name>create-tenant</name>
        <script>
            CREATE DATABASE ${database};

            USE ${database};
            
            DROP TABLE IF EXISTS `sys_user`;
            CREATE TABLE `sys_user`
            (
            `user_id`         bigint(20) UNSIGNED                                           NOT NULL AUTO_INCREMENT COMMENT '用户编号',
            `account_user_id` bigint(20) UNSIGNED                                           NOT NULL COMMENT '账户用户编号',
            `nickname`        varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '昵称',
            `real_name`       varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '姓名',
            `phone`           char(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci     NOT NULL COMMENT '手机号',
            `avatar`          varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户头像',
            `email`           varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户邮箱',
            `gender`          tinyint(4) UNSIGNED                                           NOT NULL COMMENT '性别[enum]',
            `department_id`   bigint(20) UNSIGNED                                           NOT NULL COMMENT '部门编号',
            `enable`          tinyint(4) UNSIGNED                                           NOT NULL COMMENT '启用禁用[enum]',
            `last_login_time` datetime(0)                                                   NOT NULL COMMENT '最后登录时间',
            `last_login_ip`   varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '最后登录IP',
            `create_time`     datetime(0)                                                   NOT NULL COMMENT '创建时间',
            `update_time`     datetime(0)                                                   NOT NULL COMMENT '更新时间',
            `create_user_id`  bigint(20) UNSIGNED                                           NOT NULL COMMENT '创建人编号',
            `update_user_id`  bigint(20) UNSIGNED                                           NOT NULL COMMENT '更新人编号',
            `is_deleted`      tinyint(4) UNSIGNED                                           NOT NULL DEFAULT 0 COMMENT '逻辑删除[enum]',
            PRIMARY KEY (`user_id`) USING BTREE
            ) ENGINE = InnoDB
            AUTO_INCREMENT = 2
            CHARACTER SET = utf8mb4
            COLLATE = utf8mb4_general_ci
            ROW_FORMAT = Dynamic;
          
          ......
          
        </script>
    </statement>
</document>

不过我们需要注意的是,当前是在MySQL环境下,Database的概念和Scheme的概念是等价的。其实在其他数据库中,还可以存在database-scheme-table的结构,这里不说太多,如果你真正用过多种数据库后,你就理解我的意思了。

在完成数据表的创建后,然后就是具体的增删改查问题。我们还是可以使用MyBatis插件,动态的改造表名,在表名前拼接上数据库名,具体的实现也是非常简单,直接拿方案二稍微改改即可。

最后的效果是,将原SQL语句select * from user 动态改成了select * from db_001.user,连表查询也是可用的。

dynamicTableNameInnerInterceptor.setTableNameHandler((sql, tableName) -> {
    if(IgnoreTables.contains(tableName)) {
      return tableName;
    }
    String tenantId = SecurityUtil.getTenantId();
    return tenantId + "." + tableName;
});

以上三个方案都是单数据源的情况,使用这种单数据源方案可以轻松实现跨数据库执行SQL和事务执行。

我自己的开源项目sherly-SpringBoot目前也是这样设计的。不过我的项目主要是针对中小型项目设计的,因此,我认为这已经完全够用了,后面我还会开发针对大数据量的微服务框架来专门解决大数据量的问题。

  • 方案四:多数据源多数据库多数据表

在单数据源中,跨数据库操作和数据库事务是已经被支持的。

但是在多数据源中,传统的跨库操作和数据库事务已经难以适用,甚至有时候会出现动态的新增租户,并且租户数据源动态配置等问题。

这时,光靠简单的几百行代码已经无法实现。首先我们试着想想如何自己实现该需求,这里我给大家提供几个方案来看看:

  • 使用AOP加注解动态切换数据源
  • 使用MyBatis插件切换数据源(可以比较方便的判断SQL类型,区分读写操作)
  • 多个MyBatis配置来引入多个数据源(比较占内存)

具体的实现方案就是在配置文件中配一个公用数据源。项目启动后,从数据库加载所有租户的数据源信息,创建DataSource并注入到容器,供三种方案下一步使用。

好的,我们已经解决了一个多数据源切换问题,那么多数据源的事务应该怎么控制呢?

如果不考虑多租户和分布式事务方案的话,简单的业务多数据源处理,我们可以使用声明式事务,稍微麻烦一点,A数据源操作->B数据源操作->出现异常。

这种场景,我们使用一下编程式事务貌似也可以解决,但是代码已经很丑了,更别说多租户这种动态选择数据源的场景了,简直难度陡升。

通过以上分析,我们发现多数据源下多租户确实难以简单实现,光涉及到我提到的两点,就已经让人头痛了。

所以,我们就需要借助第三方框架了,那就是Dynamic-datasource。它是一个基于SpringBoot的快速集成多数据源的启动器,通过它的辅助,我们就可以实现方案四的多数据源下多租户问题。

下面我先把他的一些特性列出来:

在这里插入图片描述

这个开源组件的功能是非常强大的,大家可以自己拉Git仓库研究,在本节中,我们只讨论它是如何帮助我们实现多租户的多数据源问题。

我的设计方案是:

首先,默认只加载公共数据源,然后创建一个数据源工厂接口,提供一个获取租户数据源接口,实现就是传入租户ID,查询数据库获取租户数据库连接信息,创建并返回租户数据源,存入数据源容器。若淘汰,从数据源容器删除即可。

再通过Seata的分布式事务,就可以完美解决我们的问题了。

在创建租户时,也可以初始化表结构和数据库。当然,你也可以项目启动后,直接把所有的租户数据源查出来并放到容器,这个根据自己的业务来选择。

这里设计非常复杂,我就先提供思路吧。我会在我的下一个微服务开源项目中实现这一方案,可以期待一下。这时有人问,那如果是超大型系统,微服务,多部署又怎么办呢?怎么做到数据源容器的新增和删除同步呢?

当然,我考虑到了,可以试试Redis的listener,让所有的机器监听一个事件消息。通过这个第三方组件,再根据自己的业务稍加改造,所有问题便迎刃而解了。


三、方案总结

上面说了那么多,是时候做一下总结。

从方案一到方案四,数据的隔离性是越来越好的,成本也越来越高,但是也会出现越来越多的问题,就比方说有数据汇总的需求时,租户数据都在一个表中和分散在不同表中,处理难度是不一样的。

  • 方案一总结

该方案成本最低,安全性也是最低,所有数据都放在一起,单个租户的数据恢复和备份会很复杂。

比如,有ABC三个租户共同使用,当C租户已经确定以后不再使用了,他的数据也不好从数据库中剔除。如果某一数据出现了问题,也不便于租户问题的排查。

这就得保证对开发者的严苛要求,最主要还是增加了对数据的管理复杂度。尤其注意,尽量不要去手动显式的设置租户ID到SQL,否则一旦出现问题,纠正是很复杂的,在这上面我吃过亏…

如果要做数据汇总,我们就想办法使用插件或其他方案去解决就行了。这和数据出错的影响比起来,这点设计也不值一提了。

  • 方案二总结

该方案提供了一定的数据隔离,但不是完全的隔离,由于数据库是共享的,所以成本也不是很高。

  • 方案三总结

该方案其实和方案二大同小异,对单数据源来说,都不是彻底的隔离。成本和方案二差不多。

不过我认为这样可以更方便的删除测试租户数据库和设置数据库权限,比如给A租户管理员的A数据库的查看权限,给B租户管理员的B数据库的查看权限,这就涉及到数据库的权限问题了,其实很多时候,很多的公司,都给了开发者数据库的root权限。

  • 方案四总结

这种多数据源方案,目前也有一些开源项目实现了这一方案。就是为不同的租户提供独立的数据库,数据库由租户自己购买,或商家购买,然后配置到系统中,满足不同租户的独特需求,如果出现故障,数据恢复也比较简单。

代价是什么呢?

这种方案增加了数据库的安装数量,单个数据库的资源利用率就不高了,而且数据库的数量一多,成本就高了,安全性的代价就是成本。还要考虑到多数据库的事务问题,多数据库的数据源切换问题,这代码复杂度和成本噌噌噌就上来了,相当费钱。


四、方案选型

  • 数据隔离性方面:

关于隔离性最常见的场景就是给多个银行开发的系统,那安全性是不言而喻的,甚至是容不得半点闪失。

此时的方案,我们一二三都不用考虑,可以直接选择方案四。方案四如果出现故障,恢复数据比较简单,但是相比方案三和方案二,他们数据恢复会复杂一些,因为会涉及到其他租户的数据,方案一就更不用说了,对数据的恢复都是行级别的。

  • 数据量方面:

如果只是一些后台的中小型项目,比如xx管理系统,那么我们大可以放心的优先使用单数据源方案。

如果租户有自己提供数据库的需求,那么就选方案四。但是,一旦数据量变大,比如涉及C端大量客户的系统,单数据源是难以扛住这个压力的,我们就不得不选用方案四来解决。

而且,方案四下的同一个租户我们也可以配置多个数据源,做读写分离,一主多从,多数据库,分库分表,以及分布式数据库如tidb等都是可以实现的,所以,它能够支持的数据量就很可怕了,就算是多服务也无所畏惧了。

  • 成本方面:

从单数据源和多数据源来看,多数据源不仅需要部署更多的数据库服务器,也会带来资源的利用率不充分问题,成本自然就高了。

但是,如果是中小型项目,我们大可使用更节约成本的方案一二三。如果只是租户数量多,但是每个租户数据量不大,访问量不大,我们也可以使用MyCat中间件,做一个虚假的单数据源,实现数据库的扩容,了解分布式数据库的,还是拿开源的tidb举例,基本上也可以支持数据的无感扩容,在必要的时候进行容量的增减,这样资源的利用率就很高了,按需扩容即可。


五、引申问题的解决方案

多数据库多数据表方案的数据汇总:可以使用mongoDB等大数据工具,收集数据并进行清洗、分析、汇总,生成汇总报告。

大型项目的部署方案:多部署时,使用分布式组件,如Redis,保证租户配置的一致性,使用分布式锁解决同时执行定时任务等问题。拆分成微服务也是一种很好的方案。

数据扩容:多数据源时,可以根据情景任意配置数据库的部署策略;单数据源时,使用数据库中间件,将多个数据源模拟成单数据源或者简单粗暴,直接使用分布式数据库即可。


六、写在最后

具体问题具体分析,一定要根据需求做出正确的决策。

看清自己的数据量需求,客户需求,并发需求。对于数据量特别大的项目,一定要在项目初期就考虑好成本问题,以及数据库的设计方案,一旦判断失误,那必然会损失惨重!

我也是在11月初参加了系统架构设计师的考试,在学习复习和实践中成长,本文即为我对多租户数据库的设计方案的思考和探索,希望能给大家带来启发,也欢迎留言和我一起探讨~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值