postgresql schema隔离是beego多租户唯一可运维的默认方案:为每个租户分配独立schema(如tenant_abc),通过set search_path绑定连接,复用原orm抽象;须用sync.map缓存租户id→*sql.db映射并限制每实例maxopenconns,避免连接数爆炸。

PostgreSQL schema 隔离是 Beego 多租户唯一可运维的默认方案
字段级隔离(tenant_id)在 Beego 中极易失效,不是“加个 Filter("tenant_id", tid) 就安全”。Beego ORM 的 Raw()、Joins()、Count()、Unscoped() 等方法完全绕过任何全局过滤逻辑;Unscoped().Where("id = ?", x) 会直接查出全库数据。测试也难覆盖——单元测试常 mock DB 层,真实 SQL 漏条件根本跑不出来。
真正兜底的做法是:每个租户独占一个 PostgreSQL schema(如 tenant_abc),所有表建在该 schema 下,通过 SET search_path TO tenant_abc 绑定连接上下文。Beego 不需要改写表名(比如拼成 "tenant_abc.users"),而是复用原 ORM 抽象,仅初始化连接时执行一次 db.Exec("SET search_path TO " + tenantID),后续所有 QueryTable("users") 自动落在对应 schema。
- 必须用
sync.Map缓存租户 ID →*sql.DB实例映射,避免每次请求新建连接 - 每个租户专属的
*sql.DB必须调用SetMaxOpenConns(5),否则 1000 租户 × 默认 100 连接 = 10 万连接,PG 直接 OOM - Beego 的
orm.RegisterDriver和orm.RegisterDataBase要配合租户连接池使用,不能只注册一个全局 DB
MySQL 下硬上 schema 隔离等于放弃 Beego ORM 优势
MySQL 没有 search_path,也没有原生 schema 权限隔离能力。“每个租户一个 database”看似类比 PostgreSQL schema,实则导致 Beego ORM 彻底失能:
- 每个 database 对应一个独立
*sql.DB实例,Beego 的orm.RegisterDataBase无法动态切换 database,必须为每个租户预注册 —— 代码爆炸、配置不可维护 - 连接池无法复用:
database/sql的空闲连接、健康检查、超时回收全部按实例隔离,1000 租户就是 1000 套独立池子,K8s 下扩缩容时 CPU/内存抖动剧烈 - Beego ORM 的
QueryTable("users")会默认查defaultdatabase,你得手动在每处 DAO 写o.Using("tenant_123"),漏一处就跨租户
若必须用 MySQL,唯一可行路径是字段隔离 + 数据库层 RLS(如 MySQL 8.0 的 ROW ACCESS POLICY),但 Beego 本身不感知 RLS,需 DBA 配合强约束,且无法覆盖 Raw() 场景。
Beego 中如何安全透传租户上下文到 ORM 层
别把 tenant_id 塞进 context.Context 再一路传到 DAO —— Beego 的控制器、中间件、ORM 调用链太长,任意一层漏传或复用旧 context,数据就裸奔。正确做法是:在路由中间件中解析租户标识(如子域名、请求头、JWT claim),然后直接绑定到当前请求的数据库连接实例上。
- 在 Beego 的
Prepare()方法里获取租户 ID,并从sync.Map查找或新建租户专属*sql.DB - 将该
*sql.DB注入到当前请求的beego.Controller.Ctx.Input.Data或自定义字段,供 DAO 层直接取用 - DAO 层统一用
orm.NewOrmWithDB(db, driverName)构造 ORM 实例,不依赖全局注册的 DB - 绝对禁止在 DAO 层再做
tenant_id条件拼接 —— 那是字段隔离的残余思维,schema 隔离下它既冗余又危险
Beego 多租户项目结构与初始化关键点
标准 Beego 项目结构(controllers/、models/、routers/)本身不支持多租户,必须在初始化阶段注入租户感知能力。核心不在改目录,而在改入口和连接管理。
-
main.go中不要只调beego.Run(),要提前加载租户元数据(如从 Redis 或 PG 的tenants表读取活跃租户列表) -
routers/router.go的init()只注册通用路由,租户级路由由中间件动态处理,不走静态beego.Router -
models/里的 struct 定义保持干净,不加tenant_id字段 —— schema 隔离下它无意义且污染模型语义 - 首次建表必须带 schema 前缀:
CREATE TABLE tenant_abc.users (id SERIAL, name TEXT),Beego ORM 的SyncDatabase不支持跨 schema 同步,得用外部 migration 工具(如 Goose)驱动
最易被忽略的是连接池复用边界:sync.Map 缓存的是 *sql.DB,不是 orm.Ormer;每个 HTTP 请求都该用新 orm.Ormer,但底层 DB 实例必须复用。漏掉这层,性能和稳定性立刻崩盘。











