SaaS多租户架构--数据库

Nebula支持的SaaS架构有以下特点:

  • 一份程序:所有租户共用相同版本的程序,可以部署为多个节点/POD
  • 数据隔离:各个租户的业务数据采用不同的DataBase来实现数据隔离



数据存储架构

可参考下图

xx

数据库分为3大类:

  • 主库(红色):也称为 master库
    • 存放租户元数据信息,例如租户管理表
    • 只有一个DataBase
  • 租户库(蓝色)
    • 存放各个租户的业务数据,例如:订单,合同,收支金额等等
    • 每个租户一个DataBase, 1万个租户就对应1万个DataBase
    • 允许将DataBase划分到不同的数据库实例(Server)中
  • 应用库(绿包)
    • 存放特定用途的数据,例如:全局配置,日志,等等数据(不需要区分租户,且用途单一)
    • DataBase数量不固定



数据访问

对于SaaS应用程序来说,数据访问操作的关键点其实就是:数据库连接的切换问题。

根据上图所示,切换数据库连接有4种场景:

  1. 访问主库
    • 例如:需要校验 "租户ID" 这类操作
  2. 访问应用库
    • 例如:读配置,写日志
  3. 用户登录
    • 此时用户身份没有确定,不知道属于哪个租户
  4. 已登录用户访问租户业务数据
    • 根据当前用户身份,访问对应的租户数据

下面来介绍如何实现以上4种场景的数据访问,示例代码全部用于Controller代码。



访问主库(Controller代码)

public string ConvertToTenantId(string tenantCode)
{
    string sql = "select tenantId from p_tenants where tenantCode = @tenantCode";
    var args = new { tenantCode };

    // 连接到主库
    using DbContext dbContext = this.CreateMasterConnection();

    return dbContext.CPQuery.Create(sql, args).ExecuteScalar<string>();
}

访问应用库(Controller代码)

private void WirteLog(string text)
{
    string sql = "insert into .....";
    var args = new { text };

    // 连接到应用库, "logging" 是连接名称,它注册在配置服务中
    using DbContext dbContext = this.CreateAppDbConnection("logging");
    dbContext.CPQuery.Create(sql, args).ExecuteNonQuery();
}

说明:

  • 主库和应用库的连接,必须事先在配置服务中注册,例如下图所示
  • 主库的连接名称固定为:master

xx



用户登录(Controller代码)

[HttpPost]
[LoginAction]    
[Route("login-demo.aspx")]
public int UserLoginDemo([Required][FromForm] string userCode, [Required][FromForm] string pwd, [Required][FromForm] string tenantCode)
{
    // 租户ID对用户不友好,用户只知道租户Code,所以转换得到 tenantId
    string tenantId = ConvertToTenantId(tenantCode);

    string pwdHash = HashHelper.Sha256(pwd);

    string sql = "select count(*) from LoginUsers where userCode = @userCode and pwd = @pwd";
    var args = new { userCode, pwd = pwdHash };

    // 连接到指定的租户数据库
    using( DbContext dbContext = this.CreateTenantConnection(tenantId) ) {

        // 查询数据库,判断用户名和密码是否正确
        DemoLoginUser user = dbContext.CPQuery.Create(sql, args).ToSingle<DemoLoginUser>();

        if( user != null ) {
            // 用户名密码正确
            WebUserInfo userInfo = new WebUserInfo { TenantId = tenantId, UserCode = userCode  /*  设置各数据成员  */  };
            AuthenticationManager.Login(userInfo, 3600 * 24 * 7);
            return 200;
        }
        else {
            // 用户名或者密码 不正确
            return 0;
        }
    }
}


已登录用户访问租户业务数据(Controller代码)

[Authorize]  // 确保只能【已登录用户】才能访问
[HttpGet]
[Route("demo2.aspx")]
public void ReadTenantData(int xxxId)
{
    string sql = "select * from TableXXX where id = @id";
    var args = new { id = xxxId };

    // 根据当前用户身份,切换到对应的租户库
    using DbContext dbContext = this.CreateTenantConnection();
    object data = dbContext.CPQuery.Create(sql, args).ToScalarList<string>();
}

说明:

  • CreateTenantConnection() 会根据当前【已登录】用户身份中的 TenantId 自动切换到对应的租户库
  • 如果当前用户没有登录,或者 用户身份的 TenantId 为空,则抛出异常!




在消息处理或者后台任务中访问数据库

可参考以下代码

// 打开 主库 的连接
using DbContext dbContext = DbConnManager.CreateMaster();

// 打开  某个应用库 的连接
using DbContext dbContext = DbConnManager.CreateAppDb("logging");

// 打开 某个租户库 的连接
using DbContext dbContext = DbConnManager.CreateTenant(tenantId);