SaaS多租户架构--数据库
Nebula支持的SaaS架构有以下特点:
- 一份程序:所有租户共用相同版本的程序,可以部署为多个节点/POD
- 数据隔离:各个租户的业务数据采用不同的DataBase来实现数据隔离
数据存储架构
可参考下图
数据库分为3大类:
- 主库(红色):也称为 master库
- 存放租户元数据信息,例如租户管理表
- 只有一个DataBase
- 租户库(蓝色)
- 存放各个租户的业务数据,例如:订单,合同,收支金额等等
- 每个租户一个DataBase, 1万个租户就对应1万个DataBase
- 允许将DataBase划分到不同的数据库实例(Server)中
- 应用库(绿包)
- 存放特定用途的数据,例如:全局配置,日志,等等数据(不需要区分租户,且用途单一)
- DataBase数量不固定
数据访问
对于SaaS应用程序来说,数据访问操作的关键点其实就是:数据库连接的切换问题。
根据上图所示,切换数据库连接有4种场景:
- 访问主库
- 例如:需要校验 "租户ID" 这类操作
- 访问应用库
- 例如:读配置,写日志
- 用户登录
- 此时用户身份没有确定,不知道属于哪个租户
- 已登录用户访问租户业务数据
- 根据当前用户身份,访问对应的租户数据
下面来介绍如何实现以上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
用户登录(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);