Go简单实现多租户数据库隔离的方法

多租户

在 SaaS 系统中多租户是一个很重要的架构,在服务上仅需运行一个软件实例就能够支持每个租户的功能,它们之间的数据互相隔离。就比如 Gitee ,每个用户或组织都拥有自己的空间。它们自己的数据仅在自己的空间内流通,彼此之间互不影响。多租户其实就是一种软件设计结构。
所以。租户模型其实就是分离数据的方式:

  • 单租户:一个应用一个数据库。其实就是应用分离,数据分离。

  • 多租户:一个应用多个数据库。就是同一个应用不同数据库。

为什么需要多租户?

刚毕业第一家公司,接触的第一个项目的内容主要帮助客户提高生产力。每次有一个很大的问题就是,合作一个客户,项目复制一份出来,然后改成该客户定制的功能需求。这样就导致相同功能要更新多份代码,还要注意可能会出现因不同客户的不同需求问题。整个过程就导致了一个很大的维护成本的支出。

多租户应用程序的维护更容易,只需根据每个租户专属标志,然后提供不同的功能,且公共的功能可以集中更新升级。

数据隔离方式

在 SaaS 系统中一般分离租户的数据有两种模型:

  • 数据的逻辑分离:所有租户只使用一个数据库。它们的数据通过为每个租户使用一个唯一标识符来分隔。

  • 数据的物理分离:一个租户分配一个数据库。就是一个应用对应多个数据库。每个数据库就是一个租户。这种方式在客户的增长时扩展了应用的功能,也方便扩展数据库

物理分离详细实现

目标:每个请求过来,应用程序能够识别租户,并从该租户数据库中进行提供数据。

  • 公共数据库:存储所有租户相关全局配置和所有租户数据库信息。

  • 租户数据库:每个租户独立的的数据库,根据租户需要保存数据。

启动服务

创建 main 文件,并使用 Gin 框架。初始化一个简单的应用程序并创建基本的路由。

func main() {    r := gin.Default()    r.GET("/ping", func(c *gin.Context) {        c.JSON(http.StatusOK, gin.H{            "message": "pong",        })    })    r.Run()}

数据库配置

在公共数据库创建一个保存租户数据库信息表并添加上租户的数据库。

Create Table db_tenant (    id int primary key,    db_name varchar(100) unique not null,    db_domain  varchar(100) unique not null,    conn_str varchar(200) not null,    remark varchar(1000) ,)

在配置文件中加入一个公共数据库的连接配置,这样就可以在服务启动时第一次连接的是公共数据库。接着需要实现根据请求连接到正确的租户数据库。主要使用 gorm 的 DBResolver ,多个数据库的连接支持

func allDbConnect(db *db.gorm) (allDbs map[string]) {     db.Model(&db_tenant).Find(&allDbs)    var gconn []gorm.Dialector      for _,connStr := range allDbs {       db :=mysql.Open(connStr)                gconn = append(gconn,db)    }db.Use(dbresolver.Register(dbresolver.Config{  Sources:  gconn,  // sources/replicas 负载均衡策略  Policy: dbresolver.RandomPolicy{},})   return allDbs}

通过使用中间件来解析每个请求连接,从而确定整个请求是由哪个租户数据库来提供数据读写。中间件中主要的处理方式是得到租户对应的数据库连接名,然后从连接池中拿到该连接并设置在全局变量中。在业务逻辑处理中从全局拿到数据库连接,就可以进行数据库读写。

sr.Use(middlerware.dbResolve)func dbResolve(ctx *gin.Context) {    dbName := ctx.Query("dbName")    setGlobalDb(dbName)}func dbOperater(){    db := getDb()  var users []User  db.Clauses(dbresolver.Use(dbName)).First(&users)}

这样就可以启动项目,然后访问 localhost:8003?dbName=db1 ,这样就可以正常进行数据读写。

其实可以从域名上解析对应的数据库连接。这里最重要就是对数据库的解析。这里的例子只是实验使用,其中还有很多可优化的点。比如可以考虑使用连接池,这样就不用在项目启动时都全部创建数据库连接,造成浪费。